diff --git a/sigap-website/app/_components/map/controls/left/sidebar/map-sidebar.tsx b/sigap-website/app/_components/map/controls/left/sidebar/map-sidebar.tsx index d1981cb..159b7e8 100644 --- a/sigap-website/app/_components/map/controls/left/sidebar/map-sidebar.tsx +++ b/sigap-website/app/_components/map/controls/left/sidebar/map-sidebar.tsx @@ -104,26 +104,26 @@ export default function CrimeSidebar({ if (!map || !incident.longitude || !incident.latitude) return // Fly to the incident location - map.flyTo({ - center: [incident.longitude, incident.latitude], - zoom: 15, - pitch: 0, - bearing: 0, - duration: 1500, - easing: (t) => t * (2 - t), // easeOutQuad - }) + // map.flyTo({ + // center: [incident.longitude, incident.latitude], + // zoom: 15, + // pitch: 0, + // bearing: 0, + // duration: 1500, + // easing: (t) => t * (2 - t), // easeOutQuad + // }) - // Create and dispatch a custom event for the incident click - const customEvent = new CustomEvent("incident_click", { - detail: incident, - bubbles: true - }) + // // Create and dispatch a custom event for the incident click + // const customEvent = new CustomEvent("incident_click", { + // detail: incident, + // bubbles: true + // }) - if (map.getMap().getCanvas()) { - map.getMap().getCanvas().dispatchEvent(customEvent) - } else { - document.dispatchEvent(customEvent) - } + // if (map.getMap().getCanvas()) { + // map.getMap().getCanvas().dispatchEvent(customEvent) + // } else { + // document.dispatchEvent(customEvent) + // } } return ( diff --git a/sigap-website/app/_components/map/controls/top/search-control.tsx b/sigap-website/app/_components/map/controls/top/search-control.tsx index 8b45d31..59e9072 100644 --- a/sigap-website/app/_components/map/controls/top/search-control.tsx +++ b/sigap-website/app/_components/map/controls/top/search-control.tsx @@ -256,7 +256,7 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes = const handleFlyToIncident = () => { if (!selectedSuggestion || !selectedSuggestion.locations.latitude || !selectedSuggestion.locations.longitude) return; - // First, trigger a separate mapbox_fly_to event to handle the camera movement + // Dispatch mapbox_fly_to event to the main map canvas only const flyToMapEvent = new CustomEvent('mapbox_fly_to', { detail: { longitude: selectedSuggestion.locations.longitude, @@ -269,7 +269,11 @@ export default function SearchTooltip({ onControlChange, activeControl, crimes = bubbles: true }); - document.dispatchEvent(flyToMapEvent); + // Find the main map canvas and dispatch event there + const mapCanvas = document.querySelector('.mapboxgl-canvas'); + if (mapCanvas) { + mapCanvas.dispatchEvent(flyToMapEvent); + } // Wait for the fly animation to complete before showing the popup setTimeout(() => { diff --git a/sigap-website/app/_components/map/crime-map.tsx b/sigap-website/app/_components/map/crime-map.tsx index 650f1ad..fe8ca0a 100644 --- a/sigap-website/app/_components/map/crime-map.tsx +++ b/sigap-website/app/_components/map/crime-map.tsx @@ -16,34 +16,18 @@ import { useGetAvailableYears, useGetCrimeCategories, useGetCrimes } from "@/app import MapSelectors from "./controls/map-selector" import { cn } from "@/app/_lib/utils" -import CrimePopup from "./pop-up/crime-popup" import { $Enums, crime_categories, crime_incidents, crimes, demographics, districts, geographics, locations } from "@prisma/client" import { CrimeTimelapse } from "./controls/bottom/crime-timelapse" import { ITooltips } from "./controls/top/tooltips" import CrimeSidebar from "./controls/left/sidebar/map-sidebar" import Tooltips from "./controls/top/tooltips" -import { BASE_BEARING, BASE_PITCH, BASE_ZOOM } from "@/app/_utils/const/map" import Layers from "./layers/layers" import DistrictLayer, { DistrictFeature } from "./layers/district-layer-old" import { useGetUnitsQuery } from "@/app/(pages)/(admin)/dashboard/crime-management/units/_queries/queries" -interface ICrimeIncident { - id: string - district?: string - category?: string - type_category?: string | null - description?: string - status: string - address?: string | null - timestamp?: Date - latitude?: number - longitude?: number -} - export default function CrimeMap() { const [sidebarCollapsed, setSidebarCollapsed] = useState(true) const [selectedDistrict, setSelectedDistrict] = useState(null) - const [selectedIncident, setSelectedIncident] = useState(null) const [showLegend, setShowLegend] = useState(true) const [selectedCategory, setSelectedCategory] = useState("all") const [selectedYear, setSelectedYear] = useState(2024) @@ -131,152 +115,6 @@ export default function CrimeMap() { }) }, [filteredByYearAndMonth, selectedCategory]) - useEffect(() => { - const handleIncidentClickEvent = (e: CustomEvent) => { - console.log("Received incident_click event:", e.detail); - if (!e.detail || !e.detail.id) { - console.error("Invalid incident data in event:", e.detail); - return; - } - - let foundIncident: ICrimeIncident | undefined; - - filteredCrimes.forEach(crime => { - crime.crime_incidents.forEach(incident => { - if (incident.id === e.detail.id) { - foundIncident = { - id: incident.id, - district: crime.districts.name, - description: incident.description, - status: incident.status || "unknown", - timestamp: incident.timestamp, - category: incident.crime_categories.name, - type_category: incident.crime_categories.type, - address: incident.locations.address, - latitude: incident.locations.latitude, - longitude: incident.locations.longitude, - }; - } - }); - }); - - if (!foundIncident) { - console.error("Could not find incident with ID:", e.detail.id); - return; - } - - if (!foundIncident.latitude || !foundIncident.longitude) { - console.error("Invalid incident coordinates:", foundIncident); - return; - } - - setSelectedDistrict(null); - - setSelectedIncident(foundIncident); - }; - - const mapContainer = mapContainerRef.current; - - document.removeEventListener('incident_click', handleIncidentClickEvent as EventListener); - if (mapContainer) { - mapContainer.removeEventListener('incident_click', handleIncidentClickEvent as EventListener); - } - - document.addEventListener('incident_click', handleIncidentClickEvent as EventListener); - - if (mapContainer) { - mapContainer.addEventListener('incident_click', handleIncidentClickEvent as EventListener); - } - - return () => { - document.removeEventListener('incident_click', handleIncidentClickEvent as EventListener); - if (mapContainer) { - mapContainer.removeEventListener('incident_click', handleIncidentClickEvent as EventListener); - } - }; - }, [filteredCrimes]); - - useEffect(() => { - const handleMapFlyTo = (e: CustomEvent) => { - if (!e.detail) { - console.error("Invalid fly-to data:", e.detail); - return; - } - - const { longitude, latitude, zoom, bearing, pitch, duration } = e.detail; - - const mapInstance = mapContainerRef.current?.querySelector('.mapboxgl-map'); - if (!mapInstance) { - console.error("Map instance not found"); - return; - } - - const mapboxEvent = new CustomEvent('mapbox_fly', { - detail: { - center: [longitude, latitude], - zoom: zoom || 15, - bearing: bearing || 0, - pitch: pitch || 45, - duration: duration || 2000 - }, - bubbles: true - }); - - mapInstance.dispatchEvent(mapboxEvent); - }; - - document.addEventListener('mapbox_fly_to', handleMapFlyTo as EventListener); - - return () => { - document.removeEventListener('mapbox_fly_to', handleMapFlyTo as EventListener); - }; - }, []); - - useEffect(() => { - const handleMapReset = (e: CustomEvent) => { - const { duration } = e.detail || { duration: 1500 }; - - const mapInstance = mapContainerRef.current?.querySelector('.mapboxgl-map'); - if (!mapInstance) { - console.error("Map instance not found"); - return; - } - - const mapboxEvent = new CustomEvent('mapbox_fly', { - detail: { - duration: duration, - resetCamera: true - }, - bubbles: true - }); - - mapInstance.dispatchEvent(mapboxEvent); - }; - - document.addEventListener('mapbox_reset', handleMapReset as EventListener); - - return () => { - document.removeEventListener('mapbox_reset', handleMapReset as EventListener); - }; - }, []); - - const handlePopupClose = () => { - setSelectedIncident(null); - - const mapInstance = mapContainerRef.current?.querySelector('.mapboxgl-map'); - - if (mapInstance) { - const resetEvent = new CustomEvent('mapbox_reset', { - detail: { - duration: 1500, - }, - bubbles: true - }); - - mapInstance.dispatchEvent(resetEvent); - } - } - const handleTimelineChange = useCallback((year: number, month: number, progress: number) => { setSelectedYear(year) setSelectedMonth(month) @@ -287,7 +125,6 @@ export default function CrimeMap() { setisTimelapsePlaying(playing) if (playing) { - setSelectedIncident(null) setSelectedDistrict(null) } }, []) @@ -382,17 +219,6 @@ export default function CrimeMap() { useAllData={useAllYears} /> - {selectedIncident && selectedIncident.latitude && selectedIncident.longitude && ( - <> - - - )} - {isFullscreen && ( <>
diff --git a/sigap-website/app/_components/map/fly-to-handler.tsx b/sigap-website/app/_components/map/fly-to-handler.tsx new file mode 100644 index 0000000..414abb9 --- /dev/null +++ b/sigap-website/app/_components/map/fly-to-handler.tsx @@ -0,0 +1,117 @@ +"use client" + +import { useEffect, useRef } from "react" + +interface FlyToHandlerProps { + map: mapboxgl.Map +} + +export default function FlyToHandler({ map }: FlyToHandlerProps) { + const animationRef = useRef(null) + + useEffect(() => { + if (!map) return + + const handleFlyToEvent = (e: CustomEvent) => { + if (!map || !e.detail) return + + const { longitude, latitude, zoom, bearing, pitch, duration } = e.detail + + map.flyTo({ + center: [longitude, latitude], + zoom: zoom || 15, + bearing: bearing || 0, + pitch: pitch || 45, + duration: duration || 2000, + }) + + // Cancel any existing animation + if (animationRef.current) { + cancelAnimationFrame(animationRef.current) + } + + // Add a highlight or pulse effect to the target incident + try { + if (map.getLayer("target-incident-highlight")) { + map.removeLayer("target-incident-highlight") + } + if (map.getSource("target-incident-highlight")) { + map.removeSource("target-incident-highlight") + } + map.addSource("target-incident-highlight", { + type: "geojson", + data: { + type: "Feature", + geometry: { + type: "Point", + coordinates: [longitude, latitude], + }, + properties: {}, + }, + }) + map.addLayer({ + id: "target-incident-highlight", + source: "target-incident-highlight", + type: "circle", + paint: { + "circle-radius": ["interpolate", ["linear"], ["zoom"], 10, 10, 15, 15, 20, 20], + "circle-color": "#ff0000", + "circle-opacity": 0.7, + "circle-stroke-width": 2, + "circle-stroke-color": "#ffffff", + }, + }) + + // Add a slower pulsing effect + let size = 10 + let frameCount = 0 + const animationSpeed = 3 + + const animatePulse = () => { + if (!map || !map.getLayer("target-incident-highlight")) { + if (animationRef.current) { + cancelAnimationFrame(animationRef.current) + animationRef.current = null + } + return + } + frameCount++ + if (frameCount % animationSpeed === 0) { + size = (size % 20) + 0.5 + } + map.setPaintProperty("target-incident-highlight", "circle-radius", [ + "interpolate", + ["linear"], + ["zoom"], + 10, + size, + 15, + size * 1.5, + 20, + size * 2, + ]) + animationRef.current = requestAnimationFrame(animatePulse) + } + animationRef.current = requestAnimationFrame(animatePulse) + } catch (error) { + // ignore highlight error + } + } + + map.getCanvas().addEventListener("mapbox_fly_to", handleFlyToEvent as EventListener) + document.addEventListener("mapbox_fly_to", handleFlyToEvent as EventListener) + + return () => { + if (map && map.getCanvas()) { + map.getCanvas().removeEventListener("mapbox_fly_to", handleFlyToEvent as EventListener) + } + document.removeEventListener("mapbox_fly_to", handleFlyToEvent as EventListener) + if (animationRef.current) { + cancelAnimationFrame(animationRef.current) + animationRef.current = null + } + } + }, [map]) + + return null +} diff --git a/sigap-website/app/_components/map/fly-to.tsx b/sigap-website/app/_components/map/fly-to.tsx index ab6f29a..77e2296 100644 --- a/sigap-website/app/_components/map/fly-to.tsx +++ b/sigap-website/app/_components/map/fly-to.tsx @@ -1,128 +1 @@ -"use client" - -import { IBaseLayerProps } from "@/app/_utils/types/map" -import { useEffect, useRef } from "react" - - -export default function FlyToHandler({ map }: Pick) { - // Track active animations - const animationRef = useRef(null) - - useEffect(() => { - if (!map) return - - const handleFlyToEvent = (e: CustomEvent) => { - if (!map || !e.detail) return - - const { longitude, latitude, zoom, bearing, pitch, duration } = e.detail - - map.flyTo({ - center: [longitude, latitude], - zoom: zoom || 15, - bearing: bearing || 0, - pitch: pitch || 45, - duration: duration || 2000, - }) - - // Cancel any existing animation - if (animationRef.current) { - cancelAnimationFrame(animationRef.current) - } - - // Add a highlight or pulse effect to the target incident - try { - if (map.getLayer("target-incident-highlight")) { - map.removeLayer("target-incident-highlight") - } - - if (map.getSource("target-incident-highlight")) { - map.removeSource("target-incident-highlight") - } - - map.addSource("target-incident-highlight", { - type: "geojson", - data: { - type: "Feature", - geometry: { - type: "Point", - coordinates: [longitude, latitude], - }, - properties: {}, - }, - }) - - map.addLayer({ - id: "target-incident-highlight", - source: "target-incident-highlight", - type: "circle", - paint: { - "circle-radius": ["interpolate", ["linear"], ["zoom"], 10, 10, 15, 15, 20, 20], - "circle-color": "#ff0000", - "circle-opacity": 0.7, - "circle-stroke-width": 2, - "circle-stroke-color": "#ffffff", - }, - }) - - // 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) { - cancelAnimationFrame(animationRef.current) - animationRef.current = null - } - return - } - - 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", - ["linear"], - ["zoom"], - 10, - size, - 15, - size * 1.5, - 20, - size * 2, - ]) - - animationRef.current = requestAnimationFrame(animatePulse) - } - - animationRef.current = requestAnimationFrame(animatePulse) - } catch (error) { - console.error("Error adding highlight effect:", error) - } - } - - // Listen for the custom fly-to event - map.getCanvas().addEventListener("mapbox_fly_to", handleFlyToEvent as EventListener) - // Also listen on the document to ensure we catch the event - document.addEventListener("mapbox_fly_to", handleFlyToEvent as EventListener) - - return () => { - if (map && map.getCanvas()) { - map.getCanvas().removeEventListener("mapbox_fly_to", handleFlyToEvent as EventListener) - } - document.removeEventListener("mapbox_fly_to", handleFlyToEvent as EventListener) - - // Clean up animation on unmount - if (animationRef.current) { - cancelAnimationFrame(animationRef.current) - animationRef.current = null - } - } - }, [map]) - - return null -} +// (File ini tidak perlu diaktifkan, semua fly-to sudah dihandle secara lokal pada masing-masing komponen.) diff --git a/sigap-website/app/_components/map/layers/cluster-layer.tsx b/sigap-website/app/_components/map/layers/cluster-layer.tsx index daebc58..29cd39f 100644 --- a/sigap-website/app/_components/map/layers/cluster-layer.tsx +++ b/sigap-website/app/_components/map/layers/cluster-layer.tsx @@ -4,12 +4,12 @@ import { useEffect, useCallback } from "react" import type mapboxgl from "mapbox-gl" import type { GeoJSON } from "geojson" -import { IClusterLayerProps } from "@/app/_utils/types/map" +import type { IClusterLayerProps } from "@/app/_utils/types/map" import { extractCrimeIncidents } from "@/app/_utils/map" interface ExtendedClusterLayerProps extends IClusterLayerProps { - clusteringEnabled?: boolean; - showClusters?: boolean; + clusteringEnabled?: boolean + showClusters?: boolean } export default function ClusterLayer({ @@ -36,37 +36,26 @@ export default function ClusterLayer({ 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 - } + ; (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 + const coordinates = (features[0].geometry as any).coordinates - // Dispatch a custom event for the fly-to behavior - const clusterClickEvent = new CustomEvent('cluster_click', { - detail: { + // Explicitly fly to the cluster location + map.flyTo({ 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 - }) - }) + zoom: zoom ?? 12, + bearing: 0, + pitch: 45, + duration: 1000, + }) + }, + ) } catch (error) { console.error("Error handling cluster click:", error) } @@ -157,19 +146,19 @@ export default function ClusterLayer({ // 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 + 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; + 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"); + 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.removeSource("crime-incidents") map.addSource("crime-incidents", { type: "geojson", @@ -177,7 +166,7 @@ export default function ClusterLayer({ cluster: clusteringEnabled, clusterMaxZoom: 14, clusterRadius: 50, - }); + }) // Re-add the layers if (!map.getLayer("clusters")) { @@ -197,7 +186,7 @@ export default function ClusterLayer({ }, }, firstSymbolId, - ); + ) } if (!map.getLayer("cluster-count")) { @@ -215,11 +204,11 @@ export default function ClusterLayer({ paint: { "text-color": "#ffffff", }, - }); + }) } } } catch (error) { - console.error("Error updating cluster source:", error); + console.error("Error updating cluster source:", error) } // Update visibility based on focused district and showClusters flag @@ -227,7 +216,11 @@ export default function ClusterLayer({ map.setLayoutProperty("clusters", "visibility", showClusters && !focusedDistrictId ? "visible" : "none") } if (map.getLayer("cluster-count")) { - map.setLayoutProperty("cluster-count", "visibility", showClusters && !focusedDistrictId ? "visible" : "none") + map.setLayoutProperty( + "cluster-count", + "visibility", + showClusters && !focusedDistrictId ? "visible" : "none", + ) } // Update the cluster click handler @@ -269,15 +262,15 @@ export default function ClusterLayer({ // Update visibility when showClusters changes useEffect(() => { - if (!map || !map.getLayer("clusters") || !map.getLayer("cluster-count")) return; + 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"); + 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); + console.error("Error updating cluster visibility:", error) } - }, [map, showClusters, focusedDistrictId]); + }, [map, showClusters, focusedDistrictId]) return null } diff --git a/sigap-website/app/_components/map/layers/district-layer-old.tsx b/sigap-website/app/_components/map/layers/district-layer-old.tsx index 573b327..45ac2bc 100644 --- a/sigap-website/app/_components/map/layers/district-layer-old.tsx +++ b/sigap-website/app/_components/map/layers/district-layer-old.tsx @@ -339,18 +339,38 @@ export default function DistrictLayer({ timestamp: new Date(), } - console.log("Incident clicked:", incidentDetails) + // console.log("Incident clicked:", incidentDetails) - const customEvent = new CustomEvent("incident_click", { - detail: incidentDetails, - bubbles: true, - }) + // Dispatch mapbox_fly_to event instead of direct flyTo + // const flyToEvent = new CustomEvent("mapbox_fly_to", { + // detail: { + // longitude: incidentDetails.longitude, + // latitude: incidentDetails.latitude, + // zoom: 15, + // bearing: 0, + // pitch: 45, + // duration: 2000, + // }, + // bubbles: true, + // }) + + // if (map.getMap().getCanvas()) { + // map.getMap().getCanvas().dispatchEvent(flyToEvent) + // } else { + // document.dispatchEvent(flyToEvent) + // } + + // Dispatch incident_click event after a short delay to allow fly animation + // const customEvent = new CustomEvent("incident_click", { + // detail: incidentDetails, + // bubbles: true, + // }) + // if (map.getMap().getCanvas()) { + // map.getMap().getCanvas().dispatchEvent(customEvent) + // } else { + // document.dispatchEvent(customEvent) + // } - if (map.getMap().getCanvas()) { - map.getMap().getCanvas().dispatchEvent(customEvent) - } else { - document.dispatchEvent(customEvent) - } }, [map], ) @@ -417,10 +437,11 @@ export default function DistrictLayer({ useEffect(() => { if (!map) return; - const handleFlyToEvent = (e: CustomEvent) => { - if (!map || !e.detail) return; + const handleFlyToEvent = (e: Event) => { + const customEvent = e as CustomEvent; + if (!map || !customEvent.detail) return; - const { longitude, latitude, zoom, bearing, pitch, duration } = e.detail; + const { longitude, latitude, zoom, bearing, pitch, duration } = customEvent.detail; map.flyTo({ center: [longitude, latitude], diff --git a/sigap-website/app/_components/map/layers/district-layer.tsx b/sigap-website/app/_components/map/layers/district-layer.tsx index 16a8820..b9adca5 100644 --- a/sigap-website/app/_components/map/layers/district-layer.tsx +++ b/sigap-website/app/_components/map/layers/district-layer.tsx @@ -10,6 +10,7 @@ export default function DistrictFillLineLayer({ visible = true, map, onClick, + onDistrictClick, // Add the new prop year, month, filterCategory = "all", @@ -20,7 +21,7 @@ export default function DistrictFillLineLayer({ crimeDataByDistrict, showFill = true, activeControl, -}: IDistrictLayerProps) { +}: IDistrictLayerProps & { onDistrictClick?: (district: any) => void }) { // Extend the type inline useEffect(() => { if (!map || !visible) return @@ -91,7 +92,10 @@ export default function DistrictFillLineLayer({ easing: (t) => t * (2 - t), // easeOutQuad }) - if (onClick) { + // Use onDistrictClick if available, otherwise fall back to onClick + if (onDistrictClick) { + onDistrictClick(district) + } else if (onClick) { onClick(district) } }, 100) @@ -130,7 +134,10 @@ export default function DistrictFillLineLayer({ easing: (t) => t * (2 - t), // easeOutQuad }) - if (onClick) { + // Use onDistrictClick if available, otherwise fall back to onClick + if (onDistrictClick) { + onDistrictClick(district) + } else if (onClick) { onClick(district) } } @@ -239,6 +246,7 @@ export default function DistrictFillLineLayer({ focusedDistrictId, crimeDataByDistrict, onClick, + onDistrictClick, // Add to dependency array setFocusedDistrictId, showFill, activeControl, diff --git a/sigap-website/app/_components/map/layers/layers.tsx b/sigap-website/app/_components/map/layers/layers.tsx index 186d573..3f65986 100644 --- a/sigap-website/app/_components/map/layers/layers.tsx +++ b/sigap-website/app/_components/map/layers/layers.tsx @@ -7,42 +7,63 @@ import DistrictPopup from "../pop-up/district-popup" import DistrictExtrusionLayer from "./district-extrusion-layer" import ClusterLayer from "./cluster-layer" import HeatmapLayer from "./heatmap-layer" -import DistrictLayer from "./district-layer-old" import TimelineLayer from "./timeline-layer" import type { ICrimes } from "@/app/_utils/types/crimes" -import { IDistrictFeature } from "@/app/_utils/types/map" +import type { IDistrictFeature } from "@/app/_utils/types/map" import { createFillColorExpression, processCrimeDataByDistrict } from "@/app/_utils/map" import UnclusteredPointLayer from "./uncluster-layer" -import FlyToHandler from "../fly-to" + import { toast } from "sonner" -import { ITooltips } from "../controls/top/tooltips" -import { IUnits } from "@/app/_utils/types/units" +import type { ITooltips } from "../controls/top/tooltips" +import type { IUnits } from "@/app/_utils/types/units" import UnitsLayer from "./units-layer" import DistrictFillLineLayer from "./district-layer" +import CrimePopup from "../pop-up/crime-popup" + +// Interface for crime incident +interface ICrimeIncident { + id: string + district?: string + category?: string + type_category?: string | null + description?: string + status: string + address?: string | null + timestamp?: Date + latitude?: number + longitude?: number +} // District layer props export interface IDistrictLayerProps { visible?: boolean onClick?: (feature: IDistrictFeature) => void + onDistrictClick?: (feature: IDistrictFeature) => void + map?: any year: string month: string filterCategory: string | "all" crimes: ICrimes[] units?: IUnits[] tilesetId?: string + focusedDistrictId?: string | null + setFocusedDistrictId?: (id: string | null) => void + crimeDataByDistrict?: Record + showFill?: boolean + activeControl?: ITooltips } interface LayersProps { - visible?: boolean; - crimes: ICrimes[]; - units?: IUnits[]; - year: string; - month: string; - filterCategory: string | "all"; - activeControl: ITooltips; - tilesetId?: string; - useAllData?: boolean; // New prop to indicate if we're showing all data + visible?: boolean + crimes: ICrimes[] + units?: IUnits[] + year: string + month: string + filterCategory: string | "all" + activeControl: ITooltips + tilesetId?: string + useAllData?: boolean } export default function Layers({ @@ -66,68 +87,228 @@ export default function Layers({ const mapboxMap = map.getMap() const [selectedDistrict, setSelectedDistrict] = useState(null) + const [selectedIncident, setSelectedIncident] = useState(null) const [focusedDistrictId, setFocusedDistrictId] = useState(null) const selectedDistrictRef = useRef(null) const crimeDataByDistrict = processCrimeDataByDistrict(crimes) - // Set up custom event handler for cluster clicks to ensure it works across components - useEffect(() => { - if (!mapboxMap) return; - - const handleClusterClickEvent = (e: CustomEvent) => { - if (!e.detail) return; - - const { center, zoom } = e.detail; - if (center && zoom) { - mapboxMap.flyTo({ - center: center, - zoom: zoom, - duration: 1000, - easing: (t) => t * (2 - t) - }); - } - }; - - mapboxMap.getCanvas().addEventListener('cluster_click', handleClusterClickEvent as EventListener); - - return () => { - mapboxMap.getCanvas().removeEventListener('cluster_click', handleClusterClickEvent as EventListener); - }; - }, [mapboxMap]); - - // Handle popup close - const handleCloseDistrictPopup = () => { - console.log("Closing district popup") + // Handle popup close with a common reset pattern + const handlePopupClose = useCallback(() => { + // Reset selected state selectedDistrictRef.current = null setSelectedDistrict(null) - setFocusedDistrictId(null) + setSelectedIncident(null) + setFocusedDistrictId(null) - // Reset pitch and bearing - if (map) { - map.easeTo({ - zoom: BASE_ZOOM, - pitch: BASE_PITCH, - bearing: BASE_BEARING, - duration: 1500, - easing: (t) => t * (2 - t), // easeOutQuad - }) + // Reset map view/camera + if (map) { + map.easeTo({ + zoom: BASE_ZOOM, + pitch: BASE_PITCH, + bearing: BASE_BEARING, + duration: 1500, + easing: (t) => t * (2 - t), // easeOutQuad + }) - // Show all clusters again when closing popup - if (map.getLayer("clusters")) { - map.getMap().setLayoutProperty("clusters", "visibility", "visible") - } - if (map.getLayer("unclustered-point")) { - map.getMap().setLayoutProperty("unclustered-point", "visibility", "visible") - } + // Show all clusters again when closing popup + if (map.getLayer("clusters")) { + map.getMap().setLayoutProperty("clusters", "visibility", "visible") + } + if (map.getLayer("unclustered-point")) { + map.getMap().setLayoutProperty("unclustered-point", "visibility", "visible") + } - // Explicitly update fill color for all districts - if (map.getLayer("district-fill")) { - const fillColorExpression = createFillColorExpression(null, crimeDataByDistrict) - map.getMap().setPaintProperty("district-fill", "fill-color", fillColorExpression as any) - } + // Update fill color for all districts + if (map.getLayer("district-fill")) { + const fillColorExpression = createFillColorExpression(null, crimeDataByDistrict) + map.getMap().setPaintProperty("district-fill", "fill-color", fillColorExpression as any) } } + }, [map, crimeDataByDistrict]) + + // Handle district popup close specifically + const handleCloseDistrictPopup = useCallback(() => { + console.log("Closing district popup") + handlePopupClose() + }, [handlePopupClose]) + + // Handle incident popup close specifically + const handleCloseIncidentPopup = useCallback(() => { + console.log("Closing incident popup") + handlePopupClose() + }, [handlePopupClose]) + + // Handle district clicks + const handleDistrictClick = useCallback( + (feature: IDistrictFeature) => { + console.log("District clicked:", feature) + + // Clear any incident selection when showing a district + setSelectedIncident(null) + + // Set the selected district + setSelectedDistrict(feature) + selectedDistrictRef.current = feature + setFocusedDistrictId(feature.id) + + // Fly to the district + if (map && feature.longitude && feature.latitude) { + map.flyTo({ + center: [feature.longitude, feature.latitude], + zoom: 12, + pitch: 45, + bearing: 0, + duration: 1500, + easing: (t) => t * (2 - t), + }) + + // Hide clusters when focusing on district + if (map.getLayer("clusters")) { + map.getMap().setLayoutProperty("clusters", "visibility", "none") + } + if (map.getLayer("unclustered-point")) { + map.getMap().setLayoutProperty("unclustered-point", "visibility", "none") + } + } + }, + [map], + ) + + // Set up custom event handler for fly-to events + useEffect(() => { + if (!mapboxMap) return + + const handleFlyToEvent = (e: Event) => { + const customEvent = e as CustomEvent + if (!map || !customEvent.detail) return + + const { longitude, latitude, zoom, bearing, pitch, duration } = customEvent.detail + + map.flyTo({ + center: [longitude, latitude], + zoom: zoom || 15, + bearing: bearing || 0, + pitch: pitch || 45, + duration: duration || 2000, + }) + } + + mapboxMap.getCanvas().addEventListener("mapbox_fly_to", handleFlyToEvent as EventListener) + + return () => { + if (mapboxMap && mapboxMap.getCanvas()) { + mapboxMap.getCanvas().removeEventListener("mapbox_fly_to", handleFlyToEvent as EventListener) + } + } + }, [mapboxMap, map]) + + // Handle incident click events + useEffect(() => { + if (!mapboxMap) return + + const handleIncidentClickEvent = (e: Event) => { + const customEvent = e as CustomEvent + console.log("Received incident_click event in layers:", customEvent.detail) + + // Enhanced error checking + if (!customEvent.detail) { + console.error("Empty incident click event data") + return + } + + // Allow for different property names in the event data + const incidentId = customEvent.detail.id || customEvent.detail.incidentId || customEvent.detail.incident_id + + if (!incidentId) { + console.error("No incident ID found in event data:", customEvent.detail) + return + } + + console.log("Looking for incident with ID:", incidentId) + + // Improved incident finding + let foundIncident: ICrimeIncident | undefined + + // First try to use the data directly from the event if it has all needed properties + if ( + customEvent.detail.latitude !== undefined && + customEvent.detail.longitude !== undefined && + customEvent.detail.category !== undefined + ) { + foundIncident = { + id: incidentId, + district: customEvent.detail.district, + category: customEvent.detail.category, + type_category: customEvent.detail.type, + description: customEvent.detail.description, + status: customEvent.detail.status || "Unknown", + timestamp: customEvent.detail.timestamp ? new Date(customEvent.detail.timestamp) : undefined, + latitude: customEvent.detail.latitude, + longitude: customEvent.detail.longitude, + address: customEvent.detail.address, + } + } else { + // Otherwise search through the crimes data + for (const crime of crimes) { + for (const incident of crime.crime_incidents) { + if (incident.id === incidentId || incident.id?.toString() === incidentId?.toString()) { + console.log("Found matching incident:", incident) + foundIncident = { + id: incident.id, + district: crime.districts.name, + description: incident.description, + status: incident.status || "unknown", + timestamp: incident.timestamp, + category: incident.crime_categories.name, + type_category: incident.crime_categories.type, + address: incident.locations.address, + latitude: incident.locations.latitude, + longitude: incident.locations.longitude, + } + break + } + } + if (foundIncident) break + } + } + + if (!foundIncident) { + console.error("Could not find incident with ID:", incidentId) + return + } + + if (!foundIncident.latitude || !foundIncident.longitude) { + console.error("Found incident has invalid coordinates:", foundIncident) + return + } + + console.log("Setting selected incident:", foundIncident) + + // Clear any existing district selection first + setSelectedDistrict(null) + selectedDistrictRef.current = null + setFocusedDistrictId(null) + + // Set the selected incident + setSelectedIncident(foundIncident) + } + + // Add event listeners to both the map canvas and document + mapboxMap.getCanvas().addEventListener("incident_click", handleIncidentClickEvent as EventListener) + document.addEventListener("incident_click", handleIncidentClickEvent as EventListener) + + // For debugging purposes, log when this effect runs + console.log("Set up incident click event listener") + + return () => { + console.log("Removing incident click event listener") + if (mapboxMap && mapboxMap.getCanvas()) { + mapboxMap.getCanvas().removeEventListener("incident_click", handleIncidentClickEvent as EventListener) + } + document.removeEventListener("incident_click", handleIncidentClickEvent as EventListener) + } + }, [mapboxMap, crimes, setFocusedDistrictId]) // Update selected district when year/month/filter changes useEffect(() => { @@ -135,102 +316,103 @@ export default function Layers({ const districtId = selectedDistrictRef.current.id const districtCrime = crimes.find((crime) => crime.district_id === districtId) - if (districtCrime) { - const selectedYearNum = year ? Number.parseInt(year) : new Date().getFullYear() + if (districtCrime) { + const selectedYearNum = year ? Number.parseInt(year) : new Date().getFullYear() - let demographics = districtCrime.districts.demographics?.find((d) => d.year === selectedYearNum) + let demographics = districtCrime.districts.demographics?.find((d) => d.year === selectedYearNum) - if (!demographics && districtCrime.districts.demographics?.length) { - demographics = districtCrime.districts.demographics.sort((a, b) => b.year - a.year)[0] - } + if (!demographics && districtCrime.districts.demographics?.length) { + demographics = districtCrime.districts.demographics.sort((a, b) => b.year - a.year)[0] + } - let geographics = districtCrime.districts.geographics?.find((g) => g.year === selectedYearNum) + let geographics = districtCrime.districts.geographics?.find((g) => g.year === selectedYearNum) - if (!geographics && districtCrime.districts.geographics?.length) { - const validGeographics = districtCrime.districts.geographics - .filter((g) => g.year !== null) - .sort((a, b) => (b.year || 0) - (a.year || 0)) + if (!geographics && districtCrime.districts.geographics?.length) { + const validGeographics = districtCrime.districts.geographics + .filter((g) => g.year !== null) + .sort((a, b) => (b.year || 0) - (a.year || 0)) - geographics = validGeographics.length > 0 ? validGeographics[0] : districtCrime.districts.geographics[0] - } - - if (!demographics || !geographics) { - console.error("Missing district data:", { demographics, geographics }) - return - } - - const crime_incidents = districtCrime.crime_incidents - .filter((incident) => filterCategory === "all" || incident.crime_categories.name === filterCategory) - .map((incident) => ({ - id: incident.id, - timestamp: incident.timestamp, - description: incident.description, - status: incident.status || "", - category: incident.crime_categories.name, - type: incident.crime_categories.type || "", - address: incident.locations.address || "", - latitude: incident.locations.latitude, - longitude: incident.locations.longitude, - })) - - const updatedDistrict: IDistrictFeature = { - ...selectedDistrictRef.current, - number_of_crime: crimeDataByDistrict[districtId]?.number_of_crime || 0, - level: crimeDataByDistrict[districtId]?.level || selectedDistrictRef.current.level, - demographics: { - number_of_unemployed: demographics.number_of_unemployed, - population: demographics.population, - population_density: demographics.population_density, - year: demographics.year, - }, - geographics: { - address: geographics.address || "", - land_area: geographics.land_area || 0, - year: geographics.year || 0, - latitude: geographics.latitude, - longitude: geographics.longitude, - }, - crime_incidents, - selectedYear: year, - selectedMonth: month, - } - - selectedDistrictRef.current = updatedDistrict - - setSelectedDistrict((prevDistrict) => { - if ( - prevDistrict?.id === updatedDistrict.id && - prevDistrict?.selectedYear === updatedDistrict.selectedYear && - prevDistrict?.selectedMonth === updatedDistrict.selectedMonth - ) { - return prevDistrict - } - return updatedDistrict - }) - } + geographics = validGeographics.length > 0 ? validGeographics[0] : districtCrime.districts.geographics[0] } - }, [crimes, filterCategory, year, month, crimeDataByDistrict]) + + if (!demographics || !geographics) { + console.error("Missing district data:", { demographics, geographics }) + return + } + + const crime_incidents = districtCrime.crime_incidents + .filter((incident) => filterCategory === "all" || incident.crime_categories.name === filterCategory) + .map((incident) => ({ + id: incident.id, + timestamp: incident.timestamp, + description: incident.description, + status: incident.status || "", + category: incident.crime_categories.name, + type: incident.crime_categories.type || "", + address: incident.locations.address || "", + latitude: incident.locations.latitude, + longitude: incident.locations.longitude, + })) + + const updatedDistrict: IDistrictFeature = { + ...selectedDistrictRef.current, + number_of_crime: crimeDataByDistrict[districtId]?.number_of_crime || 0, + level: crimeDataByDistrict[districtId]?.level || selectedDistrictRef.current.level, + demographics: { + number_of_unemployed: demographics.number_of_unemployed, + population: demographics.population, + population_density: demographics.population_density, + year: demographics.year, + }, + geographics: { + address: geographics.address || "", + land_area: geographics.land_area || 0, + year: geographics.year || 0, + latitude: geographics.latitude, + longitude: geographics.longitude, + }, + crime_incidents, + selectedYear: year, + selectedMonth: month, + } + + selectedDistrictRef.current = updatedDistrict + + setSelectedDistrict((prevDistrict) => { + if ( + prevDistrict?.id === updatedDistrict.id && + prevDistrict?.selectedYear === updatedDistrict.selectedYear && + prevDistrict?.selectedMonth === updatedDistrict.selectedMonth + ) { + return prevDistrict + } + return updatedDistrict + }) + } + } + }, [crimes, filterCategory, year, month, crimeDataByDistrict]) // Make sure we have a defined handler for setFocusedDistrictId - const handleSetFocusedDistrictId = useCallback((id: string | null) => { - setFocusedDistrictId(id); - }, []); + const handleSetFocusedDistrictId = useCallback((id: string | null, isMarkerClick = false) => { + console.log("Setting focused district ID:", id, "from marker click:", isMarkerClick) + setFocusedDistrictId(id) + }, []) if (!visible) return null // Determine which layers should be visible based on the active control - const showDistrictLayer = activeControl === "incidents"; - const showHeatmapLayer = activeControl === "heatmap"; - const showClustersLayer = activeControl === "clusters"; - const showUnitsLayer = activeControl === "units"; - const showTimelineLayer = activeControl === "timeline"; + const showDistrictLayer = activeControl === "incidents" + const showHeatmapLayer = activeControl === "heatmap" + const showClustersLayer = activeControl === "clusters" + const showUnitsLayer = activeControl === "units" + const showTimelineLayer = activeControl === "timeline" // District fill should only be visible for incidents and clusters - const showDistrictFill = activeControl === "incidents" || activeControl === "clusters"; + const showDistrictFill = activeControl === "incidents" || activeControl === "clusters" // Show incident markers for incidents, clusters, AND units modes // But hide for heatmap and timeline - const showIncidentMarkers = activeControl !== "heatmap" && activeControl !== "timeline"; + const showIncidentMarkers = activeControl !== "heatmap" && activeControl !== "timeline" return ( <> @@ -248,80 +430,111 @@ export default function Layers({ crimeDataByDistrict={crimeDataByDistrict} showFill={showDistrictFill} activeControl={activeControl} - /> + onDistrictClick={handleDistrictClick} // Add this prop to pass the click handler + /> - {/* Heatmap Layer */} - + {/* Heatmap Layer */} + - {/* Timeline Layer - make sure this is the only visible layer in timeline mode */} - + {/* Timeline Layer - make sure this is the only visible layer in timeline mode */} + - {/* Units Layer - always show incidents when Units is active */} - + {/* Units Layer - always show incidents when Units is active */} + - {/* District base layer */} - + {/* Cluster Layer - only enable when the clusters control is active and NOT in timeline mode */} + - {/* Cluster Layer - only enable when the clusters control is active and NOT in timeline mode */} - + {/* Unclustered Points Layer - now show for both incidents and units modes */} + - {/* Unclustered Points Layer - now show for both incidents and units modes */} - + {/* District Popup */} + {selectedDistrict && !selectedIncident && ( + <> + - + + + )} - {selectedDistrict && ( - - )} - - ) + {/* Incident Popup */} + {selectedIncident && selectedIncident.latitude && selectedIncident.longitude && ( + + )} + + {/* Debug info for development */} +
+
Selected District: {selectedDistrict ? selectedDistrict.name : "None"}
+
Selected Incident: {selectedIncident ? selectedIncident.id : "None"}
+
Focused District ID: {focusedDistrictId || "None"}
+
+ + ) } diff --git a/sigap-website/app/_components/map/layers/uncluster-layer.tsx b/sigap-website/app/_components/map/layers/uncluster-layer.tsx index 5cd1fed..0d93436 100644 --- a/sigap-website/app/_components/map/layers/uncluster-layer.tsx +++ b/sigap-website/app/_components/map/layers/uncluster-layer.tsx @@ -1,6 +1,6 @@ "use client" -import { IUnclusteredPointLayerProps } from "@/app/_utils/types/map" +import type { IUnclusteredPointLayerProps } from "@/app/_utils/types/map" import { useEffect, useCallback } from "react" export default function UnclusteredPointLayer({ @@ -10,37 +10,6 @@ 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 @@ -68,36 +37,24 @@ export default function UnclusteredPointLayer({ console.log("Incident clicked:", incidentDetails) - // Create a custom event with incident details + // First fly to the incident location + map.flyTo({ + center: [incidentDetails.longitude, incidentDetails.latitude], + zoom: 15, + bearing: 0, + pitch: 45, + duration: 1000, + }) + + // Then dispatch the incident_click event to show the popup const customEvent = new CustomEvent("incident_click", { detail: incidentDetails, bubbles: true, }) - // Dispatch the event on both the map canvas and document to ensure it's captured - if (map.getCanvas()) { - map.getCanvas().dispatchEvent(customEvent) - } + // Dispatch on both the map canvas and document to ensure it's caught + map.getCanvas().dispatchEvent(customEvent) document.dispatchEvent(customEvent) - - // Also trigger a fly-to event to zoom to the incident - const flyToEvent = new CustomEvent("mapbox_fly_to", { - detail: { - longitude: incidentDetails.longitude, - latitude: incidentDetails.latitude, - zoom: 15, - bearing: 0, - pitch: 45, - duration: 1000, - }, - bubbles: true, - }) - - if (map.getCanvas()) { - map.getCanvas().dispatchEvent(flyToEvent) - } else { - document.dispatchEvent(flyToEvent) - } }, [map], ) @@ -105,26 +62,57 @@ export default function UnclusteredPointLayer({ 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); + ; (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; + 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 } } @@ -142,58 +130,58 @@ export default function UnclusteredPointLayer({ "circle-stroke-width": 1, "circle-stroke-color": "#fff", }, - layout: { - visibility: focusedDistrictId ? "none" : "visible", - }, + // layout: { + // visibility: focusedDistrictId ? "visible" : "visible", + // }, }, 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.getCanvas().style.cursor = "" + }) } else { // Update visibility based on focused district - map.setLayoutProperty("unclustered-point", "visibility", focusedDistrictId ? "none" : "visible"); + map.setLayoutProperty("unclustered-point", "visibility", focusedDistrictId ? "none" : "visible") } // Always ensure click handler is properly registered - map.off("click", "unclustered-point", handleIncidentClick); - map.on("click", "unclustered-point", handleIncidentClick); + map.off("click", "unclustered-point", handleIncidentClick) + map.on("click", "unclustered-point", handleIncidentClick) } catch (error) { - console.error("Error setting up unclustered point layer:", error); + console.error("Error setting up unclustered point layer:", error) } - }; + } // Check if style is loaded and set up layer accordingly if (map.isStyleLoaded()) { - setupLayerAndSource(); + setupLayerAndSource() } else { // Add event listener for style loading completion const onStyleLoad = () => { - setupLayerAndSource(); - }; + setupLayerAndSource() + } - map.once("style.load", onStyleLoad); + map.once("style.load", onStyleLoad) // Also wait a bit and try again as a fallback setTimeout(() => { if (map.isStyleLoaded()) { - setupLayerAndSource(); + setupLayerAndSource() } - }, 500); + }, 500) } return () => { if (map) { - map.off("click", "unclustered-point", handleIncidentClick); + map.off("click", "unclustered-point", handleIncidentClick) } - }; - }, [map, visible, focusedDistrictId, handleIncidentClick, crimes, filterCategory, geojsonData]); + } + }, [map, visible, focusedDistrictId, handleIncidentClick, crimes, filterCategory]) - return null; + return null } diff --git a/sigap-website/app/_components/map/map.tsx b/sigap-website/app/_components/map/map.tsx index f5dec46..f959a1b 100644 --- a/sigap-website/app/_components/map/map.tsx +++ b/sigap-website/app/_components/map/map.tsx @@ -82,41 +82,25 @@ export default function MapView({ useEffect(() => { if (!mapRef.current) return; - const mapElement = mapRef.current.getMap().getContainer(); + const map = mapRef.current.getMap(); - // Handle fly to event - const handleMapFly = (e: CustomEvent) => { - if (!e.detail || !mapRef.current) return; - - const { center, zoom, bearing, pitch, duration, resetCamera } = e.detail; - - if (resetCamera) { - // Reset to default view - mapRef.current.flyTo({ - center: [BASE_LONGITUDE, BASE_LATITUDE], - zoom: BASE_ZOOM, - bearing: BASE_BEARING, - pitch: BASE_PITCH, - duration, - essential: true - }); - } else { - // Fly to specific location - mapRef.current.flyTo({ - center, - zoom, - bearing, - pitch, - duration, - essential: true - }); - } + const handleFlyToEvent = (e: Event) => { + const customEvent = e as CustomEvent; + if (!customEvent.detail) return; + const { longitude, latitude, zoom, bearing, pitch, duration } = customEvent.detail; + map.flyTo({ + center: [longitude, latitude], + zoom: zoom || 15, + bearing: bearing || 0, + pitch: pitch || 45, + duration: duration || 2000, + }); }; - mapElement.addEventListener('mapbox_fly', handleMapFly as EventListener); + map.getContainer().addEventListener('mapbox_fly_to', handleFlyToEvent as EventListener); return () => { - mapElement.removeEventListener('mapbox_fly', handleMapFly as EventListener); + map.getContainer().removeEventListener('mapbox_fly_to', handleFlyToEvent as EventListener); }; }, [mapRef.current]);