MIF_E31221222/sigap-website/app/_components/map/layers/recent-incidents-layer.tsx

238 lines
8.0 KiB
TypeScript

"use client"
import { useEffect, useCallback, useRef } from "react"
import type { IIncidentLogs } from "@/app/_utils/types/crimes"
interface RecentIncidentsLayerProps {
visible?: boolean
map: any
incidents?: IIncidentLogs[]
}
export default function RecentIncidentsLayer({ visible = false, map, incidents = [] }: RecentIncidentsLayerProps) {
const isInteractingWithMarker = useRef(false)
// Filter incidents from the last 24 hours
const recentIncidents = incidents.filter((incident) => {
if (!incident.timestamp) return false
const incidentDate = new Date(incident.timestamp)
const now = new Date()
const timeDiff = now.getTime() - incidentDate.getTime()
// 86400000 = 24 hours in milliseconds
return timeDiff <= 86400000
})
const handleIncidentClick = useCallback(
(e: any) => {
if (!map) return
const features = map.queryRenderedFeatures(e.point, { layers: ["recent-incidents"] })
if (!features || features.length === 0) return
// Stop event propagation
e.originalEvent.stopPropagation()
e.preventDefault()
isInteractingWithMarker.current = true
const incident = features[0]
if (!incident.properties) return
e.originalEvent.stopPropagation()
e.preventDefault()
const incidentDetails = {
id: incident.properties.id,
description: incident.properties.description,
status: incident.properties?.status || "Active",
longitude: (incident.geometry as any).coordinates[0],
latitude: (incident.geometry as any).coordinates[1],
timestamp: new Date(incident.properties.timestamp || Date.now()),
category: incident.properties.category,
}
// console.log("Recent incident clicked:", incidentDetails)
// Ensure markers stay visible
if (map.getLayer("recent-incidents")) {
map.setLayoutProperty("recent-incidents", "visibility", "visible")
}
// First fly to the incident location
map.flyTo({
center: [incidentDetails.longitude, incidentDetails.latitude],
zoom: 15,
bearing: 0,
pitch: 45,
duration: 2000,
})
// Dispatch the incident_click event to show the popup
const customEvent = new CustomEvent("incident_click", {
detail: incidentDetails,
bubbles: true,
})
map.getCanvas().dispatchEvent(customEvent)
document.dispatchEvent(customEvent)
// Reset the flag after a delay
setTimeout(() => {
isInteractingWithMarker.current = false
}, 5000)
},
[map],
)
useEffect(() => {
if (!map || !visible) return
// console.log(`Setting up recent incidents layer with ${recentIncidents.length} incidents from the last 24 hours`)
// Convert incidents to GeoJSON
const recentData = {
type: "FeatureCollection" as const,
features: recentIncidents.map((incident) => ({
type: "Feature" as const,
geometry: {
type: "Point" as const,
coordinates: [incident.longitude, incident.latitude],
},
properties: {
id: incident.id,
user_id: incident.user_id,
address: incident.address,
description: incident.description,
timestamp: incident.timestamp ? incident.timestamp.toString() : new Date().toString(),
category: incident.category,
district: incident.district,
severity: incident.severity,
status: incident.verified,
source: incident.source,
},
})),
}
const setupLayerAndSource = () => {
try {
// Check if source exists and update it
if (map.getSource("recent-incidents-source")) {
; (map.getSource("recent-incidents-source") as any).setData(recentData)
} else {
// If not, add source
map.addSource("recent-incidents-source", {
type: "geojson",
data: recentData,
})
}
// Find first symbol layer for proper layering
const layers = map.getStyle().layers
let firstSymbolId: string | undefined
for (const layer of layers) {
if (layer.type === "symbol") {
firstSymbolId = layer.id
break
}
}
// Check if layer exists already
if (!map.getLayer("recent-incidents")) {
map.addLayer(
{
id: "recent-incidents",
type: "circle",
source: "recent-incidents-source",
paint: {
"circle-color": "#FF5252", // Red color for recent incidents
"circle-radius": [
"interpolate",
["linear"],
["zoom"],
7,
4, // Slightly larger at lower zooms for visibility
12,
8,
15,
12, // Larger maximum size
],
"circle-stroke-width": 2,
"circle-stroke-color": "#FFFFFF",
"circle-opacity": 0.8,
// Add a pulsing effect
"circle-stroke-opacity": ["interpolate", ["linear"], ["zoom"], 7, 0.5, 15, 0.8],
},
layout: {
visibility: visible ? "visible" : "none",
},
},
firstSymbolId,
)
// Add a glow effect with a larger circle behind
map.addLayer(
{
id: "recent-incidents-glow",
type: "circle",
source: "recent-incidents-source",
paint: {
"circle-color": "#FF5252",
"circle-radius": ["interpolate", ["linear"], ["zoom"], 7, 6, 12, 12, 15, 18],
"circle-opacity": 0.2,
"circle-blur": 1,
},
layout: {
visibility: visible ? "visible" : "none",
},
},
"recent-incidents",
)
// Add mouse events
map.on("mouseenter", "recent-incidents", () => {
map.getCanvas().style.cursor = "pointer"
})
map.on("mouseleave", "recent-incidents", () => {
map.getCanvas().style.cursor = ""
})
} else {
// Update existing layer visibility
map.setLayoutProperty("recent-incidents", "visibility", visible ? "visible" : "none")
map.setLayoutProperty("recent-incidents-glow", "visibility", visible ? "visible" : "none")
}
// Ensure click handler is properly registered
map.off("click", "recent-incidents", handleIncidentClick)
map.on("click", "recent-incidents", handleIncidentClick)
} catch (error) {
console.error("Error setting up recent incidents layer:", error)
}
}
// Check if style is loaded and set up layer accordingly
if (map.isStyleLoaded()) {
setupLayerAndSource()
} else {
map.once("style.load", setupLayerAndSource)
// Fallback
setTimeout(() => {
if (map.isStyleLoaded()) {
setupLayerAndSource()
} else {
console.warn("Map style still not loaded after timeout")
}
}, 1000)
}
return () => {
if (map) {
map.off("click", "recent-incidents", handleIncidentClick)
}
}
}, [map, visible, recentIncidents, handleIncidentClick])
return null
}