fix: fix kesalahan oengambilan properti distance pada units layer

This commit is contained in:
vergiLgood1 2025-05-14 17:17:10 +07:00
parent 58f033d0e4
commit 849b3c1ae3
5 changed files with 229 additions and 144 deletions

View File

@ -1,42 +1,42 @@
"use client"; // "use client";
import { useState, useEffect } from 'react'; // import { useState, useEffect } from 'react';
import mapboxgl from 'mapbox-gl'; // import mapboxgl from 'mapbox-gl';
import EWSAlertLayer from '../layers/ews-alert-layer'; // import EWSAlertLayer from '../layers/ews-alert-layer';
import { IIncidentLog } from '@/app/_utils/types/ews'; // import { IIncidentLog } from '@/app/_utils/types/ews';
interface AlertLayerContainerProps { // interface AlertLayerContainerProps {
map: mapboxgl.Map | null; // map: mapboxgl.Map | null;
activeLayer: string; // activeLayer: string;
incidents: IIncidentLog[]; // incidents: IIncidentLog[];
onIncidentResolved?: (id: string) => void; // onIncidentResolved?: (id: string) => void;
} // }
export default function AlertLayerContainer({ // export default function AlertLayerContainer({
map, // map,
activeLayer, // activeLayer,
incidents, // incidents,
onIncidentResolved, // onIncidentResolved,
}: AlertLayerContainerProps) { // }: AlertLayerContainerProps) {
const [ewsVisible, setEwsVisible] = useState(false); // const [ewsVisible, setEwsVisible] = useState(false);
// Determine which layers to show based on activeLayer // // Determine which layers to show based on activeLayer
useEffect(() => { // useEffect(() => {
const isAlertLayer = activeLayer === 'alerts'; // const isAlertLayer = activeLayer === 'alerts';
setEwsVisible(isAlertLayer); // setEwsVisible(isAlertLayer);
}, [activeLayer]); // }, [activeLayer]);
return ( // return (
<> // <>
{/* EWS Alert Layer for emergency notifications */} // {/* EWS Alert Layer for emergency notifications */}
<EWSAlertLayer // <EWSAlertLayer
map={map} // map={map}
incidents={incidents} // incidents={incidents}
onIncidentResolved={onIncidentResolved} // onIncidentResolved={onIncidentResolved}
visible={ewsVisible} // visible={ewsVisible}
/> // />
</> // </>
); // );
} // }

View File

