"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) => { // Use district_id (which corresponds to kode_kec in the tileset) as the key const districtId = crime.distrcit_id || crime.district_name console.log("Mapping district:", districtId, "level:", crime.level) 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] || {} console.log("Hover district:", districtId, "found data:", crimeData) // 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) return // Handler for style load event const onStyleLoad = () => { // Skip if map is not available if (!map) return try { // Check if the source already exists to prevent duplicates if (!map.getMap().getSource("districts")) { // Get the first symbol layer ID from the map style // This ensures our layers appear below labels and POIs const layers = map.getStyle().layers let firstSymbolId: string | undefined for (const layer of layers) { if (layer.type === "symbol") { firstSymbolId = layer.id break } } // Add the vector tile source map.getMap().addSource("districts", { type: "vector", url: `mapbox://${tilesetId}`, }) // Create the dynamic fill color expression based on crime data const fillColorExpression: any = [ "case", ["has", "kode_kec"], [ "match", ["get", "kode_kec"], ...Object.entries(crimeDataByDistrict).flatMap(([districtId, data]) => { console.log("Initial color setting for:", districtId, "level:", data.level) 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, ] // Only add layers if they don't already exist if (!map.getMap().getLayer("district-fill")) { // Add the fill layer for districts with dynamic colors from the start // Insert below the first symbol layer to preserve Mapbox default layers map.getMap().addLayer( { id: "district-fill", type: "fill", source: "districts", "source-layer": "Districts", paint: { "fill-color": fillColorExpression, // Apply colors based on crime data "fill-opacity": 0.6, }, }, firstSymbolId, ) // Add before the first symbol layer } if (!map.getMap().getLayer("district-line")) { // 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, }, }, firstSymbolId, ) } // if (!map.getMap().getLayer("district-labels")) { // // Add district labels with improved visibility and responsive sizing // map.getMap().addLayer( // { // id: "district-labels", // type: "symbol", // source: "districts", // "source-layer": "Districts", // layout: { // "text-field": ["get", "nama"], // "text-font": ["Open Sans Bold", "Arial Unicode MS Bold"], // // Make text size responsive to zoom level // "text-size": [ // "interpolate", // ["linear"], // ["zoom"], // 9, // 8, // At zoom level 9, size 8px // 12, // 12, // At zoom level 12, size 12px // 15, // 14, // At zoom level 15, size 14px // ], // "text-allow-overlap": false, // "text-ignore-placement": false, // // Adjust text anchor based on zoom level // "text-anchor": "center", // "text-justify": "center", // "text-max-width": 8, // // Show labels only at certain zoom levels // "text-optional": true, // "symbol-sort-key": ["get", "kode_kec"], // Sort labels by district code // "symbol-z-order": "source", // }, // paint: { // "text-color": "#000000", // "text-halo-color": "#ffffff", // "text-halo-width": 2, // "text-halo-blur": 1, // // Fade in text opacity based on zoom level // "text-opacity": [ // "interpolate", // ["linear"], // ["zoom"], // 8, // 0, // Fully transparent at zoom level 8 // 9, // 0.6, // 60% opacity at zoom level 9 // 10, // 1.0, // Fully opaque at zoom level 10 // ], // }, // }, // firstSymbolId, // ) // } // Create a source for clustered incident markers if (crimes.length > 0 && !map.getMap().getSource("crime-incidents")) { // Collect all incidents from all districts const allIncidents = crimes.flatMap((crime) => crime.incidents.map((incident) => ({ type: "Feature" as const, properties: { id: incident.id, district: crime.district_name, category: incident.category, incidentType: incident.type, level: crime.level, description: incident.description, }, geometry: { type: "Point" as const, coordinates: [incident.longitude, incident.latitude], }, })), ) // Add a clustered GeoJSON source for incidents map.getMap().addSource("crime-incidents", { type: "geojson", data: { type: "FeatureCollection", features: allIncidents, }, cluster: true, clusterMaxZoom: 14, clusterRadius: 50, }) // Only add layers if they don't already exist if (!map.getMap().getLayer("clusters")) { // Add a layer for the clusters - place below default symbol layers map.getMap().addLayer( { id: "clusters", type: "circle", source: "crime-incidents", filter: ["has", "point_count"], paint: { "circle-color": [ "step", ["get", "point_count"], "#51bbd6", // Blue for small clusters 5, "#f1f075", // Yellow for medium clusters 15, "#f28cb1", // Pink for large clusters ], "circle-radius": [ "step", ["get", "point_count"], 20, // Size for small clusters 5, 30, // Size for medium clusters 15, 40, // Size for large clusters ], "circle-opacity": 0.75, }, }, firstSymbolId, ) } if (!map.getMap().getLayer("cluster-count")) { // Add a layer for cluster counts 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")) { // Add a layer for individual incident points 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, ) } // Add click handler for clusters 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 // Get the cluster expansion zoom ; (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, }) }, ) }) // Show pointer cursor on clusters and points 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 = "" }) } // 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") } else { // If the source already exists, just update the data console.log("District source already exists, updating data") // Update the district-fill layer with new crime data if it exists 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 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) { // Only remove event listeners, not the layers themselves map.off("click", "district-fill", handleClick) map.off("mousemove", "district-fill", handleMouseMove) map.off("mouseleave", "district-fill", () => setHoverInfo(null)) // We're not removing the layers or sources here to avoid disrupting the map // This prevents the issue of removing default layers } } }, [map, visible, tilesetId, crimes]) // Update the crime data when it changes useEffect(() => { if (!map || !layersAdded.current) return console.log("Updating district colors with data:", crimeDataByDistrict) // Update the district-fill layer with new crime data try { // Check if the layer exists before updating it if (map.getMap().getLayer("district-fill")) { // We need to update the layer paint property to correctly apply colors map.getMap().setPaintProperty("district-fill", "fill-color", [ "case", ["has", "kode_kec"], [ "match", ["get", "kode_kec"], ...Object.entries(crimeDataByDistrict).flatMap(([districtId, data]) => { console.log("Setting color for:", districtId, "level:", data.level) 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 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} /> )} ) }