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

261 lines
10 KiB
TypeScript

"use client"
import { useEffect, useCallback, useRef, useState } from "react"
import type { ICrimes } from "@/app/_utils/types/crimes"
interface HistoricalIncidentsLayerProps {
visible?: boolean
map: any
crimes?: ICrimes[]
filterCategory?: string | "all"
focusedDistrictId?: string | null
}
export default function HistoricalIncidentsLayer({
visible = false,
map,
crimes = [],
filterCategory = "all",
focusedDistrictId,
}: HistoricalIncidentsLayerProps) {
const isInteractingWithMarker = useRef(false);
const currentYear = new Date().getFullYear();
const startYear = 2020;
const [yearColors, setYearColors] = useState<Record<number, string>>({});
// Generate colors for each year from 2020 to current year
useEffect(() => {
const colors: Record<number, string> = {};
const yearCount = currentYear - startYear + 1;
for (let i = 0; i < yearCount; i++) {
const year = startYear + i;
// Generate a color gradient from red (2020) to blue (current year)
const red = Math.floor(255 - (i * 255 / yearCount));
const blue = Math.floor(i * 255 / yearCount);
colors[year] = `rgb(${red}, 70, ${blue})`;
}
setYearColors(colors);
}, [currentYear]);
const handleIncidentClick = useCallback(
(e: any) => {
if (!map) return;
const features = map.queryRenderedFeatures(e.point, { layers: ["historical-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,
district: incident.properties.district,
category: incident.properties.category,
type: incident.properties.incidentType,
description: incident.properties.description,
status: incident.properties?.status || "Unknown",
longitude: (incident.geometry as any).coordinates[0],
latitude: (incident.geometry as any).coordinates[1],
timestamp: new Date(incident.properties.timestamp || Date.now()),
year: incident.properties.year,
};
// console.log("Historical incident clicked:", incidentDetails);
// Ensure markers stay visible when clicking on them
if (map.getLayer("historical-incidents")) {
map.setLayoutProperty("historical-incidents", "visibility", "visible");
}
// First fly to the incident location
map.flyTo({
center: [incidentDetails.longitude, incidentDetails.latitude],
zoom: 15,
bearing: 0,
pitch: 45,
duration: 2000,
});
// Then 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 to allow the event to process
setTimeout(() => {
isInteractingWithMarker.current = false;
}, 5000);
},
[map]
);
useEffect(() => {
if (!map || !visible) return;
// console.log("Setting up historical incidents layer");
// Filter incidents from 2020 to current year
const historicalData = {
type: "FeatureCollection" as const,
features: crimes.flatMap((crime) =>
crime.crime_incidents
.filter(
(incident) => {
const incidentYear = incident.timestamp ? new Date(incident.timestamp).getFullYear() : null;
return (
(filterCategory === "all" || incident.crime_categories.name === filterCategory) &&
incident.locations &&
typeof incident.locations.longitude === "number" &&
typeof incident.locations.latitude === "number" &&
incidentYear !== null &&
incidentYear >= startYear &&
incidentYear <= currentYear
);
}
)
.map((incident) => {
const incidentYear = incident.timestamp ? new Date(incident.timestamp).getFullYear() : currentYear;
return {
type: "Feature" as const,
geometry: {
type: "Point" as const,
coordinates: [incident.locations.longitude, incident.locations.latitude],
},
properties: {
id: incident.id,
district: crime.districts.name,
district_id: crime.district_id,
category: incident.crime_categories.name,
incidentType: incident.crime_categories.type || "",
description: incident.description,
status: incident.status || "",
timestamp: incident.timestamp ? incident.timestamp.toString() : "",
year: incidentYear,
},
};
})
),
};
// console.log(`Found ${historicalData.features.length} historical incidents from 2020 to ${currentYear}`);
const setupLayerAndSource = () => {
try {
// Check if source exists and update it
if (map.getSource("historical-incidents-source")) {
(map.getSource("historical-incidents-source") as any).setData(historicalData);
} else {
// If not, add source
map.addSource("historical-incidents-source", {
type: "geojson",
data: historicalData,
// No clustering configuration
});
}
// 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;
}
}
// Style for year-based coloring
const circleColorExpression: any[] = [
"match",
["get", "year"],
];
// Add entries for each year and its color
Object.entries(yearColors).forEach(([year, color]) => {
circleColorExpression.push(parseInt(year), color);
});
// Default color for unknown years
circleColorExpression.push("#888888");
// Check if layer exists already
if (!map.getLayer("historical-incidents")) {
map.addLayer({
id: "historical-incidents",
type: "circle",
source: "historical-incidents-source",
paint: {
"circle-color": circleColorExpression,
"circle-radius": [
"interpolate",
["linear"],
["zoom"],
7, 2, // Smaller circles at lower zoom levels
12, 4,
15, 6, // Smaller maximum size
],
"circle-stroke-width": 1,
"circle-stroke-color": "#ffffff",
"circle-opacity": 0.8,
},
layout: {
visibility: visible ? "visible" : "none",
}
}, firstSymbolId);
// Add mouse events
map.on("mouseenter", "historical-incidents", () => {
map.getCanvas().style.cursor = "pointer";
});
map.on("mouseleave", "historical-incidents", () => {
map.getCanvas().style.cursor = "";
});
} else {
// Update existing layer visibility
map.setLayoutProperty("historical-incidents", "visibility", visible ? "visible" : "none");
}
// Ensure click handler is properly registered
map.off("click", "historical-incidents", handleIncidentClick);
map.on("click", "historical-incidents", handleIncidentClick);
} catch (error) {
console.error("Error setting up historical 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();
}
}, 1000);
}
return () => {
if (map) {
map.off("click", "historical-incidents", handleIncidentClick);
}
};
}, [map, visible, crimes, filterCategory, handleIncidentClick, currentYear, yearColors]);
return null;
}