"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" interface ExtendedClusterLayerProps extends IClusterLayerProps { clusteringEnabled?: boolean; showClusters?: boolean; } export default function ClusterLayer({ visible = true, map, crimes = [], filterCategory = "all", focusedDistrictId, clusteringEnabled = false, showClusters = false, }: ExtendedClusterLayerProps) { 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: clusteringEnabled, 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: showClusters && !focusedDistrictId ? "visible" : "none", }, }, 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: showClusters && !focusedDistrictId ? "visible" : "none", }, 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 source clustering option try { // We need to recreate the source if we're changing the clustering option const currentSource = map.getSource("crime-incidents") as mapboxgl.GeoJSONSource; const data = (currentSource as any)._data; // Get current data // If clustering state has changed, recreate the source const existingClusterState = (currentSource as any).options?.cluster; if (existingClusterState !== clusteringEnabled) { // Remove existing layers that use this source if (map.getLayer("clusters")) map.removeLayer("clusters"); if (map.getLayer("cluster-count")) map.removeLayer("cluster-count"); if (map.getLayer("unclustered-point")) map.removeLayer("unclustered-point"); // Remove and recreate source with new clustering setting map.removeSource("crime-incidents"); map.addSource("crime-incidents", { type: "geojson", data: data, cluster: clusteringEnabled, clusterMaxZoom: 14, clusterRadius: 50, }); // Re-add the layers 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: showClusters && !focusedDistrictId ? "visible" : "none", }, }, 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: showClusters && !focusedDistrictId ? "visible" : "none", }, paint: { "text-color": "#ffffff", }, }); } } } catch (error) { console.error("Error updating cluster source:", error); } // Update visibility based on focused district and showClusters flag if (map.getLayer("clusters")) { map.setLayoutProperty("clusters", "visibility", showClusters && !focusedDistrictId ? "visible" : "none") } if (map.getLayer("cluster-count")) { map.setLayoutProperty("cluster-count", "visibility", showClusters && !focusedDistrictId ? "visible" : "none") } // 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, clusteringEnabled, showClusters]) // 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]) // Update visibility when showClusters changes useEffect(() => { if (!map || !map.getLayer("clusters") || !map.getLayer("cluster-count")) return; try { map.setLayoutProperty("clusters", "visibility", showClusters && !focusedDistrictId ? "visible" : "none"); map.setLayoutProperty("cluster-count", "visibility", showClusters && !focusedDistrictId ? "visible" : "none"); } catch (error) { console.error("Error updating cluster visibility:", error); } }, [map, showClusters, focusedDistrictId]); return null }