diff --git a/sigap-website/app/_components/map/containers/alert-layer-container.tsx b/sigap-website/app/_components/map/containers/alert-layer-container.tsx index 37e21dc..745f317 100644 --- a/sigap-website/app/_components/map/containers/alert-layer-container.tsx +++ b/sigap-website/app/_components/map/containers/alert-layer-container.tsx @@ -1,42 +1,42 @@ -"use client"; +// "use client"; -import { useState, useEffect } from 'react'; -import mapboxgl from 'mapbox-gl'; -import EWSAlertLayer from '../layers/ews-alert-layer'; +// import { useState, useEffect } from 'react'; +// import mapboxgl from 'mapbox-gl'; +// import EWSAlertLayer from '../layers/ews-alert-layer'; -import { IIncidentLog } from '@/app/_utils/types/ews'; +// import { IIncidentLog } from '@/app/_utils/types/ews'; -interface AlertLayerContainerProps { - map: mapboxgl.Map | null; - activeLayer: string; - incidents: IIncidentLog[]; - onIncidentResolved?: (id: string) => void; -} +// interface AlertLayerContainerProps { +// map: mapboxgl.Map | null; +// activeLayer: string; +// incidents: IIncidentLog[]; +// onIncidentResolved?: (id: string) => void; +// } -export default function AlertLayerContainer({ - map, - activeLayer, - incidents, - onIncidentResolved, -}: AlertLayerContainerProps) { - const [ewsVisible, setEwsVisible] = useState(false); +// export default function AlertLayerContainer({ +// map, +// activeLayer, +// incidents, +// onIncidentResolved, +// }: AlertLayerContainerProps) { +// const [ewsVisible, setEwsVisible] = useState(false); - // Determine which layers to show based on activeLayer - useEffect(() => { - const isAlertLayer = activeLayer === 'alerts'; - setEwsVisible(isAlertLayer); - }, [activeLayer]); +// // Determine which layers to show based on activeLayer +// useEffect(() => { +// const isAlertLayer = activeLayer === 'alerts'; +// setEwsVisible(isAlertLayer); +// }, [activeLayer]); - return ( - <> - {/* EWS Alert Layer for emergency notifications */} - - - ); -} +// return ( +// <> +// {/* EWS Alert Layer for emergency notifications */} +// +// +// ); +// } diff --git a/sigap-website/app/_components/map/crime-map.tsx b/sigap-website/app/_components/map/crime-map.tsx index c06d61e..d628582 100644 --- a/sigap-website/app/_components/map/crime-map.tsx +++ b/sigap-website/app/_components/map/crime-map.tsx @@ -25,6 +25,11 @@ import Layers from "./layers/layers" import { useGetUnitsQuery } from "@/app/(pages)/(admin)/dashboard/crime-management/units/_queries/queries" import { IDistrictFeature } from "@/app/_utils/types/map" +import EWSAlertLayer from "./layers/ews-alert-layer" +import { IIncidentLog } from "@/app/_utils/types/ews" +import { addMockIncident, getAllIncidents, resolveIncident } from "@/app/_utils/mock/ews-data" +import { useMap } from "react-map-gl/mapbox" +import PanicButtonDemo from "./controls/panic-button-demo" export default function CrimeMap() { const [sidebarCollapsed, setSidebarCollapsed] = useState(true) @@ -45,9 +50,16 @@ export default function CrimeMap() { const [useAllYears, setUseAllYears] = useState(false) const [useAllMonths, setUseAllMonths] = useState(false) const [showEWS, setShowEWS] = useState(true) + const [ewsIncidents, setEwsIncidents] = useState([]) + const [showPanicDemo, setShowPanicDemo] = useState(true) + const [displayPanicDemo, setDisplayPanicDemo] = useState(showEWS && showPanicDemo) const mapContainerRef = useRef(null) + const { current: mapInstance } = useMap() + + const mapboxMap = mapInstance?.getMap() || null + const { isFullscreen } = useFullscreen(mapContainerRef) const { data: availableSourceTypes, isLoading: isTypeLoading } = useGetCrimeTypes() @@ -142,6 +154,29 @@ export default function CrimeMap() { } }, [selectedSourceType, activeControl]); + useEffect(() => { + setEwsIncidents(getAllIncidents()) + }, []) + + const handleTriggerAlert = useCallback((priority: "high" | "medium" | "low") => { + const newIncident = addMockIncident({ priority }) + setEwsIncidents(getAllIncidents()) + }, []) + + const handleResolveIncident = useCallback((id: string) => { + resolveIncident(id) + setEwsIncidents(getAllIncidents()) + }, []) + + const handleResolveAllAlerts = useCallback(() => { + ewsIncidents.forEach((incident) => { + if (incident.status === "active") { + resolveIncident(incident.id) + } + }) + setEwsIncidents(getAllIncidents()) + }, [ewsIncidents]) + const handleSourceTypeChange = useCallback((sourceType: string) => { setSelectedSourceType(sourceType); @@ -283,6 +318,7 @@ export default function CrimeMap() { sourceType={selectedSourceType} /> + {isFullscreen && ( <>
@@ -304,6 +340,19 @@ export default function CrimeMap() { />
+ {mapboxMap && ( + + )} + {displayPanicDemo && ( +
+ inc.status === "active")} + /> +
+ )} + - {isFullscreen && ( -
- {showClusters && ( - - )} - {showUnclustered && !showClusters && ( - - )} -
- )} - {isFullscreen && showUnitsLayer && ( +
+ {showClusters && ( + + )} + {showUnclustered && !showClusters && ( + + )} +
+ + + {showUnitsLayer && (
)} - {isFullscreen && showTimelineLayer && ( + {showTimelineLayer && (
@@ -340,17 +389,17 @@ export default function CrimeMap() { )} - {isFullscreen && ( -
- -
- )} + +
+ +
+
diff --git a/sigap-website/app/_components/map/layers/layers.tsx b/sigap-website/app/_components/map/layers/layers.tsx index 2031f63..8ece396 100644 --- a/sigap-website/app/_components/map/layers/layers.tsx +++ b/sigap-website/app/_components/map/layers/layers.tsx @@ -93,6 +93,7 @@ export default function Layers({ sourceType = "cbt", }: LayersProps) { const animationRef = useRef(null) + const { current: map } = useMap() if (!map) { @@ -115,28 +116,28 @@ export default function Layers({ const [showPanicDemo, setShowPanicDemo] = useState(true) const [displayPanicDemo, setDisplayPanicDemo] = useState(showEWS && showPanicDemo) - useEffect(() => { - setEwsIncidents(getAllIncidents()) - }, []) + // useEffect(() => { + // setEwsIncidents(getAllIncidents()) + // }, []) - const handleTriggerAlert = useCallback((priority: "high" | "medium" | "low") => { - const newIncident = addMockIncident({ priority }) - setEwsIncidents(getAllIncidents()) - }, []) + // const handleTriggerAlert = useCallback((priority: "high" | "medium" | "low") => { + // const newIncident = addMockIncident({ priority }) + // setEwsIncidents(getAllIncidents()) + // }, []) - const handleResolveIncident = useCallback((id: string) => { - resolveIncident(id) - setEwsIncidents(getAllIncidents()) - }, []) + // const handleResolveIncident = useCallback((id: string) => { + // resolveIncident(id) + // setEwsIncidents(getAllIncidents()) + // }, []) - const handleResolveAllAlerts = useCallback(() => { - ewsIncidents.forEach((incident) => { - if (incident.status === "active") { - resolveIncident(incident.id) - } - }) - setEwsIncidents(getAllIncidents()) - }, [ewsIncidents]) + // const handleResolveAllAlerts = useCallback(() => { + // ewsIncidents.forEach((incident) => { + // if (incident.status === "active") { + // resolveIncident(incident.id) + // } + // }) + // setEwsIncidents(getAllIncidents()) + // }, [ewsIncidents]) const handlePopupClose = useCallback(() => { selectedDistrictRef.current = null @@ -534,9 +535,9 @@ export default function Layers({ - {showEWS && } + {/* {showEWS && } */} - {showEWS && displayPanicDemo && ( + {/* {showEWS && displayPanicDemo && (
inc.status === "active")} />
- )} + )} */} diff --git a/sigap-website/app/_components/map/layers/units-layer.tsx b/sigap-website/app/_components/map/layers/units-layer.tsx index ffe9def..03544cb 100644 --- a/sigap-website/app/_components/map/layers/units-layer.tsx +++ b/sigap-website/app/_components/map/layers/units-layer.tsx @@ -30,6 +30,20 @@ interface IDistrictIncidents { timestamp: Date } +// New interface to better type the incident properties +interface IncidentProperties { + id: string + description: string + category: string + date: string + district: string + district_id: string + categoryColor: string + distance_to_unit: number | "Unknown" + longitude: number + latitude: number +} + export default function UnitsLayer({ crimes, units = [], filterCategory, visible = false, map }: UnitsLayerProps) { const [loadedUnits, setLoadedUnits] = useState([]) const loadedUnitsRef = useRef([]) @@ -50,6 +64,9 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible 5 ) + // Add a ref to store pre-processed incidents by district for optimization + const districtIncidentsCache = useRef>(new Map()); + // Use either provided units or loaded units const unitsData = useMemo(() => { return units.length > 0 ? units : loadedUnits || [] @@ -105,7 +122,15 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible const incidentsGeoJSON = useMemo(() => { const features: any[] = [] + // Also build the district incidents cache while processing crime data + const newDistrictIncidentsCache = new Map(); + crimes.forEach((crime) => { + // Initialize the array for this district if it doesn't exist yet + if (!newDistrictIncidentsCache.has(crime.district_id)) { + newDistrictIncidentsCache.set(crime.district_id, []); + } + crime.crime_incidents.forEach((incident) => { // Skip incidents without location data or filtered by category if ( @@ -115,6 +140,22 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible ) return + // Ensure distance_to_unit is properly initialized + const distance = incident.locations.distance_to_unit !== undefined + ? incident.locations.distance_to_unit + : "Unknown"; + + // Add to district incidents cache for quicker lookup + if (incident.locations.distance_to_unit !== undefined) { + newDistrictIncidentsCache.get(crime.district_id)?.push({ + incident_id: incident.id, + category_name: incident.crime_categories.name, + incident_description: incident.description || "No description", + distance_meters: incident.locations.distance_to_unit!, + timestamp: incident.timestamp, + }); + } + features.push({ type: "Feature" as const, properties: { @@ -125,7 +166,7 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible district: crime.districts.name, district_id: crime.district_id, categoryColor: categoryColorMap[incident.crime_categories.name] || "#22c55e", - distance_to_unit: incident.locations.distance_to_unit || "Unknown", + distance_to_unit: distance, }, geometry: { type: "Point" as const, @@ -135,6 +176,9 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible }) }) + // Update the cache ref with our new data + districtIncidentsCache.current = newDistrictIncidentsCache; + return { type: "FeatureCollection" as const, features, @@ -245,32 +289,43 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible setIsLoading(true) - // Find all incidents in the same district as the unit - const districtIncidents: IDistrictIncidents[] = [] - crimes.forEach((crime) => { + // Early exit if district_id is not available + if (!unit.district_id) { + console.log("Unit has no district ID") + setUnitIncident([]) + setIsLoading(false) + return + } - console.log("Processing crime:", crime.district_id, unit.district_id) // Debug log + // Use the pre-processed district incidents from cache + let districtIncidents = districtIncidentsCache.current.get(unit.district_id) || []; - // Check if this crime is in the same district as the unit - if (crime.district_id === unit.district_id) { - crime.crime_incidents.forEach((incident) => { - if (incident.locations && typeof incident.locations.distance_to_unit !== "undefined") { - districtIncidents.push({ - incident_id: incident.id, - category_name: incident.crime_categories.name, - incident_description: incident.description || "No description", - distance_meters: incident.locations.distance_to_unit!, - timestamp: incident.timestamp, - }) - } - }) - } - }) + // If we don't have them in cache for some reason, compute them now + if (districtIncidents.length === 0) { + const tempIncidents: IDistrictIncidents[] = []; + + // Only process crimes for this specific district + crimes + .filter(crime => crime.district_id === unit.district_id) + .forEach(crime => { + crime.crime_incidents.forEach(incident => { + if (incident.locations && typeof incident.locations.distance_to_unit !== "undefined") { + tempIncidents.push({ + incident_id: incident.id, + category_name: incident.crime_categories.name, + incident_description: incident.description || "No description", + distance_meters: incident.locations.distance_to_unit!, + timestamp: incident.timestamp, + }); + } + }); + }); + + districtIncidents = tempIncidents; + } // Sort by distance (closest first) - districtIncidents.sort((a, b) => a.distance_meters - b.distance_meters) - - // console.log("Sorted district incidents:", districtIncidents) + districtIncidents.sort((a, b) => a.distance_meters - b.distance_meters); // Update the state with the distance results setUnitIncident(districtIncidents) @@ -350,6 +405,10 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible duration: BASE_DURATION, }) + // Ensure distance_to_unit has a value - use the value from GeoJSON properties directly + // This ensures we use the same data that was calculated for the GeoJSON + let distanceToUnit = properties.distance_to_unit; + // Create incident object from properties const incident = { id: properties.id, @@ -358,14 +417,11 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible date: properties.date, district: properties.district, district_id: properties.district_id, - distance_to_unit: properties.distance_to_unit, + distance_to_unit: distanceToUnit, longitude, latitude, } - // Debug log - console.log("Incident clicked:", incident) - // Set the selected incident and query parameters setSelectedIncident(incident) setSelectedUnit(null) // Clear any selected unit @@ -386,6 +442,7 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible district: properties.district, category: properties.category, description: properties.description, + distance_to_unit: distanceToUnit, longitude, latitude, }, @@ -438,11 +495,11 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible if (!map || !visible) return // Debug log untuk memeriksa keberadaan layer - console.log("Setting up event handlers, map layers:", - map.getStyle().layers?.filter(l => - l.id === "units-points" || l.id === "incidents-points" - ).map(l => l.id) - ) + // console.log("Setting up event handlers, map layers:", + // map.getStyle().layers?.filter(l => + // l.id === "units-points" || l.id === "incidents-points" + // ).map(l => l.id) + // ) // Define event handlers that can be referenced for both adding and removing const handleMouseEnter = () => { @@ -461,7 +518,7 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible map.on("click", "units-points", unitClickHandler) map.on("mouseenter", "units-points", handleMouseEnter) map.on("mouseleave", "units-points", handleMouseLeave) - console.log("✅ Unit points handler attached") + // console.log("✅ Unit points handler attached") } else { console.log("❌ units-points layer not found") } @@ -472,7 +529,7 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible map.on("click", "incidents-points", incidentClickHandler) map.on("mouseenter", "incidents-points", handleMouseEnter) map.on("mouseleave", "incidents-points", handleMouseLeave) - console.log("✅ Incident points handler attached") + // console.log("✅ Incident points handler attached") } else { console.log("❌ incidents-points layer not found") } @@ -516,7 +573,7 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible // Reset map filters when popup is closed const handleClosePopup = useCallback(() => { - console.log("Closing popup, clearing selected states") + // console.log("Closing popup, clearing selected states") setSelectedUnit(null) setSelectedIncident(null) setSelectedEntityId(undefined) @@ -545,13 +602,13 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible }, [visible, handleClosePopup]) // Debug untuk komponen render - useEffect(() => { - console.log("Render state:", { - selectedUnit: selectedUnit?.code_unit, - selectedIncident: selectedIncident?.id, - visible - }) - }, [selectedUnit, selectedIncident, visible]) + // useEffect(() => { + // console.log("Render state:", { + // selectedUnit: selectedUnit?.code_unit, + // selectedIncident: selectedIncident?.id, + // visible + // }) + // }, [selectedUnit, selectedIncident, visible]) if (!visible) return null diff --git a/sigap-website/app/_components/map/pop-up/timeline-popup.tsx b/sigap-website/app/_components/map/pop-up/timeline-popup.tsx index 37d4a1e..d8e5b16 100644 --- a/sigap-website/app/_components/map/pop-up/timeline-popup.tsx +++ b/sigap-website/app/_components/map/pop-up/timeline-popup.tsx @@ -117,28 +117,6 @@ export default function TimelinePopup({ - {/* Connection line */} -
- {/* Connection dot */} -
)