"use client" import { useEffect, useState, useRef, useCallback } from "react" import { useMap } from "react-map-gl/mapbox" import { CRIME_RATE_COLORS, MAPBOX_TILESET_ID } from "@/app/_utils/const/map" import { $Enums } from "@prisma/client" import DistrictPopup from "../pop-up/district-popup" // Types for district properties export interface DistrictFeature { id: string name: string properties: Record longitude: number latitude: number number_of_crime: number level: $Enums.crime_rates demographics: { number_of_unemployed: number population: number population_density: number year: number } geographics: { address: string land_area: number year: number latitude: number longitude: number } crime_incidents: Array<{ id: string timestamp: Date description: string status: string category: string type: string address: string latitude: number longitude: number }> selectedYear?: string selectedMonth?: string } // Updated interface to match the structure returned by getCrimeByYearAndMonth export interface ICrimeData { id: string district_id: string districts: { name: string geographics: { address: string land_area: number year: number latitude: number longitude: number } demographics: { number_of_unemployed: number population: number population_density: number year: number } } number_of_crime: number level: $Enums.crime_rates score: number month: number year: number crime_incidents: Array<{ id: string timestamp: Date description: string status: string crime_categories: { name: string type: string } locations: { address: string latitude: number longitude: number } }> } // District layer props export interface DistrictLayerProps { visible?: boolean onClick?: (feature: DistrictFeature) => void year?: string month?: string filterCategory?: string | "all" crimes?: ICrimeData[] tilesetId?: string } export default function DistrictLayer({ visible = true, onClick, year, month, filterCategory = "all", crimes = [], tilesetId = MAPBOX_TILESET_ID, }: DistrictLayerProps) { const { current: map } = useMap() const [hoverInfo, setHoverInfo] = useState<{ x: number y: number feature: any } | null>(null) // Menggunakan useRef untuk menyimpan informasi distrik yang dipilih // sehingga nilainya tidak hilang saat komponen di-render ulang const selectedDistrictRef = useRef(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) => { // Use district_id (which corresponds to kode_kec in the tileset) as the key const districtId = crime.district_id 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] || {} // Get ALL crime_incidents for this district by aggregating from all matching crime records let crime_incidents: Array<{ id: string timestamp: Date description: string status: string category: string type: string address: string latitude: number longitude: number }> = [] // Find all crime data records for this district (across all months) const districtCrimes = crimes.filter(crime => crime.district_id === districtId) console.log(`Found ${districtCrimes.length} crime data records for district ID ${districtId}`) // Collect all crime incidents from all month records for this district 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] } }) console.log(`Aggregated ${crime_incidents.length} total crime incidents for district`) // Get demographics and geographics from the first record (should be the same across all records) const firstDistrictCrime = districtCrimes.length > 0 ? districtCrimes[0] : null const demographics = firstDistrictCrime?.districts.demographics const geographics = firstDistrictCrime?.districts.geographics // Make sure we have valid coordinates from the click event 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 } if (!demographics) { console.error("Missing demographics data for district:", districtId) return } // Create a complete district object ensuring all required properties are present const district: DistrictFeature = { id: districtId, name: feature.properties.nama || feature.properties.kecamatan || "Unknown District", properties: feature.properties, longitude: geographics.longitude || clickLng || 0, latitude: geographics.latitude || clickLat || 0, // Use the total crime count across all months number_of_crime: crimeData.number_of_crime || 0, // Use the level from the currently selected month/year (or default to low) level: crimeData.level || $Enums.crime_rates.low, demographics, geographics, // Include all aggregated crime incidents crime_incidents: crime_incidents || [], selectedYear: year, selectedMonth: month } if (!district.longitude || !district.latitude) { console.error("Invalid district coordinates:", district); return; } console.log("Selected district:", district); console.log(`Selected district has ${district.crime_incidents?.length || 0} crime_incidents out of ${district.number_of_crime} total crimes`); // Set the reference BEFORE handling the onClick or setState selectedDistrictRef.current = district; if (onClick) { onClick(district); } else { setSelectedDistrict(district); } } // Pastikan event handler klik selalu diperbarui // dan re-attach setiap kali ada perubahan data useEffect(() => { if (!map || !visible || !map.getMap().getLayer("district-fill")) return; // Re-attach click handler map.off("click", "district-fill", handleClick); map.on("click", "district-fill", handleClick); console.log("Re-attached click handler, current district:", selectedDistrict?.name || "None"); return () => { if (map) { map.off("click", "district-fill", handleClick); } }; }, [map, visible, crimes, filterCategory, year, month]); // Add district layer to the map when it's loaded useEffect(() => { if (!map || !visible) return; const onStyleLoad = () => { if (!map) return; try { if (!map.getMap().getSource("districts")) { const layers = map.getStyle().layers let firstSymbolId: string | undefined for (const layer of layers) { if (layer.type === "symbol") { firstSymbolId = layer.id break } } map.getMap().addSource("districts", { type: "vector", url: `mapbox://${tilesetId}`, }) const fillColorExpression: any = [ "case", ["has", "kode_kec"], [ "match", ["get", "kode_kec"], ...Object.entries(crimeDataByDistrict).flatMap(([districtId, data]) => { return [ districtId, data.level === "low" ? CRIME_RATE_COLORS.low : data.level === "medium" ? CRIME_RATE_COLORS.medium : data.level === "high" ? CRIME_RATE_COLORS.high : CRIME_RATE_COLORS.default, ] }), CRIME_RATE_COLORS.default, ], CRIME_RATE_COLORS.default, ] if (!map.getMap().getLayer("district-fill")) { map.getMap().addLayer( { id: "district-fill", type: "fill", source: "districts", "source-layer": "Districts", paint: { "fill-color": fillColorExpression, "fill-opacity": 0.6, }, }, firstSymbolId, ) } if (!map.getMap().getLayer("district-line")) { map.getMap().addLayer( { id: "district-line", type: "line", source: "districts", "source-layer": "Districts", paint: { "line-color": "#ffffff", "line-width": 1, "line-opacity": 0.5, }, }, firstSymbolId, ) } if (crimes.length > 0 && !map.getMap().getSource("crime-incidents")) { const allIncidents = crimes.flatMap((crime) => { let filteredIncidents = crime.crime_incidents if (filterCategory !== "all") { filteredIncidents = crime.crime_incidents.filter( incident => incident.crime_categories.name === filterCategory ) } return filteredIncidents.map((incident) => ({ type: "Feature" as const, properties: { id: incident.id, district: crime.districts.name, category: incident.crime_categories.name, incidentType: incident.crime_categories.type, level: crime.level, description: incident.description, }, geometry: { type: "Point" as const, coordinates: [incident.locations.longitude, incident.locations.latitude], }, })) }) map.getMap().addSource("crime-incidents", { type: "geojson", data: { type: "FeatureCollection", features: allIncidents, }, cluster: true, clusterMaxZoom: 14, clusterRadius: 50, }) if (!map.getMap().getLayer("clusters")) { map.getMap().addLayer( { id: "clusters", type: "circle", source: "crime-incidents", filter: ["has", "point_count"], paint: { "circle-color": [ "step", ["get", "point_count"], "#51bbd6", 5, "#f1f075", 15, "#f28cb1", ], "circle-radius": [ "step", ["get", "point_count"], 20, 5, 30, 15, 40, ], "circle-opacity": 0.75, }, }, firstSymbolId, ) } if (!map.getMap().getLayer("cluster-count")) { map.getMap().addLayer({ id: "cluster-count", type: "symbol", source: "crime-incidents", filter: ["has", "point_count"], layout: { "text-field": "{point_count_abbreviated}", "text-font": ["DIN Offc Pro Medium", "Arial Unicode MS Bold"], "text-size": 12, }, paint: { "text-color": "#ffffff", }, }) } if (!map.getMap().getLayer("unclustered-point")) { map.getMap().addLayer( { id: "unclustered-point", type: "circle", source: "crime-incidents", filter: ["!", ["has", "point_count"]], paint: { "circle-color": "#11b4da", "circle-radius": 8, "circle-stroke-width": 1, "circle-stroke-color": "#fff", }, }, firstSymbolId, ) } map.on("click", "clusters", (e) => { const features = map.queryRenderedFeatures(e.point, { layers: ["clusters"] }) if (!features || features.length === 0) return const clusterId = features[0].properties?.cluster_id ; (map.getSource("crime-incidents") as mapboxgl.GeoJSONSource).getClusterExpansionZoom( clusterId, (err, zoom) => { if (err) return map.easeTo({ center: (features[0].geometry as any).coordinates, zoom: zoom ?? undefined, }) }, ) }) map.on("mouseenter", "clusters", () => { map.getCanvas().style.cursor = "pointer" }) map.on("mouseleave", "clusters", () => { map.getCanvas().style.cursor = "" }) map.on("mouseenter", "unclustered-point", () => { map.getCanvas().style.cursor = "pointer" }) map.on("mouseleave", "unclustered-point", () => { map.getCanvas().style.cursor = "" }) } // Rebind the click event listener for "district-fill" map.on("click", "district-fill", handleClick); // Ensure hover info is cleared when leaving the layer map.on("mouseleave", "district-fill", () => setHoverInfo(null)); layersAdded.current = true; } else { if (map.getMap().getLayer("district-fill")) { map.getMap().setPaintProperty("district-fill", "fill-color", [ "case", ["has", "kode_kec"], [ "match", ["get", "kode_kec"], ...Object.entries(crimeDataByDistrict).flatMap(([districtId, data]) => { return [ districtId, data.level === "low" ? CRIME_RATE_COLORS.low : data.level === "medium" ? CRIME_RATE_COLORS.medium : data.level === "high" ? CRIME_RATE_COLORS.high : CRIME_RATE_COLORS.default, ] }), CRIME_RATE_COLORS.default, ], CRIME_RATE_COLORS.default, ] as any) } } } catch (error) { console.error("Error adding district layers:", error) } }; if (map.isStyleLoaded()) { onStyleLoad(); } else { map.once("style.load", onStyleLoad); } return () => { if (map) { // Remove the click event listener when the component unmounts or dependencies change map.off("click", "district-fill", handleClick); } }; }, [map, visible, tilesetId, crimes, filterCategory, year, month]); useEffect(() => { if (!map || !layersAdded.current) return try { if (map.getMap().getLayer("district-fill")) { // Create a safety check for empty or invalid data const colorEntries = Object.entries(crimeDataByDistrict).flatMap(([districtId, data]) => { if (!data || !data.level) { return [ districtId, CRIME_RATE_COLORS.default ] } return [ districtId, data.level === "low" ? CRIME_RATE_COLORS.low : data.level === "medium" ? CRIME_RATE_COLORS.medium : data.level === "high" ? CRIME_RATE_COLORS.high : CRIME_RATE_COLORS.default, ] }) const fillColorExpression = [ "case", ["has", "kode_kec"], [ "match", ["get", "kode_kec"], ...colorEntries, CRIME_RATE_COLORS.default, ], CRIME_RATE_COLORS.default, ] as any map.getMap().setPaintProperty("district-fill", "fill-color", fillColorExpression) } } catch (error) { console.error("Error updating district layer:", error) } }, [map, crimes, crimeDataByDistrict]) useEffect(() => { if (!map || !map.getMap().getSource("crime-incidents")) return try { const allIncidents = crimes.flatMap((crime) => { // Make sure we handle cases where crime_incidents might be undefined if (!crime.crime_incidents) return [] // Apply category filter if specified 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) => { // Handle possible null/undefined values 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 || "", }, geometry: { type: "Point" as const, coordinates: [ incident.locations.longitude || 0, incident.locations.latitude || 0 ], }, } }).filter(Boolean) // Remove null values }); (map.getMap().getSource("crime-incidents") as mapboxgl.GeoJSONSource).setData({ type: "FeatureCollection", features: allIncidents as GeoJSON.Feature[], }) } catch (error) { console.error("Error updating incident data:", error) } }, [map, crimes, filterCategory]) // Effect khusus untuk memastikan state selectedDistrict dipertahankan // ketika data berubah (tahun/bulan diubah) useEffect(() => { // Jika kita memiliki district yang dipilih dalam ref, pastikan // state juga diperbarui dengan data terbaru if (selectedDistrictRef.current) { // Cari crime data terbaru untuk district yang dipilih const districtId = selectedDistrictRef.current.id; const crimeData = crimeDataByDistrict[districtId] || {}; // Cari data district terkini const districtCrime = crimes.find(crime => crime.district_id === districtId); // Perbarui data district dengan informasi terkini if (districtCrime) { const demographics = districtCrime.districts.demographics; const geographics = districtCrime.districts.geographics; // Filter crime_incidents const crime_incidents = districtCrime.crime_incidents .filter(incident => filterCategory === "all" || incident.crime_categories.name === filterCategory ) .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, longitude: incident.locations.longitude })); // Buat district object baru dengan data terkini const updatedDistrict: DistrictFeature = { ...selectedDistrictRef.current, ...crimeData, demographics, geographics, crime_incidents, selectedYear: year, selectedMonth: month }; // Perbarui ref tetapi BUKAN state di sini selectedDistrictRef.current = updatedDistrict; // Gunakan functional update untuk menghindari loop // Hanya update jika berbeda dari state sebelumnya setSelectedDistrict(prevDistrict => { // Jika sudah sama, tidak perlu update if (prevDistrict?.id === updatedDistrict.id && prevDistrict?.selectedYear === updatedDistrict.selectedYear && prevDistrict?.selectedMonth === updatedDistrict.selectedMonth) { return prevDistrict; } return updatedDistrict; }); console.log("Updated selected district with new data:", updatedDistrict.name); } } }, [crimes, filterCategory, year, month]); // hapus crimeDataByDistrict dari dependencies const handleIncidentClick = useCallback((e: any) => { if (!map) return; // Try to query for crime_incidents at the click location const features = map.queryRenderedFeatures(e.point, { layers: ["unclustered-point"] }) if (!features || features.length === 0) return const incident = features[0] if (!incident.properties) return // Prevent the click from propagating to other layers e.originalEvent.stopPropagation() // Extract the incident details from the feature properties const incidentDetails = { id: incident.properties.id, district: incident.properties.district, category: incident.properties.category, type: incident.properties.incidentType, description: incident.properties.description, status: incident.properties?.status || "Unknown", longitude: (incident.geometry as any).coordinates[0], latitude: (incident.geometry as any).coordinates[1], timestamp: new Date(), } // Dispatch the custom event to the map container element directly const customEvent = new CustomEvent('incident_click', { detail: incidentDetails, bubbles: true // Make sure the event bubbles up }) if (map.getMap().getCanvas()) { map.getMap().getCanvas().dispatchEvent(customEvent) } else { document.dispatchEvent(customEvent) // Fallback } }, [map]); useEffect(() => { if (!map || !visible) return; // Add click handler for individual incident points if (map.getMap().getLayer("unclustered-point")) { map.on("click", "unclustered-point", handleIncidentClick) } return () => { if (map && map.getMap().getLayer("unclustered-point")) { map.off("click", "unclustered-point", handleIncidentClick) } } }, [map, visible, handleIncidentClick]); if (!visible) return null return ( <> {/* {hoverInfo && (

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

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

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

)}
)} */} {selectedDistrictRef.current ? ( { selectedDistrictRef.current = null; setSelectedDistrict(null); }} district={selectedDistrictRef.current} year={year} month={month} filterCategory={filterCategory} /> ) : null} ) }