@ -25,6 +25,11 @@ import Layers from "./layers/layers"
import { useGetUnitsQuery } from "@/app/(pages)/(admin)/dashboard/crime-management/units/_queries/queries" import { useGetUnitsQuery } from "@/app/(pages)/(admin)/dashboard/crime-management/units/_queries/queries"
import { IDistrictFeature } from "@/app/_utils/types/map" 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() { export default function CrimeMap() {
const [sidebarCollapsed, setSidebarCollapsed] = useState(true) const [sidebarCollapsed, setSidebarCollapsed] = useState(true)
@ -45,9 +50,16 @@ export default function CrimeMap() {
const [useAllYears, setUseAllYears] = useState<boolean>(false) const [useAllYears, setUseAllYears] = useState<boolean>(false)
const [useAllMonths, setUseAllMonths] = useState<boolean>(false) const [useAllMonths, setUseAllMonths] = useState<boolean>(false)
const [showEWS, setShowEWS] = useState<boolean>(true) const [showEWS, setShowEWS] = useState<boolean>(true)
const [ewsIncidents, setEwsIncidents] = useState<IIncidentLog[]>([])
const [showPanicDemo, setShowPanicDemo] = useState(true)
const [displayPanicDemo, setDisplayPanicDemo] = useState(showEWS && showPanicDemo)
const mapContainerRef = useRef<HTMLDivElement>(null) const mapContainerRef = useRef<HTMLDivElement>(null)
const { current: mapInstance } = useMap()
const mapboxMap = mapInstance?.getMap() || null
const { isFullscreen } = useFullscreen(mapContainerRef) const { isFullscreen } = useFullscreen(mapContainerRef)
const { data: availableSourceTypes, isLoading: isTypeLoading } = useGetCrimeTypes() const { data: availableSourceTypes, isLoading: isTypeLoading } = useGetCrimeTypes()
@ -142,6 +154,29 @@ export default function CrimeMap() {
} }
}, [selectedSourceType, activeControl]); }, [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) => { const handleSourceTypeChange = useCallback((sourceType: string) => {
setSelectedSourceType(sourceType); setSelectedSourceType(sourceType);
@ -283,6 +318,7 @@ export default function CrimeMap() {
sourceType={selectedSourceType} sourceType={selectedSourceType}
/> />
{isFullscreen && ( {isFullscreen && (
<> <>
<div className="absolute flex w-full p-2"> <div className="absolute flex w-full p-2">
@ -304,6 +340,19 @@ export default function CrimeMap() {
/> />
</div> </div>
{mapboxMap && (
<EWSAlertLayer map={mapboxMap} incidents={ewsIncidents} onIncidentResolved={handleResolveIncident} />
)}
{displayPanicDemo && (
<div className="absolute top-0 right-20 z-50 p-2">
<PanicButtonDemo
onTriggerAlert={handleTriggerAlert}
onResolveAllAlerts={handleResolveAllAlerts}
activeIncidents={ewsIncidents.filter((inc) => inc.status === "active")}
/>
</div>
)}
<CrimeSidebar <CrimeSidebar
crimes={filteredCrimes || []} crimes={filteredCrimes || []}
defaultCollapsed={sidebarCollapsed} defaultCollapsed={sidebarCollapsed}
@ -312,18 +361,18 @@ export default function CrimeMap() {
selectedMonth={selectedMonth} selectedMonth={selectedMonth}
sourceType={selectedSourceType} // Pass the sourceType sourceType={selectedSourceType} // Pass the sourceType
/> />
{isFullscreen && (
<div className="absolute bottom-20 right-0 z-20 p-2">
{showClusters && (
<MapLegend position="bottom-right" />
)}
{showUnclustered && !showClusters && (
<MapLegend position="bottom-right" />
)}
</div>
)}
{isFullscreen && showUnitsLayer && ( <div className="absolute bottom-20 right-0 z-20 p-2">
{showClusters && (
<MapLegend position="bottom-right" />
)}
{showUnclustered && !showClusters && (
<MapLegend position="bottom-right" />
)}
</div>
{showUnitsLayer && (
<div className="absolute bottom-20 right-0 z-10 p-2"> <div className="absolute bottom-20 right-0 z-10 p-2">
<UnitsLegend <UnitsLegend
categories={categories} categories={categories}
@ -332,7 +381,7 @@ export default function CrimeMap() {
</div> </div>
)} )}
{isFullscreen && showTimelineLayer && ( {showTimelineLayer && (
<div className="absolute flex bottom-20 right-0 z-10 p-2"> <div className="absolute flex bottom-20 right-0 z-10 p-2">
<TimelineLegend position="bottom-right" /> <TimelineLegend position="bottom-right" />
</div> </div>
@ -340,17 +389,17 @@ export default function CrimeMap() {
</> </>
)} )}
{isFullscreen && (
<div className="absolute flex w-full bottom-0"> <div className="absolute flex w-full bottom-0">
<CrimeTimelapse <CrimeTimelapse
startYear={2020} startYear={2020}
endYear={2024} endYear={2024}
autoPlay={false} autoPlay={false}
onChange={handleTimelineChange} onChange={handleTimelineChange}
onPlayingChange={handleTimelinePlayingChange} onPlayingChange={handleTimelinePlayingChange}
/> />
</div> </div>
)}
</MapView> </MapView>
</div> </div>
</div> </div>

View File

@ -93,6 +93,7 @@ export default function Layers({
sourceType = "cbt", sourceType = "cbt",
}: LayersProps) { }: LayersProps) {
const animationRef = useRef<number | null>(null) const animationRef = useRef<number | null>(null)
const { current: map } = useMap() const { current: map } = useMap()
if (!map) { if (!map) {
@ -115,28 +116,28 @@ export default function Layers({
const [showPanicDemo, setShowPanicDemo] = useState(true) const [showPanicDemo, setShowPanicDemo] = useState(true)
const [displayPanicDemo, setDisplayPanicDemo] = useState(showEWS && showPanicDemo) const [displayPanicDemo, setDisplayPanicDemo] = useState(showEWS && showPanicDemo)
useEffect(() => { // useEffect(() => {
setEwsIncidents(getAllIncidents()) // setEwsIncidents(getAllIncidents())
}, []) // }, [])
const handleTriggerAlert = useCallback((priority: "high" | "medium" | "low") => { // const handleTriggerAlert = useCallback((priority: "high" | "medium" | "low") => {
const newIncident = addMockIncident({ priority }) // const newIncident = addMockIncident({ priority })
setEwsIncidents(getAllIncidents()) // setEwsIncidents(getAllIncidents())
}, []) // }, [])
const handleResolveIncident = useCallback((id: string) => { // const handleResolveIncident = useCallback((id: string) => {
resolveIncident(id) // resolveIncident(id)
setEwsIncidents(getAllIncidents()) // setEwsIncidents(getAllIncidents())
}, []) // }, [])
const handleResolveAllAlerts = useCallback(() => { // const handleResolveAllAlerts = useCallback(() => {
ewsIncidents.forEach((incident) => { // ewsIncidents.forEach((incident) => {
if (incident.status === "active") { // if (incident.status === "active") {
resolveIncident(incident.id) // resolveIncident(incident.id)
} // }
}) // })
setEwsIncidents(getAllIncidents()) // setEwsIncidents(getAllIncidents())
}, [ewsIncidents]) // }, [ewsIncidents])
const handlePopupClose = useCallback(() => { const handlePopupClose = useCallback(() => {
selectedDistrictRef.current = null selectedDistrictRef.current = null
@ -534,9 +535,9 @@ export default function Layers({
<FaultLinesLayer map={mapboxMap} /> <FaultLinesLayer map={mapboxMap} />
{showEWS && <EWSAlertLayer map={mapboxMap} incidents={ewsIncidents} onIncidentResolved={handleResolveIncident} />} {/* {showEWS && <EWSAlertLayer map={mapboxMap} incidents={ewsIncidents} onIncidentResolved={handleResolveIncident} />} */}
{showEWS && displayPanicDemo && ( {/* {showEWS && displayPanicDemo && (
<div className="absolute top-0 right-20 z-50 p-2"> <div className="absolute top-0 right-20 z-50 p-2">
<PanicButtonDemo <PanicButtonDemo
onTriggerAlert={handleTriggerAlert} onTriggerAlert={handleTriggerAlert}
@ -544,7 +545,7 @@ export default function Layers({
activeIncidents={ewsIncidents.filter((inc) => inc.status === "active")} activeIncidents={ewsIncidents.filter((inc) => inc.status === "active")}
/> />
</div> </div>
)} )} */}
</> </>

View File

@ -30,6 +30,20 @@ interface IDistrictIncidents {
timestamp: Date 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) { export default function UnitsLayer({ crimes, units = [], filterCategory, visible = false, map }: UnitsLayerProps) {
const [loadedUnits, setLoadedUnits] = useState<IUnits[]>([]) const [loadedUnits, setLoadedUnits] = useState<IUnits[]>([])
const loadedUnitsRef = useRef<IUnits[]>([]) const loadedUnitsRef = useRef<IUnits[]>([])
@ -50,6 +64,9 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
5 5
) )
// Add a ref to store pre-processed incidents by district for optimization
const districtIncidentsCache = useRef<Map<string, IDistrictIncidents[]>>(new Map());
// Use either provided units or loaded units // Use either provided units or loaded units
const unitsData = useMemo(() => { const unitsData = useMemo(() => {
return units.length > 0 ? units : loadedUnits || [] return units.length > 0 ? units : loadedUnits || []
@ -105,7 +122,15 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
const incidentsGeoJSON = useMemo(() => { const incidentsGeoJSON = useMemo(() => {
const features: any[] = [] const features: any[] = []
// Also build the district incidents cache while processing crime data
const newDistrictIncidentsCache = new Map<string, IDistrictIncidents[]>();
crimes.forEach((crime) => { 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) => { crime.crime_incidents.forEach((incident) => {
// Skip incidents without location data or filtered by category // Skip incidents without location data or filtered by category
if ( if (
@ -115,6 +140,22 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
) )
return 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({ features.push({
type: "Feature" as const, type: "Feature" as const,
properties: { properties: {
@ -125,7 +166,7 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
district: crime.districts.name, district: crime.districts.name,
district_id: crime.district_id, district_id: crime.district_id,
categoryColor: categoryColorMap[incident.crime_categories.name] || "#22c55e", categoryColor: categoryColorMap[incident.crime_categories.name] || "#22c55e",
distance_to_unit: incident.locations.distance_to_unit || "Unknown", distance_to_unit: distance,
}, },
geometry: { geometry: {
type: "Point" as const, 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 { return {
type: "FeatureCollection" as const, type: "FeatureCollection" as const,
features, features,
@ -245,32 +289,43 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
setIsLoading(true) setIsLoading(true)
// Find all incidents in the same district as the unit // Early exit if district_id is not available
const districtIncidents: IDistrictIncidents[] = [] if (!unit.district_id) {
crimes.forEach((crime) => { 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 we don't have them in cache for some reason, compute them now
if (crime.district_id === unit.district_id) { if (districtIncidents.length === 0) {
crime.crime_incidents.forEach((incident) => { const tempIncidents: IDistrictIncidents[] = [];
if (incident.locations && typeof incident.locations.distance_to_unit !== "undefined") {
districtIncidents.push({ // Only process crimes for this specific district
incident_id: incident.id, crimes
category_name: incident.crime_categories.name, .filter(crime => crime.district_id === unit.district_id)
incident_description: incident.description || "No description", .forEach(crime => {
distance_meters: incident.locations.distance_to_unit!, crime.crime_incidents.forEach(incident => {
timestamp: incident.timestamp, 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) // Sort by distance (closest first)
districtIncidents.sort((a, b) => a.distance_meters - b.distance_meters) districtIncidents.sort((a, b) => a.distance_meters - b.distance_meters);
// console.log("Sorted district incidents:", districtIncidents)
// Update the state with the distance results // Update the state with the distance results
setUnitIncident(districtIncidents) setUnitIncident(districtIncidents)
@ -350,6 +405,10 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
duration: BASE_DURATION, 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 // Create incident object from properties
const incident = { const incident = {
id: properties.id, id: properties.id,
@ -358,14 +417,11 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
date: properties.date, date: properties.date,
district: properties.district, district: properties.district,
district_id: properties.district_id, district_id: properties.district_id,
distance_to_unit: properties.distance_to_unit, distance_to_unit: distanceToUnit,
longitude, longitude,
latitude, latitude,
} }
// Debug log
console.log("Incident clicked:", incident)
// Set the selected incident and query parameters // Set the selected incident and query parameters
setSelectedIncident(incident) setSelectedIncident(incident)
setSelectedUnit(null) // Clear any selected unit setSelectedUnit(null) // Clear any selected unit
@ -386,6 +442,7 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
district: properties.district, district: properties.district,
category: properties.category, category: properties.category,
description: properties.description, description: properties.description,
distance_to_unit: distanceToUnit,
longitude, longitude,
latitude, latitude,
}, },
@ -438,11 +495,11 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
if (!map || !visible) return if (!map || !visible) return
// Debug log untuk memeriksa keberadaan layer // Debug log untuk memeriksa keberadaan layer
console.log("Setting up event handlers, map layers:", // console.log("Setting up event handlers, map layers:",
map.getStyle().layers?.filter(l => // map.getStyle().layers?.filter(l =>
l.id === "units-points" || l.id === "incidents-points" // l.id === "units-points" || l.id === "incidents-points"
).map(l => l.id) // ).map(l => l.id)
) // )
// Define event handlers that can be referenced for both adding and removing // Define event handlers that can be referenced for both adding and removing
const handleMouseEnter = () => { const handleMouseEnter = () => {
@ -461,7 +518,7 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
map.on("click", "units-points", unitClickHandler) map.on("click", "units-points", unitClickHandler)
map.on("mouseenter", "units-points", handleMouseEnter) map.on("mouseenter", "units-points", handleMouseEnter)
map.on("mouseleave", "units-points", handleMouseLeave) map.on("mouseleave", "units-points", handleMouseLeave)
console.log("✅ Unit points handler attached") // console.log("✅ Unit points handler attached")
} else { } else {
console.log("❌ units-points layer not found") 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("click", "incidents-points", incidentClickHandler)
map.on("mouseenter", "incidents-points", handleMouseEnter) map.on("mouseenter", "incidents-points", handleMouseEnter)
map.on("mouseleave", "incidents-points", handleMouseLeave) map.on("mouseleave", "incidents-points", handleMouseLeave)
console.log("✅ Incident points handler attached") // console.log("✅ Incident points handler attached")
} else { } else {
console.log("❌ incidents-points layer not found") 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 // Reset map filters when popup is closed
const handleClosePopup = useCallback(() => { const handleClosePopup = useCallback(() => {
console.log("Closing popup, clearing selected states") // console.log("Closing popup, clearing selected states")
setSelectedUnit(null) setSelectedUnit(null)
setSelectedIncident(null) setSelectedIncident(null)
setSelectedEntityId(undefined) setSelectedEntityId(undefined)
@ -545,13 +602,13 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
}, [visible, handleClosePopup]) }, [visible, handleClosePopup])
// Debug untuk komponen render // Debug untuk komponen render
useEffect(() => { // useEffect(() => {
console.log("Render state:", { // console.log("Render state:", {
selectedUnit: selectedUnit?.code_unit, // selectedUnit: selectedUnit?.code_unit,
selectedIncident: selectedIncident?.id, // selectedIncident: selectedIncident?.id,
visible // visible
}) // })
}, [selectedUnit, selectedIncident, visible]) // }, [selectedUnit, selectedIncident, visible])
if (!visible) return null if (!visible) return null

View File

@ -117,28 +117,6 @@ export default function TimelinePopup({
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* Connection line */}
<div
className="absolute top-0 left-1/2 transform -translate-x-1/2 -translate-y-full"
style={{
width: '2px',
height: '20px',
backgroundColor: 'red',
boxShadow: '0 0 4px rgba(0, 0, 0, 0.3)'
}}
/>
{/* Connection dot */}
<div
className="absolute top-0 left-1/2 transform -translate-x-1/2 -translate-y-full"
style={{
width: '6px',
height: '6px',
backgroundColor: 'red',
borderRadius: '50%',
marginBottom: '20px',
boxShadow: '0 0 4px rgba(0, 0, 0, 0.3)'
}}
/>
</div> </div>
</Popup> </Popup>
) )