243 lines
9.0 KiB
TypeScript
243 lines
9.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;
|
|
|
|
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;
|
|
}
|