diff --git a/sigap-website/app/_components/map/fly-to.tsx b/sigap-website/app/_components/map/fly-to.tsx index 711c939..ab6f29a 100644 --- a/sigap-website/app/_components/map/fly-to.tsx +++ b/sigap-website/app/_components/map/fly-to.tsx @@ -64,8 +64,11 @@ export default function FlyToHandler({ map }: Pick) { }, }) - // Add a pulsing effect using animations + // Add a slower pulsing effect let size = 10 + let frameCount = 0 + const animationSpeed = 3; // Higher value = slower animation (skip frames) + const animatePulse = () => { if (!map || !map.getLayer("target-incident-highlight")) { if (animationRef.current) { @@ -75,7 +78,11 @@ export default function FlyToHandler({ map }: Pick) { return } - size = (size % 20) + 1 + frameCount++; + // Only update size every few frames to slow down the animation + if (frameCount % animationSpeed === 0) { + size = (size % 20) + 0.5; // Smaller increment for smoother, slower animation + } map.setPaintProperty("target-incident-highlight", "circle-radius", [ "interpolate", diff --git a/sigap-website/app/_components/map/layers/uncluster-layer.tsx b/sigap-website/app/_components/map/layers/uncluster-layer.tsx index 929b3e3..5cd1fed 100644 --- a/sigap-website/app/_components/map/layers/uncluster-layer.tsx +++ b/sigap-website/app/_components/map/layers/uncluster-layer.tsx @@ -3,7 +3,6 @@ import { IUnclusteredPointLayerProps } from "@/app/_utils/types/map" import { useEffect, useCallback } from "react" - export default function UnclusteredPointLayer({ visible = true, map, @@ -11,6 +10,37 @@ export default function UnclusteredPointLayer({ filterCategory = "all", focusedDistrictId, }: IUnclusteredPointLayerProps) { + // 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 handleIncidentClick = useCallback( (e: any) => { if (!map) return @@ -75,20 +105,31 @@ export default function UnclusteredPointLayer({ useEffect(() => { if (!map || !visible) return - const onStyleLoad = () => { - if (!map) return - + const setupLayerAndSource = () => { try { - const layers = map.getStyle().layers - let firstSymbolId: string | undefined + // 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 + firstSymbolId = layer.id; + break; } } - if (map.getSource("crime-incidents") && !map.getLayer("unclustered-point")) { + // Check if layer exists + if (!map.getLayer("unclustered-point")) { map.addLayer( { id: "unclustered-point", @@ -106,43 +147,53 @@ export default function UnclusteredPointLayer({ }, }, firstSymbolId, - ) + ); map.on("mouseenter", "unclustered-point", () => { - map.getCanvas().style.cursor = "pointer" - }) + map.getCanvas().style.cursor = "pointer"; + }); map.on("mouseleave", "unclustered-point", () => { - map.getCanvas().style.cursor = "" - }) - - map.off("click", "unclustered-point", handleIncidentClick) - map.on("click", "unclustered-point", handleIncidentClick) - } else if (map.getLayer("unclustered-point")) { + map.getCanvas().style.cursor = ""; + }); + } else { // Update visibility based on focused district - map.setLayoutProperty("unclustered-point", "visibility", focusedDistrictId ? "none" : "visible") - - // Ensure click handler is registered - map.off("click", "unclustered-point", handleIncidentClick) - map.on("click", "unclustered-point", handleIncidentClick) + map.setLayoutProperty("unclustered-point", "visibility", focusedDistrictId ? "none" : "visible"); } - } catch (error) { - console.error("Error adding unclustered point layer:", error) - } - } + // 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); + } + }; + + // Check if style is loaded and set up layer accordingly if (map.isStyleLoaded()) { - onStyleLoad() + setupLayerAndSource(); } else { - map.once("style.load", onStyleLoad) + // 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.off("click", "unclustered-point", handleIncidentClick); } - } - }, [map, visible, focusedDistrictId, handleIncidentClick]) + }; + }, [map, visible, focusedDistrictId, handleIncidentClick, crimes, filterCategory, geojsonData]); - return null + return null; } diff --git a/sigap-website/app/_utils/types/map.ts b/sigap-website/app/_utils/types/map.ts index a76a2a4..0f42b82 100644 --- a/sigap-website/app/_utils/types/map.ts +++ b/sigap-website/app/_utils/types/map.ts @@ -104,4 +104,5 @@ export interface IUnclusteredPointLayerProps extends IBaseLayerProps { crimes: ICrimes[]; filterCategory: string | 'all'; focusedDistrictId: string | null; + showIncidentMarkers?: boolean; // Add this prop to control marker visibility }