264 lines
9.2 KiB
TypeScript
264 lines
9.2 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useState, useRef } from 'react';
|
|
import { useMap } from 'react-map-gl/mapbox';
|
|
import { CRIME_RATE_COLORS, MAPBOX_TILESET_ID } from '@/app/_utils/const/map';
|
|
import { DistrictPopup } from '../pop-up';
|
|
|
|
// Types for district properties
|
|
export interface DistrictFeature {
|
|
id: string;
|
|
name: string;
|
|
properties: Record<string, any>;
|
|
longitude?: number;
|
|
latitude?: number;
|
|
number_of_crime?: number;
|
|
level?: 'low' | 'medium' | 'high' | 'critical';
|
|
}
|
|
|
|
// District layer props
|
|
export interface DistrictLayerProps {
|
|
visible?: boolean;
|
|
onClick?: (feature: DistrictFeature) => void;
|
|
year?: string;
|
|
month?: string;
|
|
crimes?: Array<{
|
|
id: string;
|
|
district_name: string;
|
|
distrcit_id?: string;
|
|
number_of_crime?: number;
|
|
level?: 'low' | 'medium' | 'high' | 'critical';
|
|
incidents: any[];
|
|
}>;
|
|
tilesetId?: string;
|
|
}
|
|
|
|
export default function DistrictLayer({
|
|
visible = true,
|
|
onClick,
|
|
year,
|
|
month,
|
|
crimes = [],
|
|
tilesetId = MAPBOX_TILESET_ID
|
|
}: DistrictLayerProps) {
|
|
const { current: map } = useMap();
|
|
|
|
const [hoverInfo, setHoverInfo] = useState<{
|
|
x: number;
|
|
y: number;
|
|
feature: any;
|
|
} | null>(null);
|
|
const [selectedDistrict, setSelectedDistrict] = useState<DistrictFeature | null>(null);
|
|
|
|
// Use a ref to track whether layers have been added
|
|
const layersAdded = useRef(false);
|
|
|
|
// Process crime data to map to districts by district_id (kode_kec)
|
|
const crimeDataByDistrict = crimes.reduce((acc, crime) => {
|
|
// We'll use kode_kec as the key to match with tileset properties
|
|
const districtId = crime.distrcit_id || crime.district_name;
|
|
|
|
acc[districtId] = {
|
|
number_of_crime: crime.number_of_crime,
|
|
level: crime.level,
|
|
};
|
|
return acc;
|
|
}, {} as Record<string, { number_of_crime?: number; level?: 'low' | 'medium' | 'high' | 'critical' }>);
|
|
|
|
// Handle click on district
|
|
const handleClick = (e: any) => {
|
|
if (!map || !e.features || e.features.length === 0) return;
|
|
|
|
const feature = e.features[0];
|
|
const districtId = feature.properties.kode_kec; // Using kode_kec as the unique identifier
|
|
const crimeData = crimeDataByDistrict[districtId] || {};
|
|
|
|
const district: DistrictFeature = {
|
|
id: districtId,
|
|
name: feature.properties.nama || feature.properties.kecamatan,
|
|
properties: feature.properties,
|
|
longitude: e.lngLat.lng,
|
|
latitude: e.lngLat.lat,
|
|
...crimeData,
|
|
};
|
|
|
|
if (onClick) {
|
|
onClick(district);
|
|
} else {
|
|
setSelectedDistrict(district);
|
|
}
|
|
};
|
|
|
|
// Handle mouse move for hover effect
|
|
const handleMouseMove = (e: any) => {
|
|
if (!map || !e.features || e.features.length === 0) return;
|
|
|
|
const feature = e.features[0];
|
|
const districtId = feature.properties.kode_kec; // Using kode_kec as the unique identifier
|
|
const crimeData = crimeDataByDistrict[districtId] || {};
|
|
|
|
// Enhance feature with crime data
|
|
feature.properties = {
|
|
...feature.properties,
|
|
...crimeData,
|
|
};
|
|
|
|
setHoverInfo({
|
|
x: e.point.x,
|
|
y: e.point.y,
|
|
feature: feature,
|
|
});
|
|
};
|
|
|
|
// Add district layer to the map when it's loaded
|
|
useEffect(() => {
|
|
if (!map || !visible || layersAdded.current) return;
|
|
|
|
// Handler for style load event
|
|
const onStyleLoad = () => {
|
|
// Skip if layers are already added or map is not available
|
|
if (layersAdded.current || !map) return;
|
|
|
|
try {
|
|
// Add the vector tile source
|
|
map.getMap().addSource('districts', {
|
|
type: 'vector',
|
|
url: `mapbox://${tilesetId}`
|
|
});
|
|
|
|
// Add the fill layer for districts
|
|
map.getMap().addLayer({
|
|
id: 'district-fill',
|
|
type: 'fill',
|
|
source: 'districts',
|
|
'source-layer': 'Districts',
|
|
paint: {
|
|
'fill-color': [
|
|
'match',
|
|
['get', 'level'],
|
|
'low', CRIME_RATE_COLORS.low,
|
|
'medium', CRIME_RATE_COLORS.medium,
|
|
'high', CRIME_RATE_COLORS.high,
|
|
'critical', CRIME_RATE_COLORS.critical,
|
|
CRIME_RATE_COLORS.default
|
|
],
|
|
'fill-opacity': 0.6,
|
|
}
|
|
});
|
|
|
|
// Add the line layer for district borders
|
|
map.getMap().addLayer({
|
|
id: 'district-line',
|
|
type: 'line',
|
|
source: 'districts',
|
|
'source-layer': 'Districts',
|
|
paint: {
|
|
'line-color': '#ffffff',
|
|
'line-width': 1,
|
|
'line-opacity': 0.5,
|
|
}
|
|
});
|
|
|
|
// Set event handlers
|
|
map.on('click', 'district-fill', handleClick);
|
|
map.on('mousemove', 'district-fill', handleMouseMove);
|
|
map.on('mouseleave', 'district-fill', () => setHoverInfo(null));
|
|
|
|
// Mark layers as added
|
|
layersAdded.current = true;
|
|
console.log('District layers added successfully');
|
|
} catch (error) {
|
|
console.error('Error adding district layers:', error);
|
|
}
|
|
};
|
|
|
|
// If the map's style is already loaded, add the layers immediately
|
|
if (map.isStyleLoaded()) {
|
|
onStyleLoad();
|
|
} else {
|
|
// Otherwise, wait for the style.load event
|
|
map.once('style.load', onStyleLoad);
|
|
}
|
|
|
|
// Cleanup function
|
|
return () => {
|
|
if (map && layersAdded.current) {
|
|
map.off('click', 'district-fill', handleClick);
|
|
map.off('mousemove', 'district-fill', handleMouseMove);
|
|
map.off('mouseleave', 'district-fill', () => setHoverInfo(null));
|
|
|
|
// If we want to remove the layers and source on component unmount:
|
|
if (map.getLayer('district-line')) map.getMap().removeLayer('district-line');
|
|
if (map.getLayer('district-fill')) map.getMap().removeLayer('district-fill');
|
|
if (map.getSource('districts')) map.getMap().removeSource('districts');
|
|
|
|
layersAdded.current = false;
|
|
}
|
|
};
|
|
}, [map, visible, tilesetId]);
|
|
|
|
// Update the crime data when it changes
|
|
useEffect(() => {
|
|
if (!map || !layersAdded.current) return;
|
|
|
|
// Update the district-fill layer with new crime data
|
|
try {
|
|
// We need to update the layer paint property to correctly apply colors
|
|
map.getMap().setPaintProperty('district-fill', 'fill-color', [
|
|
'match',
|
|
['coalesce', ['get', 'level'], 'default'],
|
|
'low', CRIME_RATE_COLORS.low,
|
|
'medium', CRIME_RATE_COLORS.medium,
|
|
'high', CRIME_RATE_COLORS.high,
|
|
'critical', CRIME_RATE_COLORS.critical,
|
|
CRIME_RATE_COLORS.default
|
|
]);
|
|
} catch (error) {
|
|
console.error('Error updating district layer:', error);
|
|
}
|
|
}, [map, crimes]);
|
|
|
|
if (!visible) return null;
|
|
|
|
return (
|
|
<>
|
|
{/* Hover tooltip */}
|
|
{hoverInfo && (
|
|
<div
|
|
className="absolute z-10 bg-white rounded-md shadow-md px-3 py-2 pointer-events-none"
|
|
style={{
|
|
left: hoverInfo.x + 10,
|
|
top: hoverInfo.y + 10,
|
|
}}
|
|
>
|
|
<p className="text-sm font-medium">
|
|
{hoverInfo.feature.properties.nama || hoverInfo.feature.properties.kecamatan}
|
|
</p>
|
|
{hoverInfo.feature.properties.number_of_crime !== undefined && (
|
|
<p className="text-xs text-gray-600">
|
|
{hoverInfo.feature.properties.number_of_crime} incidents
|
|
{hoverInfo.feature.properties.level && (
|
|
<span className="ml-2 text-xs font-semibold text-gray-500">
|
|
({hoverInfo.feature.properties.level})
|
|
</span>
|
|
)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* District popup */}
|
|
{selectedDistrict && selectedDistrict.longitude && selectedDistrict.latitude && (
|
|
<DistrictPopup
|
|
longitude={selectedDistrict.longitude}
|
|
latitude={selectedDistrict.latitude}
|
|
onClose={() => setSelectedDistrict(null)}
|
|
district={selectedDistrict}
|
|
year={year}
|
|
month={month}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|