MIF_E31221222/sigap-website/app/_components/map/layers/district-layer.tsx

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}
/>
)}
</>
);
}