"use client" import { useEffect, useCallback } from "react" import type mapboxgl from "mapbox-gl" import type { GeoJSON } from "geojson" import { IClusterLayerProps } from "@/app/_utils/types/map" import { extractCrimeIncidents } from "@/app/_utils/map" export default function ClusterLayer({ visible = true, map, crimes = [], filterCategory = "all", focusedDistrictId, }: IClusterLayerProps) { const handleClusterClick = useCallback( (e: any) => { if (!map) return e.originalEvent.stopPropagation() e.preventDefault() const features = map.queryRenderedFeatures(e.point, { layers: ["clusters"] }) if (!features || features.length === 0) return const clusterId: number = features[0].properties?.cluster_id as number try { // Get the expanded zoom level for this cluster (map.getSource("crime-incidents") as mapboxgl.GeoJSONSource).getClusterExpansionZoom(clusterId, (err, zoom) => { if (err) { console.error("Error getting cluster expansion zoom:", err) return } const coordinates = (features[0].geometry as any).coordinates // Dispatch a custom event for the fly-to behavior const clusterClickEvent = new CustomEvent('cluster_click', { detail: { center: coordinates, zoom: zoom ?? undefined, }, bubbles: true }) if (map.getCanvas()) { map.getCanvas().dispatchEvent(clusterClickEvent) } else { document.dispatchEvent(clusterClickEvent) } // Also perform the direct flyTo operation for immediate feedback map.flyTo({ center: coordinates, zoom: zoom ?? 12, duration: 1000, easing: (t) => t * (2 - t) // easeOutQuad }) }) } catch (error) { console.error("Error handling cluster click:", error) } }, [map], ) useEffect(() => { if (!map || !visible) return const onStyleLoad = () => { if (!map) return try { const layers = map.getStyle().layers let firstSymbolId: string | undefined for (const layer of layers) { if (layer.type === "symbol") { firstSymbolId = layer.id break } } if (!map.getSource("crime-incidents")) { const allIncidents = extractCrimeIncidents(crimes, filterCategory) map.addSource("crime-incidents", { type: "geojson", data: { type: "FeatureCollection", features: allIncidents as GeoJSON.Feature[], }, cluster: true, clusterMaxZoom: 14, clusterRadius: 50, }) if (!map.getLayer("clusters")) { map.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, }, layout: { visibility: focusedDistrictId ? "none" : "visible", }, }, firstSymbolId, ) } if (!map.getLayer("cluster-count")) { map.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, visibility: focusedDistrictId ? "none" : "visible", }, paint: { "text-color": "#ffffff", }, }) } map.on("mouseenter", "clusters", () => { map.getCanvas().style.cursor = "pointer" }) map.on("mouseleave", "clusters", () => { map.getCanvas().style.cursor = "" }) // Remove and re-add click handler to avoid duplicates map.off("click", "clusters", handleClusterClick) map.on("click", "clusters", handleClusterClick) } else { // Update visibility based on focused district if (map.getLayer("clusters")) { map.setLayoutProperty("clusters", "visibility", focusedDistrictId ? "none" : "visible") } if (map.getLayer("cluster-count")) { map.setLayoutProperty("cluster-count", "visibility", focusedDistrictId ? "none" : "visible") } // Update the cluster click handler map.off("click", "clusters", handleClusterClick) map.on("click", "clusters", handleClusterClick) } } catch (error) { console.error("Error adding cluster layer:", error) } } if (map.isStyleLoaded()) { onStyleLoad() } else { map.once("style.load", onStyleLoad) } return () => { if (map) { map.off("click", "clusters", handleClusterClick) } } }, [map, visible, crimes, filterCategory, focusedDistrictId, handleClusterClick]) // Update crime incidents data when filters change useEffect(() => { if (!map || !map.getSource("crime-incidents")) return try { const allIncidents = extractCrimeIncidents(crimes, filterCategory) ; (map.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]) return null }