MIF_E31221222/sigap-website/app/_utils/map.ts

285 lines
8.0 KiB
TypeScript

import { $Enums } from '@prisma/client';
import { CRIME_RATE_COLORS } from '@/app/_utils/const/map';
import type { ICrimes } from '@/app/_utils/types/crimes';
import { IDistrictFeature } from './types/map';
import { ITooltipsControl } from '../_components/map/controls/top/tooltips';
// Process crime data by district
export const processCrimeDataByDistrict = (crimes: ICrimes[]) => {
return crimes.reduce(
(acc, crime) => {
const districtId = crime.district_id;
acc[districtId] = {
number_of_crime: crime.number_of_crime,
level: crime.level,
};
return acc;
},
{} as Record<
string,
{ number_of_crime?: number; level?: $Enums.crime_rates }
>
);
};
// Get color for crime rate level
export const getCrimeRateColor = (level?: $Enums.crime_rates) => {
if (!level) return CRIME_RATE_COLORS.default;
switch (level) {
case 'low':
return CRIME_RATE_COLORS.low;
case 'medium':
return CRIME_RATE_COLORS.medium;
case 'high':
return CRIME_RATE_COLORS.high;
default:
return CRIME_RATE_COLORS.default;
}
};
// Create fill color expression for district layer
export const createFillColorExpression = (
focusedDistrictId: string | null,
crimeDataByDistrict: Record<
string,
{ number_of_crime?: number; level?: $Enums.crime_rates }
>
) => {
const colorEntries = focusedDistrictId
? [
[
focusedDistrictId,
getCrimeRateColor(crimeDataByDistrict[focusedDistrictId]?.level),
],
'rgba(0,0,0,0.05)',
]
: Object.entries(crimeDataByDistrict).flatMap(([districtId, data]) => {
return [districtId, getCrimeRateColor(data.level)];
});
return [
'case',
['has', 'kode_kec'],
[
'match',
['get', 'kode_kec'],
...colorEntries,
focusedDistrictId ? 'rgba(0,0,0,0.05)' : CRIME_RATE_COLORS.default,
],
CRIME_RATE_COLORS.default,
];
};
// Extract crime incidents for GeoJSON
export const extractCrimeIncidents = (
crimes: ICrimes[],
filterCategory: string | 'all'
) => {
return crimes.flatMap((crime) => {
if (!crime.crime_incidents) return [];
let filteredIncidents = crime.crime_incidents;
if (filterCategory !== 'all') {
filteredIncidents = crime.crime_incidents.filter(
(incident) =>
incident.crime_categories &&
incident.crime_categories.name === filterCategory
);
}
return filteredIncidents
.map((incident) => {
if (!incident.locations) {
console.warn('Missing location for incident:', incident.id);
return null;
}
return {
type: 'Feature' as const,
properties: {
id: incident.id,
district: crime.districts?.name || 'Unknown',
category: incident.crime_categories?.name || 'Unknown',
incidentType: incident.crime_categories?.type || 'Unknown',
level: crime.level || 'low',
description: incident.description || '',
status: incident.status || '',
},
geometry: {
type: 'Point' as const,
coordinates: [
incident.locations.longitude || 0,
incident.locations.latitude || 0,
],
},
};
})
.filter(Boolean);
});
};
// Process district feature from map click
export const processDistrictFeature = (
feature: any,
e: any,
districtId: string,
crimeDataByDistrict: Record<
string,
{ number_of_crime?: number; level?: $Enums.crime_rates }
>,
crimes: ICrimes[],
year: string,
month: string
): IDistrictFeature | null => {
const crimeData = crimeDataByDistrict[districtId] || {};
let crime_incidents: Array<{
id: string;
timestamp: Date;
description: string;
status: string;
category: string;
type: string;
address: string;
latitude: number;
longitude: number;
}> = [];
const districtCrimes = crimes.filter(
(crime) => crime.district_id === districtId
);
districtCrimes.forEach((crimeRecord) => {
if (crimeRecord && crimeRecord.crime_incidents) {
const incidents = crimeRecord.crime_incidents.map((incident) => ({
id: incident.id,
timestamp: incident.timestamp,
description: incident.description || '',
status: incident.status || '',
category: incident.crime_categories?.name || '',
type: incident.crime_categories?.type || '',
address: incident.locations?.address || '',
latitude: incident.locations?.latitude || 0,
longitude: incident.locations?.longitude || 0,
}));
crime_incidents = [...crime_incidents, ...incidents];
}
});
const firstDistrictCrime =
districtCrimes.length > 0 ? districtCrimes[0] : null;
if (!firstDistrictCrime) return null;
const selectedYearNum = year
? Number.parseInt(year)
: new Date().getFullYear();
let demographics = firstDistrictCrime?.districts.demographics?.find(
(d) => d.year === selectedYearNum
);
if (!demographics && firstDistrictCrime?.districts.demographics?.length) {
demographics = firstDistrictCrime.districts.demographics.sort(
(a, b) => b.year - a.year
)[0];
console.log(
`Tidak ada data demografis untuk tahun ${selectedYearNum}, menggunakan data tahun ${demographics.year}`
);
}
let geographics = firstDistrictCrime?.districts.geographics?.find(
(g) => g.year === selectedYearNum
);
if (!geographics && firstDistrictCrime?.districts.geographics?.length) {
const validGeographics = firstDistrictCrime.districts.geographics
.filter((g) => g.year !== null)
.sort((a, b) => (b.year || 0) - (a.year || 0));
geographics =
validGeographics.length > 0
? validGeographics[0]
: firstDistrictCrime.districts.geographics[0];
console.log(
`Tidak ada data geografis untuk tahun ${selectedYearNum}, menggunakan data ${geographics.year ? `tahun ${geographics.year}` : 'tanpa tahun'}`
);
}
const clickLng = e.lngLat ? e.lngLat.lng : null;
const clickLat = e.lngLat ? e.lngLat.lat : null;
if (!geographics) {
console.error('Missing geographics data for district:', districtId);
return null;
}
if (!demographics) {
console.error('Missing demographics data for district:', districtId);
return null;
}
return {
id: districtId,
name:
feature.properties.nama ||
feature.properties.kecamatan ||
'Unknown District',
longitude: geographics.longitude || clickLng || 0,
latitude: geographics.latitude || clickLat || 0,
number_of_crime: crimeData.number_of_crime || 0,
level: crimeData.level || $Enums.crime_rates.low,
demographics: {
number_of_unemployed: demographics.number_of_unemployed,
population: demographics.population,
population_density: demographics.population_density,
year: demographics.year,
},
geographics: {
address: geographics.address || '',
land_area: geographics.land_area || 0,
year: geographics.year || 0,
latitude: geographics.latitude,
longitude: geographics.longitude,
},
crime_incidents: crime_incidents || [],
selectedYear: year,
selectedMonth: month,
isFocused: true,
};
};
/**
* Format distance in a human-readable way
* @param meters Distance in meters
* @returns Formatted distance string
*/
export function formatDistance(meters: number): string {
if (meters < 1000) {
return `${Math.round(meters)} m`;
} else {
return `${(meters / 1000).toFixed(1)} km`;
}
}
// Helper function to determine fill opacity based on active control
export function getFillOpacity(activeControl?: ITooltipsControl, showFill?: boolean): number {
if (!showFill) return 0
// Full opacity for incidents and clusters
if (activeControl === "incidents" || activeControl === "clusters" || activeControl === "units") {
return 0.6
}
// Low opacity for timeline to show markers but still see district boundaries
if (activeControl === "timeline") {
return 0.1
}
// No fill for other controls, but keep boundaries visible
return 0
}