MIF_E31221222/sigap-website/app/_components/map/layers/cluster-layer.tsx

284 lines
9.6 KiB
TypeScript

"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
}