"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; 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(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); // 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 && (

{hoverInfo.feature.properties.nama || hoverInfo.feature.properties.kecamatan}

{hoverInfo.feature.properties.number_of_crime !== undefined && (

{hoverInfo.feature.properties.number_of_crime} incidents {hoverInfo.feature.properties.level && ( ({hoverInfo.feature.properties.level}) )}

)}
)} {/* District popup */} {selectedDistrict && selectedDistrict.longitude && selectedDistrict.latitude && ( setSelectedDistrict(null)} district={selectedDistrict} year={year} month={month} /> )} ); }