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 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 */}
<EWSAlertLayer
map={map}
incidents={incidents}
onIncidentResolved={onIncidentResolved}
visible={ewsVisible}
/>
</>
);
}
// return (
// <>
// {/* EWS Alert Layer for emergency notifications */}
// <EWSAlertLayer
// map={map}
// incidents={incidents}
// onIncidentResolved={onIncidentResolved}
// 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 { 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<boolean>(false)
const [useAllMonths, setUseAllMonths] = useState<boolean>(false)
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 { 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 && (
<>
<div className="absolute flex w-full p-2">
@ -304,6 +340,19 @@ export default function CrimeMap() {
/>
</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
crimes={filteredCrimes || []}
defaultCollapsed={sidebarCollapsed}
@ -312,18 +361,18 @@ export default function CrimeMap() {
selectedMonth={selectedMonth}
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">
<UnitsLegend
categories={categories}
@ -332,7 +381,7 @@ export default function CrimeMap() {
</div>
)}
{isFullscreen && showTimelineLayer && (
{showTimelineLayer && (
<div className="absolute flex bottom-20 right-0 z-10 p-2">
<TimelineLegend position="bottom-right" />
</div>
@ -340,17 +389,17 @@ export default function CrimeMap() {
</>
)}
{isFullscreen && (
<div className="absolute flex w-full bottom-0">
<CrimeTimelapse
startYear={2020}
endYear={2024}
autoPlay={false}
onChange={handleTimelineChange}
onPlayingChange={handleTimelinePlayingChange}
/>
</div>
)}
<div className="absolute flex w-full bottom-0">
<CrimeTimelapse
startYear={2020}
endYear={2024}
autoPlay={false}
onChange={handleTimelineChange}
onPlayingChange={handleTimelinePlayingChange}
/>
</div>
</MapView>
</div>
</div>

View File

@ -93,6 +93,7 @@ export default function Layers({
sourceType = "cbt",
}: LayersProps) {
const animationRef = useRef<number | null>(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({
<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">
<PanicButtonDemo
onTriggerAlert={handleTriggerAlert}
@ -544,7 +545,7 @@ export default function Layers({
activeIncidents={ewsIncidents.filter((inc) => inc.status === "active")}
/>
</div>
)}
)} */}
</>

View File

@ -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<IUnits[]>([])
const loadedUnitsRef = useRef<IUnits[]>([])
@ -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<Map<string, IDistrictIncidents[]>>(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<string, IDistrictIncidents[]>();
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

View File

@ -117,28 +117,6 @@ export default function TimelinePopup({
</div>
</CardContent>
</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>
</Popup>
)