230 lines
9.1 KiB
TypeScript
230 lines
9.1 KiB
TypeScript
"use client"
|
|
|
|
import type { IUnclusteredPointLayerProps } from "@/app/_utils/types/map"
|
|
import { useEffect, useCallback, useRef } from "react"
|
|
import { manageLayerVisibility } from "@/app/_utils/map/layer-visibility"
|
|
|
|
export default function UnclusteredPointLayer({
|
|
visible = true,
|
|
map,
|
|
crimes = [],
|
|
filterCategory = "all",
|
|
focusedDistrictId,
|
|
}: IUnclusteredPointLayerProps) {
|
|
// Add a ref to track if we're currently interacting with a marker
|
|
const isInteractingWithMarker = useRef(false);
|
|
|
|
// Define layer IDs for consistent management
|
|
const LAYER_IDS = ['unclustered-point'];
|
|
|
|
const handleIncidentClick = useCallback(
|
|
(e: any) => {
|
|
if (!map) return
|
|
|
|
const features = map.queryRenderedFeatures(e.point, { layers: ["unclustered-point"] })
|
|
if (!features || features.length === 0) return
|
|
|
|
// Set flag to indicate we're interacting with a marker
|
|
isInteractingWithMarker.current = true;
|
|
|
|
const incident = features[0]
|
|
if (!incident.properties) return
|
|
|
|
e.originalEvent.stopPropagation()
|
|
e.preventDefault()
|
|
|
|
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(incident.properties.timestamp || Date.now()),
|
|
}
|
|
|
|
// console.log("Incident clicked:", incidentDetails)
|
|
|
|
// Ensure markers stay visible when clicking on them
|
|
if (map.getLayer("unclustered-point")) {
|
|
map.setLayoutProperty("unclustered-point", "visibility", "visible");
|
|
}
|
|
|
|
// First fly to the incident location
|
|
map.flyTo({
|
|
center: [incidentDetails.longitude, incidentDetails.latitude],
|
|
zoom: 15,
|
|
bearing: 0,
|
|
pitch: 45,
|
|
duration: 2000,
|
|
})
|
|
|
|
// Then dispatch the incident_click event to show the popup
|
|
const customEvent = new CustomEvent("incident_click", {
|
|
detail: incidentDetails,
|
|
bubbles: true,
|
|
})
|
|
|
|
// Dispatch on both the map canvas and document to ensure it's caught
|
|
map.getCanvas().dispatchEvent(customEvent)
|
|
document.dispatchEvent(customEvent)
|
|
|
|
// Reset the flag after a delay to allow the event to process
|
|
setTimeout(() => {
|
|
isInteractingWithMarker.current = false;
|
|
}, 500);
|
|
},
|
|
[map],
|
|
);
|
|
|
|
// Use centralized layer visibility management
|
|
useEffect(() => {
|
|
if (!map) return;
|
|
|
|
// Special case for this layer: also consider focusedDistrictId
|
|
const isActuallyVisible = visible && !(focusedDistrictId && !isInteractingWithMarker.current);
|
|
|
|
return manageLayerVisibility(map, LAYER_IDS, isActuallyVisible);
|
|
}, [map, visible, focusedDistrictId]);
|
|
|
|
useEffect(() => {
|
|
if (!map || !visible) return
|
|
|
|
// Konversi crimes ke GeoJSON FeatureCollection
|
|
const geojsonData = {
|
|
type: "FeatureCollection" as const,
|
|
features: crimes.flatMap((crime) =>
|
|
crime.crime_incidents
|
|
.filter(
|
|
(incident) =>
|
|
(filterCategory === "all" || incident.crime_categories.name === filterCategory) &&
|
|
incident.locations &&
|
|
typeof incident.locations.longitude === "number" &&
|
|
typeof incident.locations.latitude === "number",
|
|
)
|
|
.map((incident) => ({
|
|
type: "Feature" as const,
|
|
geometry: {
|
|
type: "Point" as const,
|
|
coordinates: [incident.locations.longitude, incident.locations.latitude],
|
|
},
|
|
properties: {
|
|
id: incident.id,
|
|
district: crime.districts.name,
|
|
category: incident.crime_categories.name,
|
|
incidentType: incident.crime_categories.type || "",
|
|
description: incident.description,
|
|
status: incident.status || "",
|
|
timestamp: incident.timestamp ? incident.timestamp.toString() : "",
|
|
},
|
|
})),
|
|
),
|
|
}
|
|
|
|
const setupLayerAndSource = () => {
|
|
try {
|
|
// First check if source exists and update it
|
|
if (map.getSource("crime-incidents")) {
|
|
; (map.getSource("crime-incidents") as any).setData(geojsonData)
|
|
} else {
|
|
// If not, add source
|
|
map.addSource("crime-incidents", {
|
|
type: "geojson",
|
|
data: geojsonData,
|
|
})
|
|
}
|
|
|
|
// Get layers to find first symbol layer
|
|
const layers = map.getStyle().layers
|
|
let firstSymbolId: string | undefined
|
|
for (const layer of layers) {
|
|
if (layer.type === "symbol") {
|
|
firstSymbolId = layer.id
|
|
break
|
|
}
|
|
}
|
|
|
|
// Check if layer exists
|
|
if (!map.getLayer("unclustered-point")) {
|
|
map.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",
|
|
},
|
|
layout: {
|
|
// Only hide markers if a district is focused AND we're not interacting with a marker
|
|
visibility: focusedDistrictId && !isInteractingWithMarker.current ? "none" : "visible",
|
|
},
|
|
},
|
|
firstSymbolId,
|
|
)
|
|
|
|
map.on("mouseenter", "unclustered-point", () => {
|
|
map.getCanvas().style.cursor = "pointer"
|
|
})
|
|
|
|
map.on("mouseleave", "unclustered-point", () => {
|
|
map.getCanvas().style.cursor = ""
|
|
})
|
|
} else {
|
|
// Update visibility based on focused district, but keep visible when interacting with markers
|
|
const newVisibility = focusedDistrictId && !isInteractingWithMarker.current ? "none" : "visible";
|
|
map.setLayoutProperty("unclustered-point", "visibility", newVisibility);
|
|
}
|
|
|
|
// Always ensure click handler is properly registered
|
|
map.off("click", "unclustered-point", handleIncidentClick)
|
|
map.on("click", "unclustered-point", handleIncidentClick)
|
|
} catch (error) {
|
|
console.error("Error setting up unclustered point layer:", error)
|
|
}
|
|
}
|
|
|
|
if (map.getLayer("crime-incidents")) {
|
|
const newVisibility = focusedDistrictId && !isInteractingWithMarker.current ? "none" : "visible";
|
|
map.setLayoutProperty("crime-incidents", "visibility", newVisibility);
|
|
}
|
|
|
|
if (map.getLayer("unclustered-point")) {
|
|
const newVisibility = focusedDistrictId && !isInteractingWithMarker.current ? "none" : "visible";
|
|
map.setLayoutProperty("unclustered-point", "visibility", newVisibility);
|
|
}
|
|
|
|
// Check if style is loaded and set up layer accordingly
|
|
if (map.isStyleLoaded()) {
|
|
setupLayerAndSource()
|
|
} else {
|
|
// Add event listener for style loading completion
|
|
const onStyleLoad = () => {
|
|
setupLayerAndSource()
|
|
}
|
|
|
|
map.once("style.load", onStyleLoad)
|
|
|
|
// Also wait a bit and try again as a fallback
|
|
setTimeout(() => {
|
|
if (map.isStyleLoaded()) {
|
|
setupLayerAndSource()
|
|
}
|
|
}, 500)
|
|
}
|
|
|
|
return () => {
|
|
if (map) {
|
|
map.off("click", "unclustered-point", handleIncidentClick)
|
|
}
|
|
}
|
|
}, [map, visible, focusedDistrictId, handleIncidentClick, crimes, filterCategory])
|
|
|
|
return null
|
|
}
|