265 lines
7.4 KiB
TypeScript
265 lines
7.4 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';
|
|
|
|
// 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`;
|
|
}
|
|
} |