"use client" import type { IUnclusteredPointLayerProps } from "@/app/_utils/types/map" import { useEffect, useCallback, useRef } from "react" 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); 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], ) 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 }