fix: fix bug district pop up always show while selected district is not focus
This commit is contained in:
parent
834d4b02cf
commit
9c6e005839
|
@ -27,6 +27,7 @@ export default function ClusterLayer({
|
||||||
(e: any) => {
|
(e: any) => {
|
||||||
if (!map) return
|
if (!map) return
|
||||||
|
|
||||||
|
// Stop event propagation to prevent district layer from handling this click
|
||||||
e.originalEvent.stopPropagation()
|
e.originalEvent.stopPropagation()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
|
@ -219,6 +220,7 @@ export default function ClusterLayer({
|
||||||
const handleCrimePointClick = (e: any) => {
|
const handleCrimePointClick = (e: any) => {
|
||||||
if (!map) return
|
if (!map) return
|
||||||
|
|
||||||
|
// Stop event propagation
|
||||||
e.originalEvent.stopPropagation()
|
e.originalEvent.stopPropagation()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
|
@ -423,6 +425,7 @@ export default function ClusterLayer({
|
||||||
const handleCrimePointClick = (e: any) => {
|
const handleCrimePointClick = (e: any) => {
|
||||||
if (!map) return
|
if (!map) return
|
||||||
|
|
||||||
|
// Stop event propagation
|
||||||
e.originalEvent.stopPropagation()
|
e.originalEvent.stopPropagation()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
|
@ -504,19 +507,19 @@ export default function ClusterLayer({
|
||||||
map.off("click", "clusters", handleClusterClick)
|
map.off("click", "clusters", handleClusterClick)
|
||||||
if (sourceType === "cbu" && map.getLayer("crime-points")) {
|
if (sourceType === "cbu" && map.getLayer("crime-points")) {
|
||||||
// Define properly typed event handlers
|
// Define properly typed event handlers
|
||||||
const crimePointsMouseEnter = function () {
|
const crimePointsMouseEnter = () => {
|
||||||
if (map && map.getCanvas()) {
|
if (map && map.getCanvas()) {
|
||||||
map.getCanvas().style.cursor = "pointer";
|
map.getCanvas().style.cursor = "pointer";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const crimePointsMouseLeave = function () {
|
const crimePointsMouseLeave = () => {
|
||||||
if (map && map.getCanvas()) {
|
if (map && map.getCanvas()) {
|
||||||
map.getCanvas().style.cursor = "";
|
map.getCanvas().style.cursor = "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const crimePointsClick = function (e: mapboxgl.MapMouseEvent) {
|
const crimePointsClick = (e: mapboxgl.MapMouseEvent) => {
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
|
|
||||||
e.originalEvent.stopPropagation();
|
e.originalEvent.stopPropagation();
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { getCrimeRateColor } from "@/app/_utils/map"
|
import { getCrimeRateColor } from "@/app/_utils/map"
|
||||||
import { IExtrusionLayerProps } from "@/app/_utils/types/map"
|
import type { IExtrusionLayerProps } from "@/app/_utils/types/map"
|
||||||
import { useEffect, useRef } from "react"
|
import { useEffect, useRef } from "react"
|
||||||
|
|
||||||
export default function DistrictExtrusionLayer({
|
export default function DistrictExtrusionLayer({
|
||||||
|
@ -21,221 +21,227 @@ export default function DistrictExtrusionLayer({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map || !visible) return
|
if (!map || !visible) return
|
||||||
|
|
||||||
const onStyleLoad = () => {
|
console.log("DistrictExtrusionLayer effect running, focusedDistrictId:", focusedDistrictId)
|
||||||
if (!map) return
|
|
||||||
|
|
||||||
try {
|
const onStyleLoad = () => {
|
||||||
const layers = map.getStyle().layers
|
if (!map) return
|
||||||
let firstSymbolId: string | undefined
|
|
||||||
for (const layer of layers) {
|
try {
|
||||||
if (layer.type === "symbol") {
|
const layers = map.getStyle().layers
|
||||||
firstSymbolId = layer.id
|
let firstSymbolId: string | undefined
|
||||||
break
|
for (const layer of layers) {
|
||||||
}
|
if (layer.type === "symbol") {
|
||||||
|
firstSymbolId = layer.id
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove existing layer if it exists to avoid conflicts
|
|
||||||
if (map.getLayer("district-extrusion")) {
|
|
||||||
map.removeLayer("district-extrusion")
|
|
||||||
extrusionCreatedRef.current = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the districts source exists
|
|
||||||
if (!map.getSource("districts")) {
|
|
||||||
if (!tilesetId) {
|
|
||||||
console.error("No tileset ID provided for districts source");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
map.addSource("districts", {
|
|
||||||
type: "vector",
|
|
||||||
url: `mapbox://${tilesetId}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the extrusion layer
|
|
||||||
map.addLayer(
|
|
||||||
{
|
|
||||||
id: "district-extrusion",
|
|
||||||
type: "fill-extrusion",
|
|
||||||
source: "districts",
|
|
||||||
"source-layer": "Districts",
|
|
||||||
paint: {
|
|
||||||
"fill-extrusion-color": [
|
|
||||||
"case",
|
|
||||||
["has", "kode_kec"],
|
|
||||||
[
|
|
||||||
"match",
|
|
||||||
["get", "kode_kec"],
|
|
||||||
focusedDistrictId || "",
|
|
||||||
getCrimeRateColor(crimeDataByDistrict[focusedDistrictId || ""]?.level),
|
|
||||||
"transparent",
|
|
||||||
],
|
|
||||||
"transparent",
|
|
||||||
],
|
|
||||||
"fill-extrusion-height": [
|
|
||||||
"case",
|
|
||||||
["has", "kode_kec"],
|
|
||||||
["match", ["get", "kode_kec"], focusedDistrictId || "", 0, 0], // Start at 0 for animation
|
|
||||||
0,
|
|
||||||
],
|
|
||||||
"fill-extrusion-base": 0,
|
|
||||||
"fill-extrusion-opacity": 0.8,
|
|
||||||
},
|
|
||||||
filter: ["==", ["get", "kode_kec"], focusedDistrictId || ""],
|
|
||||||
},
|
|
||||||
firstSymbolId
|
|
||||||
)
|
|
||||||
|
|
||||||
extrusionCreatedRef.current = true
|
|
||||||
|
|
||||||
// If a district is focused, start the animation
|
|
||||||
if (focusedDistrictId) {
|
|
||||||
lastFocusedDistrictRef.current = focusedDistrictId
|
|
||||||
animateExtrusion()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error adding district extrusion layer:", error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove existing layer if it exists to avoid conflicts
|
||||||
|
if (map.getLayer("district-extrusion")) {
|
||||||
|
map.removeLayer("district-extrusion")
|
||||||
|
extrusionCreatedRef.current = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the districts source exists
|
||||||
|
if (!map.getSource("districts")) {
|
||||||
|
if (!tilesetId) {
|
||||||
|
console.error("No tileset ID provided for districts source")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
map.addSource("districts", {
|
||||||
|
type: "vector",
|
||||||
|
url: `mapbox://${tilesetId}`,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (map.isStyleLoaded()) {
|
// Create the extrusion layer
|
||||||
onStyleLoad()
|
map.addLayer(
|
||||||
} else {
|
{
|
||||||
map.once("style.load", onStyleLoad)
|
id: "district-extrusion",
|
||||||
}
|
type: "fill-extrusion",
|
||||||
|
source: "districts",
|
||||||
|
"source-layer": "Districts",
|
||||||
|
paint: {
|
||||||
|
"fill-extrusion-color": [
|
||||||
|
"case",
|
||||||
|
["has", "kode_kec"],
|
||||||
|
[
|
||||||
|
"match",
|
||||||
|
["get", "kode_kec"],
|
||||||
|
focusedDistrictId || "",
|
||||||
|
getCrimeRateColor(crimeDataByDistrict[focusedDistrictId || ""]?.level),
|
||||||
|
"transparent",
|
||||||
|
],
|
||||||
|
"transparent",
|
||||||
|
],
|
||||||
|
"fill-extrusion-height": [
|
||||||
|
"case",
|
||||||
|
["has", "kode_kec"],
|
||||||
|
["match", ["get", "kode_kec"], focusedDistrictId || "", 0, 0], // Start at 0 for animation
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
"fill-extrusion-base": 0,
|
||||||
|
"fill-extrusion-opacity": 0.8,
|
||||||
|
},
|
||||||
|
filter: ["==", ["get", "kode_kec"], focusedDistrictId || ""],
|
||||||
|
},
|
||||||
|
firstSymbolId,
|
||||||
|
)
|
||||||
|
|
||||||
return () => {
|
extrusionCreatedRef.current = true
|
||||||
if (animationRef.current) {
|
console.log("District extrusion layer created")
|
||||||
cancelAnimationFrame(animationRef.current)
|
|
||||||
animationRef.current = null
|
// If a district is focused, start the animation
|
||||||
|
if (focusedDistrictId) {
|
||||||
|
console.log("Starting animation for district:", focusedDistrictId)
|
||||||
|
lastFocusedDistrictRef.current = focusedDistrictId
|
||||||
|
animateExtrusion()
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error adding district extrusion layer:", error)
|
||||||
}
|
}
|
||||||
}, [map, visible, tilesetId])
|
}
|
||||||
|
|
||||||
|
if (map.isStyleLoaded()) {
|
||||||
|
onStyleLoad()
|
||||||
|
} else {
|
||||||
|
map.once("style.load", onStyleLoad)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (animationRef.current) {
|
||||||
|
cancelAnimationFrame(animationRef.current)
|
||||||
|
animationRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [map, visible, tilesetId, focusedDistrictId, crimeDataByDistrict])
|
||||||
|
|
||||||
// Update filter and color when focused district changes
|
// Update filter and color when focused district changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map || !map.getLayer("district-extrusion")) return
|
if (!map || !map.getLayer("district-extrusion")) return
|
||||||
|
|
||||||
// Skip unnecessary updates if nothing has changed
|
console.log("Updating district extrusion for district:", focusedDistrictId)
|
||||||
if (lastFocusedDistrictRef.current === focusedDistrictId) return;
|
|
||||||
|
|
||||||
// If we're unfocusing a district
|
// Skip unnecessary updates if nothing has changed
|
||||||
if (!focusedDistrictId) {
|
if (lastFocusedDistrictRef.current === focusedDistrictId) return
|
||||||
// Stop rotation when unfocusing
|
|
||||||
if (rotationAnimationRef.current) {
|
|
||||||
cancelAnimationFrame(rotationAnimationRef.current)
|
|
||||||
rotationAnimationRef.current = null
|
|
||||||
}
|
|
||||||
bearingRef.current = 0
|
|
||||||
|
|
||||||
// Animate height down
|
// If we're unfocusing a district
|
||||||
const animateHeightDown = () => {
|
if (!focusedDistrictId) {
|
||||||
if (!map || !map.getLayer("district-extrusion")) return;
|
// Stop rotation when unfocusing
|
||||||
|
if (rotationAnimationRef.current) {
|
||||||
|
cancelAnimationFrame(rotationAnimationRef.current)
|
||||||
|
rotationAnimationRef.current = null
|
||||||
|
}
|
||||||
|
bearingRef.current = 0
|
||||||
|
|
||||||
let currentHeight = 800;
|
// Animate height down
|
||||||
const duration = 500;
|
const animateHeightDown = () => {
|
||||||
const startTime = performance.now();
|
if (!map || !map.getLayer("district-extrusion")) return
|
||||||
|
|
||||||
const animate = (time: number) => {
|
const currentHeight = 800
|
||||||
const elapsed = time - startTime;
|
const duration = 500
|
||||||
const progress = Math.min(elapsed / duration, 1);
|
const startTime = performance.now()
|
||||||
const easedProgress = progress * (2 - progress); // easeOutQuad
|
|
||||||
const height = 800 - (800 * easedProgress);
|
|
||||||
|
|
||||||
try {
|
const animate = (time: number) => {
|
||||||
map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
|
const elapsed = time - startTime
|
||||||
"case",
|
const progress = Math.min(elapsed / duration, 1)
|
||||||
["has", "kode_kec"],
|
const easedProgress = progress * (2 - progress) // easeOutQuad
|
||||||
["match", ["get", "kode_kec"], lastFocusedDistrictRef.current || "", height, 0],
|
const height = 800 - 800 * easedProgress
|
||||||
0,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (progress < 1) {
|
try {
|
||||||
animationRef.current = requestAnimationFrame(animate);
|
map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
|
||||||
} else {
|
"case",
|
||||||
// Reset when animation completes
|
["has", "kode_kec"],
|
||||||
map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
|
["match", ["get", "kode_kec"], lastFocusedDistrictRef.current || "", height, 0],
|
||||||
"case",
|
0,
|
||||||
["has", "kode_kec"],
|
])
|
||||||
["match", ["get", "kode_kec"], "", 0, 0],
|
|
||||||
0,
|
|
||||||
]);
|
|
||||||
map.setFilter("district-extrusion", ["==", ["get", "kode_kec"], ""]);
|
|
||||||
|
|
||||||
lastFocusedDistrictRef.current = null;
|
if (progress < 1) {
|
||||||
|
animationRef.current = requestAnimationFrame(animate)
|
||||||
|
} else {
|
||||||
|
// Reset when animation completes
|
||||||
|
map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
|
||||||
|
"case",
|
||||||
|
["has", "kode_kec"],
|
||||||
|
["match", ["get", "kode_kec"], "", 0, 0],
|
||||||
|
0,
|
||||||
|
])
|
||||||
|
map.setFilter("district-extrusion", ["==", ["get", "kode_kec"], ""])
|
||||||
|
|
||||||
// Ensure bearing is reset
|
lastFocusedDistrictRef.current = null
|
||||||
map.setBearing(0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error animating extrusion down:", error);
|
|
||||||
if (animationRef.current) {
|
|
||||||
cancelAnimationFrame(animationRef.current);
|
|
||||||
animationRef.current = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (animationRef.current) {
|
// Ensure bearing is reset
|
||||||
cancelAnimationFrame(animationRef.current);
|
map.setBearing(0)
|
||||||
}
|
}
|
||||||
animationRef.current = requestAnimationFrame(animate);
|
} catch (error) {
|
||||||
};
|
console.error("Error animating extrusion down:", error)
|
||||||
|
if (animationRef.current) {
|
||||||
animateHeightDown();
|
cancelAnimationFrame(animationRef.current)
|
||||||
return;
|
animationRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (animationRef.current) {
|
||||||
// Update filter for the new district
|
cancelAnimationFrame(animationRef.current)
|
||||||
map.setFilter("district-extrusion", ["==", ["get", "kode_kec"], focusedDistrictId]);
|
}
|
||||||
|
animationRef.current = requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
|
||||||
// Update the extrusion color
|
animateHeightDown()
|
||||||
map.setPaintProperty("district-extrusion", "fill-extrusion-color", [
|
return
|
||||||
"case",
|
}
|
||||||
["has", "kode_kec"],
|
|
||||||
[
|
try {
|
||||||
"match",
|
// Update filter for the new district
|
||||||
["get", "kode_kec"],
|
map.setFilter("district-extrusion", ["==", ["get", "kode_kec"], focusedDistrictId])
|
||||||
focusedDistrictId,
|
|
||||||
getCrimeRateColor(crimeDataByDistrict[focusedDistrictId]?.level),
|
// Update the extrusion color
|
||||||
"transparent",
|
map.setPaintProperty("district-extrusion", "fill-extrusion-color", [
|
||||||
],
|
"case",
|
||||||
|
["has", "kode_kec"],
|
||||||
|
[
|
||||||
|
"match",
|
||||||
|
["get", "kode_kec"],
|
||||||
|
focusedDistrictId,
|
||||||
|
getCrimeRateColor(crimeDataByDistrict[focusedDistrictId]?.level),
|
||||||
"transparent",
|
"transparent",
|
||||||
]);
|
],
|
||||||
|
"transparent",
|
||||||
|
])
|
||||||
|
|
||||||
// Reset height for animation
|
// Reset height for animation
|
||||||
map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
|
map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
|
||||||
"case",
|
"case",
|
||||||
["has", "kode_kec"],
|
["has", "kode_kec"],
|
||||||
["match", ["get", "kode_kec"], focusedDistrictId, 0, 0],
|
["match", ["get", "kode_kec"], focusedDistrictId, 0, 0],
|
||||||
0,
|
0,
|
||||||
]);
|
])
|
||||||
|
|
||||||
// Store current focused district
|
// Store current focused district
|
||||||
lastFocusedDistrictRef.current = focusedDistrictId;
|
lastFocusedDistrictRef.current = focusedDistrictId
|
||||||
|
|
||||||
// Stop any existing animations and restart
|
// Stop any existing animations and restart
|
||||||
if (rotationAnimationRef.current) {
|
if (rotationAnimationRef.current) {
|
||||||
cancelAnimationFrame(rotationAnimationRef.current);
|
cancelAnimationFrame(rotationAnimationRef.current)
|
||||||
rotationAnimationRef.current = null;
|
rotationAnimationRef.current = null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (animationRef.current) {
|
if (animationRef.current) {
|
||||||
cancelAnimationFrame(animationRef.current);
|
cancelAnimationFrame(animationRef.current)
|
||||||
animationRef.current = null;
|
animationRef.current = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start animation with small delay to ensure smooth transition
|
// Start animation with small delay to ensure smooth transition
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
animateExtrusion();
|
console.log("Starting animation after district update")
|
||||||
}, 100);
|
animateExtrusion()
|
||||||
|
}, 100)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating district extrusion:", error)
|
console.error("Error updating district extrusion:", error)
|
||||||
}
|
}
|
||||||
}, [map, focusedDistrictId, crimeDataByDistrict])
|
}, [map, focusedDistrictId, crimeDataByDistrict])
|
||||||
|
|
||||||
// Cleanup on unmount
|
// Cleanup on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -253,89 +259,95 @@ export default function DistrictExtrusionLayer({
|
||||||
|
|
||||||
// Animate extrusion height
|
// Animate extrusion height
|
||||||
const animateExtrusion = () => {
|
const animateExtrusion = () => {
|
||||||
if (!map || !map.getLayer("district-extrusion") || !focusedDistrictId) return
|
if (!map || !map.getLayer("district-extrusion") || !focusedDistrictId) {
|
||||||
|
console.log("Cannot animate extrusion: missing map, layer, or focusedDistrictId")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (animationRef.current) {
|
console.log("Animating extrusion for district:", focusedDistrictId)
|
||||||
cancelAnimationFrame(animationRef.current)
|
|
||||||
animationRef.current = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const startHeight = 0
|
if (animationRef.current) {
|
||||||
const targetHeight = 800
|
cancelAnimationFrame(animationRef.current)
|
||||||
const duration = 700
|
animationRef.current = null
|
||||||
const startTime = performance.now()
|
}
|
||||||
|
|
||||||
const animate = (currentTime: number) => {
|
const startHeight = 0
|
||||||
const elapsed = currentTime - startTime
|
const targetHeight = 800
|
||||||
const progress = Math.min(elapsed / duration, 1)
|
const duration = 700
|
||||||
const easedProgress = progress * (2 - progress) // easeOutQuad
|
const startTime = performance.now()
|
||||||
const currentHeight = startHeight + (targetHeight - startHeight) * easedProgress
|
|
||||||
|
|
||||||
try {
|
const animate = (currentTime: number) => {
|
||||||
map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
|
const elapsed = currentTime - startTime
|
||||||
"case",
|
const progress = Math.min(elapsed / duration, 1)
|
||||||
["has", "kode_kec"],
|
const easedProgress = progress * (2 - progress) // easeOutQuad
|
||||||
["match", ["get", "kode_kec"], focusedDistrictId, currentHeight, 0],
|
const currentHeight = startHeight + (targetHeight - startHeight) * easedProgress
|
||||||
0,
|
|
||||||
])
|
|
||||||
|
|
||||||
if (progress < 1) {
|
try {
|
||||||
animationRef.current = requestAnimationFrame(animate)
|
map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
|
||||||
} else {
|
"case",
|
||||||
// Start rotation after extrusion completes
|
["has", "kode_kec"],
|
||||||
startRotation()
|
["match", ["get", "kode_kec"], focusedDistrictId, currentHeight, 0],
|
||||||
}
|
0,
|
||||||
} catch (error) {
|
])
|
||||||
console.error("Error animating extrusion:", error)
|
|
||||||
if (animationRef.current) {
|
if (progress < 1) {
|
||||||
cancelAnimationFrame(animationRef.current)
|
animationRef.current = requestAnimationFrame(animate)
|
||||||
animationRef.current = null
|
} else {
|
||||||
}
|
console.log("Extrusion animation complete, starting rotation")
|
||||||
|
// Start rotation after extrusion completes
|
||||||
|
startRotation()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error animating extrusion:", error)
|
||||||
|
if (animationRef.current) {
|
||||||
|
cancelAnimationFrame(animationRef.current)
|
||||||
|
animationRef.current = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
animationRef.current = requestAnimationFrame(animate)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
animationRef.current = requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
|
||||||
// Start rotation animation
|
// Start rotation animation
|
||||||
const startRotation = () => {
|
const startRotation = () => {
|
||||||
if (!map || !focusedDistrictId) return
|
if (!map || !focusedDistrictId) return
|
||||||
|
|
||||||
const rotationSpeed = 0.05 // degrees per frame
|
const rotationSpeed = 0.05 // degrees per frame
|
||||||
bearingRef.current = 0 // Reset bearing at start
|
bearingRef.current = 0 // Reset bearing at start
|
||||||
|
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
if (!map || !focusedDistrictId || focusedDistrictId !== lastFocusedDistrictRef.current) {
|
if (!map || !focusedDistrictId || focusedDistrictId !== lastFocusedDistrictRef.current) {
|
||||||
if (rotationAnimationRef.current) {
|
if (rotationAnimationRef.current) {
|
||||||
cancelAnimationFrame(rotationAnimationRef.current)
|
cancelAnimationFrame(rotationAnimationRef.current)
|
||||||
rotationAnimationRef.current = null
|
rotationAnimationRef.current = null
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Update bearing with smooth increment
|
// Update bearing with smooth increment
|
||||||
bearingRef.current = (bearingRef.current + rotationSpeed) % 360
|
bearingRef.current = (bearingRef.current + rotationSpeed) % 360
|
||||||
map.setBearing(bearingRef.current)
|
map.setBearing(bearingRef.current)
|
||||||
|
|
||||||
// Continue the animation
|
// Continue the animation
|
||||||
rotationAnimationRef.current = requestAnimationFrame(animate)
|
rotationAnimationRef.current = requestAnimationFrame(animate)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error during rotation animation:", error)
|
console.error("Error during rotation animation:", error)
|
||||||
if (rotationAnimationRef.current) {
|
if (rotationAnimationRef.current) {
|
||||||
cancelAnimationFrame(rotationAnimationRef.current)
|
cancelAnimationFrame(rotationAnimationRef.current)
|
||||||
rotationAnimationRef.current = null
|
rotationAnimationRef.current = null
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the animation loop
|
|
||||||
if (rotationAnimationRef.current) {
|
|
||||||
cancelAnimationFrame(rotationAnimationRef.current)
|
|
||||||
rotationAnimationRef.current = null
|
|
||||||
}
|
|
||||||
rotationAnimationRef.current = requestAnimationFrame(animate)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start the animation loop
|
||||||
|
if (rotationAnimationRef.current) {
|
||||||
|
cancelAnimationFrame(rotationAnimationRef.current)
|
||||||
|
rotationAnimationRef.current = null
|
||||||
|
}
|
||||||
|
rotationAnimationRef.current = requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,9 @@
|
||||||
|
|
||||||
import { BASE_BEARING, BASE_PITCH, BASE_ZOOM } from "@/app/_utils/const/map"
|
import { BASE_BEARING, BASE_PITCH, BASE_ZOOM } from "@/app/_utils/const/map"
|
||||||
import { createFillColorExpression, processDistrictFeature } from "@/app/_utils/map"
|
import { createFillColorExpression, processDistrictFeature } from "@/app/_utils/map"
|
||||||
import { IDistrictLayerProps } from "@/app/_utils/types/map"
|
import type { IDistrictLayerProps } from "@/app/_utils/types/map"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
|
|
||||||
|
|
||||||
export default function DistrictFillLineLayer({
|
export default function DistrictFillLineLayer({
|
||||||
visible = true,
|
visible = true,
|
||||||
map,
|
map,
|
||||||
|
@ -21,110 +20,79 @@ export default function DistrictFillLineLayer({
|
||||||
crimeDataByDistrict,
|
crimeDataByDistrict,
|
||||||
showFill = true,
|
showFill = true,
|
||||||
activeControl,
|
activeControl,
|
||||||
}: IDistrictLayerProps & { onDistrictClick?: (district: any) => void }) { // Extend the type inline
|
}: IDistrictLayerProps & { onDistrictClick?: (district: any) => void }) {
|
||||||
|
// Extend the type inline
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map || !visible) return
|
if (!map || !visible) return
|
||||||
|
|
||||||
const handleDistrictClick = (e: any) => {
|
const handleDistrictClick = (e: any) => {
|
||||||
const incidentFeatures = map.queryRenderedFeatures(e.point, {
|
// First check if the click was on a marker or cluster
|
||||||
layers: ["unclustered-point", "clusters"],
|
const incidentFeatures = map.queryRenderedFeatures(e.point, {
|
||||||
})
|
layers: [
|
||||||
|
"unclustered-point",
|
||||||
|
"clusters",
|
||||||
|
"crime-points",
|
||||||
|
"units-points",
|
||||||
|
"incidents-points",
|
||||||
|
"timeline-markers",
|
||||||
|
"recent-incidents",
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
if (incidentFeatures && incidentFeatures.length > 0) {
|
if (incidentFeatures && incidentFeatures.length > 0) {
|
||||||
return
|
// Click was on a marker or cluster, so don't process it as a district click
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!map || !e.features || e.features.length === 0) return
|
if (!map || !e.features || e.features.length === 0) return
|
||||||
|
|
||||||
const feature = e.features[0]
|
const feature = e.features[0]
|
||||||
const districtId = feature.properties.kode_kec
|
const districtId = feature.properties.kode_kec
|
||||||
|
|
||||||
// If clicking the same district, deselect it
|
|
||||||
if (focusedDistrictId === districtId) {
|
|
||||||
// Add null check for setFocusedDistrictId
|
|
||||||
if (setFocusedDistrictId) {
|
|
||||||
setFocusedDistrictId(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset pitch and bearing with animation
|
|
||||||
map.easeTo({
|
|
||||||
zoom: BASE_ZOOM,
|
|
||||||
pitch: BASE_PITCH,
|
|
||||||
bearing: BASE_BEARING,
|
|
||||||
duration: 1500,
|
|
||||||
easing: (t) => t * (2 - t), // easeOutQuad
|
|
||||||
})
|
|
||||||
|
|
||||||
// Restore fill color for all districts when unfocusing
|
|
||||||
const fillColorExpression = createFillColorExpression(null, crimeDataByDistrict)
|
|
||||||
map.setPaintProperty("district-fill", "fill-color", fillColorExpression as any)
|
|
||||||
|
|
||||||
// Show all clusters again when unfocusing
|
|
||||||
if (map.getLayer("clusters")) {
|
|
||||||
map.setLayoutProperty("clusters", "visibility", "visible")
|
|
||||||
}
|
|
||||||
if (map.getLayer("unclustered-point")) {
|
|
||||||
map.setLayoutProperty("unclustered-point", "visibility", "visible")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
} else if (focusedDistrictId) {
|
|
||||||
// If we're already focusing on a district and clicking a different one,
|
|
||||||
// we need to reset the current one and move to the new one
|
|
||||||
if (setFocusedDistrictId) {
|
|
||||||
setFocusedDistrictId(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait a moment before selecting the new district to ensure clean transitions
|
|
||||||
setTimeout(() => {
|
|
||||||
const district = processDistrictFeature(feature, e, districtId, crimeDataByDistrict, crimes, year, month)
|
|
||||||
if (!district || !setFocusedDistrictId) return
|
|
||||||
|
|
||||||
setFocusedDistrictId(district.id)
|
|
||||||
|
|
||||||
// Fly to the new district
|
|
||||||
map.flyTo({
|
|
||||||
center: [district.longitude, district.latitude],
|
|
||||||
zoom: 12.5,
|
|
||||||
pitch: 75,
|
|
||||||
bearing: 0,
|
|
||||||
duration: 1500,
|
|
||||||
easing: (t) => t * (2 - t), // easeOutQuad
|
|
||||||
})
|
|
||||||
|
|
||||||
// Use onDistrictClick if available, otherwise fall back to onClick
|
|
||||||
if (onDistrictClick) {
|
|
||||||
onDistrictClick(district)
|
|
||||||
} else if (onClick) {
|
|
||||||
onClick(district)
|
|
||||||
}
|
|
||||||
}, 100)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const district = processDistrictFeature(feature, e, districtId, crimeDataByDistrict, crimes, year, month)
|
|
||||||
|
|
||||||
if (!district) return
|
|
||||||
|
|
||||||
// Set the fill color expression immediately to show the focus
|
|
||||||
const focusedFillColorExpression = createFillColorExpression(district.id, crimeDataByDistrict)
|
|
||||||
map.setPaintProperty("district-fill", "fill-color", focusedFillColorExpression as any)
|
|
||||||
|
|
||||||
|
// If clicking the same district, deselect it
|
||||||
|
if (focusedDistrictId === districtId) {
|
||||||
// Add null check for setFocusedDistrictId
|
// Add null check for setFocusedDistrictId
|
||||||
if (setFocusedDistrictId) {
|
if (setFocusedDistrictId) {
|
||||||
setFocusedDistrictId(district.id)
|
setFocusedDistrictId(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide clusters when focusing on a district
|
// Reset pitch and bearing with animation
|
||||||
if (map.getLayer("clusters")) {
|
map.easeTo({
|
||||||
map.setLayoutProperty("clusters", "visibility", "none")
|
zoom: BASE_ZOOM,
|
||||||
}
|
pitch: BASE_PITCH,
|
||||||
if (map.getLayer("unclustered-point")) {
|
bearing: BASE_BEARING,
|
||||||
map.setLayoutProperty("unclustered-point", "visibility", "none")
|
duration: 1500,
|
||||||
}
|
easing: (t) => t * (2 - t), // easeOutQuad
|
||||||
|
})
|
||||||
|
|
||||||
// Animate to a pitched view focused on the district
|
// Restore fill color for all districts when unfocusing
|
||||||
|
const fillColorExpression = createFillColorExpression(null, crimeDataByDistrict)
|
||||||
|
map.setPaintProperty("district-fill", "fill-color", fillColorExpression as any)
|
||||||
|
|
||||||
|
// Show all clusters again when unfocusing
|
||||||
|
if (map.getLayer("clusters")) {
|
||||||
|
map.setLayoutProperty("clusters", "visibility", "visible")
|
||||||
|
}
|
||||||
|
if (map.getLayer("unclustered-point")) {
|
||||||
|
map.setLayoutProperty("unclustered-point", "visibility", "visible")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
} else if (focusedDistrictId) {
|
||||||
|
// If we're already focusing on a district and clicking a different one,
|
||||||
|
// we need to reset the current one and move to the new one
|
||||||
|
if (setFocusedDistrictId) {
|
||||||
|
setFocusedDistrictId(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait a moment before selecting the new district to ensure clean transitions
|
||||||
|
setTimeout(() => {
|
||||||
|
const district = processDistrictFeature(feature, e, districtId, crimeDataByDistrict, crimes, year, month)
|
||||||
|
if (!district || !setFocusedDistrictId) return
|
||||||
|
|
||||||
|
setFocusedDistrictId(district.id)
|
||||||
|
|
||||||
|
// Fly to the new district
|
||||||
map.flyTo({
|
map.flyTo({
|
||||||
center: [district.longitude, district.latitude],
|
center: [district.longitude, district.latitude],
|
||||||
zoom: 12.5,
|
zoom: 12.5,
|
||||||
|
@ -140,151 +108,193 @@ export default function DistrictFillLineLayer({
|
||||||
} else if (onClick) {
|
} else if (onClick) {
|
||||||
onClick(district)
|
onClick(district)
|
||||||
}
|
}
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const district = processDistrictFeature(feature, e, districtId, crimeDataByDistrict, crimes, year, month)
|
||||||
|
|
||||||
|
if (!district) return
|
||||||
|
|
||||||
|
// Set the fill color expression immediately to show the focus
|
||||||
|
const focusedFillColorExpression = createFillColorExpression(district.id, crimeDataByDistrict)
|
||||||
|
map.setPaintProperty("district-fill", "fill-color", focusedFillColorExpression as any)
|
||||||
|
|
||||||
|
// Add null check for setFocusedDistrictId
|
||||||
|
if (setFocusedDistrictId) {
|
||||||
|
setFocusedDistrictId(district.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onStyleLoad = () => {
|
// Hide clusters when focusing on a district
|
||||||
if (!map) return
|
if (map.getLayer("clusters")) {
|
||||||
|
map.setLayoutProperty("clusters", "visibility", "none")
|
||||||
|
}
|
||||||
|
if (map.getLayer("unclustered-point")) {
|
||||||
|
map.setLayoutProperty("unclustered-point", "visibility", "none")
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
// Animate to a pitched view focused on the district
|
||||||
if (!map.getSource("districts")) {
|
map.flyTo({
|
||||||
const layers = map.getStyle().layers
|
center: [district.longitude, district.latitude],
|
||||||
let firstSymbolId: string | undefined
|
zoom: 12.5,
|
||||||
for (const layer of layers) {
|
pitch: 75,
|
||||||
if (layer.type === "symbol") {
|
bearing: 0,
|
||||||
firstSymbolId = layer.id
|
duration: 1500,
|
||||||
break
|
easing: (t) => t * (2 - t), // easeOutQuad
|
||||||
}
|
})
|
||||||
}
|
|
||||||
|
|
||||||
map.addSource("districts", {
|
// Use onDistrictClick if available, otherwise fall back to onClick
|
||||||
type: "vector",
|
if (onDistrictClick) {
|
||||||
url: `mapbox://${tilesetId}`,
|
onDistrictClick(district)
|
||||||
})
|
} else if (onClick) {
|
||||||
|
onClick(district)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict)
|
const onStyleLoad = () => {
|
||||||
|
if (!map) return
|
||||||
|
|
||||||
// Determine fill opacity based on active control
|
try {
|
||||||
const fillOpacity = getFillOpacity(activeControl, showFill);
|
if (!map.getSource("districts")) {
|
||||||
|
const layers = map.getStyle().layers
|
||||||
if (!map.getLayer("district-fill")) {
|
let firstSymbolId: string | undefined
|
||||||
map.addLayer(
|
for (const layer of layers) {
|
||||||
{
|
if (layer.type === "symbol") {
|
||||||
id: "district-fill",
|
firstSymbolId = layer.id
|
||||||
type: "fill",
|
break
|
||||||
source: "districts",
|
|
||||||
"source-layer": "Districts",
|
|
||||||
paint: {
|
|
||||||
"fill-color": fillColorExpression as any,
|
|
||||||
"fill-opacity": fillOpacity,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
firstSymbolId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!map.getLayer("district-line")) {
|
|
||||||
map.addLayer(
|
|
||||||
{
|
|
||||||
id: "district-line",
|
|
||||||
type: "line",
|
|
||||||
source: "districts",
|
|
||||||
"source-layer": "Districts",
|
|
||||||
paint: {
|
|
||||||
"line-color": "#ffffff",
|
|
||||||
"line-width": 1,
|
|
||||||
"line-opacity": 0.5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
firstSymbolId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
map.on("mouseenter", "district-fill", () => {
|
|
||||||
map.getCanvas().style.cursor = "pointer"
|
|
||||||
})
|
|
||||||
|
|
||||||
map.on("mouseleave", "district-fill", () => {
|
|
||||||
map.getCanvas().style.cursor = ""
|
|
||||||
})
|
|
||||||
|
|
||||||
map.off("click", "district-fill", handleDistrictClick)
|
|
||||||
map.on("click", "district-fill", handleDistrictClick)
|
|
||||||
} else {
|
|
||||||
if (map.getLayer("district-fill")) {
|
|
||||||
const fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict)
|
|
||||||
map.setPaintProperty("district-fill", "fill-color", fillColorExpression as any)
|
|
||||||
|
|
||||||
// Update fill opacity when active control changes
|
|
||||||
const fillOpacity = getFillOpacity(activeControl, showFill);
|
|
||||||
map.setPaintProperty("district-fill", "fill-opacity", fillOpacity);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error("Error adding district layers:", error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (map.isStyleLoaded()) {
|
map.addSource("districts", {
|
||||||
onStyleLoad()
|
type: "vector",
|
||||||
|
url: `mapbox://${tilesetId}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
const fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict)
|
||||||
|
|
||||||
|
// Determine fill opacity based on active control
|
||||||
|
const fillOpacity = getFillOpacity(activeControl, showFill)
|
||||||
|
|
||||||
|
if (!map.getLayer("district-fill")) {
|
||||||
|
map.addLayer(
|
||||||
|
{
|
||||||
|
id: "district-fill",
|
||||||
|
type: "fill",
|
||||||
|
source: "districts",
|
||||||
|
"source-layer": "Districts",
|
||||||
|
paint: {
|
||||||
|
"fill-color": fillColorExpression as any,
|
||||||
|
"fill-opacity": fillOpacity,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
firstSymbolId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!map.getLayer("district-line")) {
|
||||||
|
map.addLayer(
|
||||||
|
{
|
||||||
|
id: "district-line",
|
||||||
|
type: "line",
|
||||||
|
source: "districts",
|
||||||
|
"source-layer": "Districts",
|
||||||
|
paint: {
|
||||||
|
"line-color": "#ffffff",
|
||||||
|
"line-width": 1,
|
||||||
|
"line-opacity": 0.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
firstSymbolId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
map.on("mouseenter", "district-fill", () => {
|
||||||
|
map.getCanvas().style.cursor = "pointer"
|
||||||
|
})
|
||||||
|
|
||||||
|
map.on("mouseleave", "district-fill", () => {
|
||||||
|
map.getCanvas().style.cursor = ""
|
||||||
|
})
|
||||||
|
|
||||||
|
map.off("click", "district-fill", handleDistrictClick)
|
||||||
|
map.on("click", "district-fill", handleDistrictClick)
|
||||||
} else {
|
} else {
|
||||||
map.once("style.load", onStyleLoad)
|
if (map.getLayer("district-fill")) {
|
||||||
}
|
const fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict)
|
||||||
|
map.setPaintProperty("district-fill", "fill-color", fillColorExpression as any)
|
||||||
|
|
||||||
return () => {
|
// Update fill opacity when active control changes
|
||||||
if (map) {
|
const fillOpacity = getFillOpacity(activeControl, showFill)
|
||||||
map.off("click", "district-fill", handleDistrictClick)
|
map.setPaintProperty("district-fill", "fill-opacity", fillOpacity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error adding district layers:", error)
|
||||||
}
|
}
|
||||||
}, [
|
}
|
||||||
map,
|
|
||||||
visible,
|
if (map.isStyleLoaded()) {
|
||||||
tilesetId,
|
onStyleLoad()
|
||||||
crimes,
|
} else {
|
||||||
filterCategory,
|
map.once("style.load", onStyleLoad)
|
||||||
year,
|
}
|
||||||
month,
|
|
||||||
focusedDistrictId,
|
return () => {
|
||||||
crimeDataByDistrict,
|
if (map) {
|
||||||
onClick,
|
map.off("click", "district-fill", handleDistrictClick)
|
||||||
onDistrictClick, // Add to dependency array
|
}
|
||||||
setFocusedDistrictId,
|
}
|
||||||
showFill,
|
}, [
|
||||||
activeControl,
|
map,
|
||||||
])
|
visible,
|
||||||
|
tilesetId,
|
||||||
|
crimes,
|
||||||
|
filterCategory,
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
focusedDistrictId,
|
||||||
|
crimeDataByDistrict,
|
||||||
|
onClick,
|
||||||
|
onDistrictClick, // Add to dependency array
|
||||||
|
setFocusedDistrictId,
|
||||||
|
showFill,
|
||||||
|
activeControl,
|
||||||
|
])
|
||||||
|
|
||||||
// Add an effect to update the fill color and opacity whenever relevant props change
|
// Add an effect to update the fill color and opacity whenever relevant props change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map || !map.getLayer("district-fill")) return;
|
if (!map || !map.getLayer("district-fill")) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict)
|
const fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict)
|
||||||
map.setPaintProperty("district-fill", "fill-color", fillColorExpression as any)
|
map.setPaintProperty("district-fill", "fill-color", fillColorExpression as any)
|
||||||
|
|
||||||
// Update fill opacity when active control changes
|
// Update fill opacity when active control changes
|
||||||
const fillOpacity = getFillOpacity(activeControl, showFill);
|
const fillOpacity = getFillOpacity(activeControl, showFill)
|
||||||
map.setPaintProperty("district-fill", "fill-opacity", fillOpacity);
|
map.setPaintProperty("district-fill", "fill-opacity", fillOpacity)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating district fill colors or opacity:", error)
|
console.error("Error updating district fill colors or opacity:", error)
|
||||||
}
|
}
|
||||||
}, [map, focusedDistrictId, crimeDataByDistrict, activeControl, showFill])
|
}, [map, focusedDistrictId, crimeDataByDistrict, activeControl, showFill])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to determine fill opacity based on active control
|
// Helper function to determine fill opacity based on active control
|
||||||
function getFillOpacity(activeControl?: string, showFill?: boolean): number {
|
function getFillOpacity(activeControl?: string, showFill?: boolean): number {
|
||||||
if (!showFill) return 0;
|
if (!showFill) return 0
|
||||||
|
|
||||||
// Full opacity for incidents and clusters
|
// Full opacity for incidents and clusters
|
||||||
if (activeControl === "incidents" || activeControl === "clusters") {
|
if (activeControl === "incidents" || activeControl === "clusters") {
|
||||||
return 0.6;
|
return 0.6
|
||||||
}
|
}
|
||||||
|
|
||||||
// Low opacity for timeline to show markers but still see district boundaries
|
// Low opacity for timeline to show markers but still see district boundaries
|
||||||
if (activeControl === "timeline") {
|
if (activeControl === "timeline") {
|
||||||
return 0.1;
|
return 0.1
|
||||||
}
|
}
|
||||||
|
|
||||||
// No fill for other controls, but keep boundaries visible
|
// No fill for other controls, but keep boundaries visible
|
||||||
return 0;
|
return 0
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useRef, useEffect, useCallback, act } from "react"
|
import { useState, useRef, useEffect, useCallback } from "react"
|
||||||
import { useMap } from "react-map-gl/mapbox"
|
import { useMap } from "react-map-gl/mapbox"
|
||||||
import { BASE_BEARING, BASE_PITCH, BASE_ZOOM, MAPBOX_TILESET_ID } from "@/app/_utils/const/map"
|
import { BASE_BEARING, BASE_PITCH, BASE_ZOOM, MAPBOX_TILESET_ID } from "@/app/_utils/const/map"
|
||||||
import DistrictPopup from "../pop-up/district-popup"
|
import DistrictPopup from "../pop-up/district-popup"
|
||||||
|
@ -20,17 +20,13 @@ import type { IUnits } from "@/app/_utils/types/units"
|
||||||
import UnitsLayer from "./units-layer"
|
import UnitsLayer from "./units-layer"
|
||||||
import DistrictFillLineLayer from "./district-layer"
|
import DistrictFillLineLayer from "./district-layer"
|
||||||
import CrimePopup from "../pop-up/crime-popup"
|
import CrimePopup from "../pop-up/crime-popup"
|
||||||
import TimeZonesDisplay from "./timezone"
|
|
||||||
import TimezoneLayer from "./timezone"
|
import TimezoneLayer from "./timezone"
|
||||||
import FaultLinesLayer from "./fault-lines"
|
import FaultLinesLayer from "./fault-lines"
|
||||||
import CoastlineLayer from "./coastline"
|
|
||||||
import EWSAlertLayer from "./ews-alert-layer"
|
import EWSAlertLayer from "./ews-alert-layer"
|
||||||
import PanicButtonDemo from "../controls/panic-button-demo"
|
import PanicButtonDemo from "../controls/panic-button-demo"
|
||||||
|
|
||||||
import { IIncidentLog } from "@/app/_utils/types/ews"
|
import type { IIncidentLog } from "@/app/_utils/types/ews"
|
||||||
import { addMockIncident, getAllIncidents, resolveIncident } from "@/app/_utils/mock/ews-data"
|
import { addMockIncident, getAllIncidents, resolveIncident } from "@/app/_utils/mock/ews-data"
|
||||||
|
|
||||||
import HistoricalIncidentsLayer from "./historical-incidents-layer"
|
|
||||||
import RecentIncidentsLayer from "./recent-incidents-layer"
|
import RecentIncidentsLayer from "./recent-incidents-layer"
|
||||||
|
|
||||||
// Interface for crime incident
|
// Interface for crime incident
|
||||||
|
@ -108,40 +104,44 @@ export default function Layers({
|
||||||
const [selectedIncident, setSelectedIncident] = useState<ICrimeIncident | null>(null)
|
const [selectedIncident, setSelectedIncident] = useState<ICrimeIncident | null>(null)
|
||||||
const [focusedDistrictId, setFocusedDistrictId] = useState<string | null>(null)
|
const [focusedDistrictId, setFocusedDistrictId] = useState<string | null>(null)
|
||||||
const selectedDistrictRef = useRef<IDistrictFeature | null>(null)
|
const selectedDistrictRef = useRef<IDistrictFeature | null>(null)
|
||||||
|
// Track if we're currently interacting with a marker to prevent district selection
|
||||||
|
const isInteractingWithMarker = useRef<boolean>(false)
|
||||||
|
|
||||||
const crimeDataByDistrict = processCrimeDataByDistrict(crimes)
|
const crimeDataByDistrict = processCrimeDataByDistrict(crimes)
|
||||||
|
|
||||||
const [ewsIncidents, setEwsIncidents] = useState<IIncidentLog[]>([]);
|
const [ewsIncidents, setEwsIncidents] = useState<IIncidentLog[]>([])
|
||||||
const [showPanicDemo, setShowPanicDemo] = useState(true);
|
const [showPanicDemo, setShowPanicDemo] = useState(true)
|
||||||
|
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
|
||||||
setSelectedDistrict(null)
|
setSelectedDistrict(null)
|
||||||
setSelectedIncident(null)
|
setSelectedIncident(null)
|
||||||
setFocusedDistrictId(null)
|
setFocusedDistrictId(null)
|
||||||
|
isInteractingWithMarker.current = false
|
||||||
|
|
||||||
if (map) {
|
if (map) {
|
||||||
map.easeTo({
|
map.easeTo({
|
||||||
|
@ -152,12 +152,12 @@ export default function Layers({
|
||||||
easing: (t) => t * (2 - t),
|
easing: (t) => t * (2 - t),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (map.getLayer("clusters")) {
|
if (map.getLayer("clusters")) {
|
||||||
map.getMap().setLayoutProperty("clusters", "visibility", "visible")
|
map.getMap().setLayoutProperty("clusters", "visibility", "visible")
|
||||||
}
|
}
|
||||||
if (map.getLayer("unclustered-point")) {
|
if (map.getLayer("unclustered-point")) {
|
||||||
map.getMap().setLayoutProperty("unclustered-point", "visibility", "visible")
|
map.getMap().setLayoutProperty("unclustered-point", "visibility", "visible")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (map.getLayer("district-fill")) {
|
if (map.getLayer("district-fill")) {
|
||||||
const fillColorExpression = createFillColorExpression(null, crimeDataByDistrict)
|
const fillColorExpression = createFillColorExpression(null, crimeDataByDistrict)
|
||||||
|
@ -180,8 +180,16 @@ export default function Layers({
|
||||||
(feature: IDistrictFeature) => {
|
(feature: IDistrictFeature) => {
|
||||||
console.log("District clicked:", feature)
|
console.log("District clicked:", feature)
|
||||||
|
|
||||||
|
// If we're currently interacting with a marker, don't process district click
|
||||||
|
if (isInteractingWithMarker.current) {
|
||||||
|
console.log("Ignoring district click because we're interacting with a marker")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any existing incident selection
|
||||||
setSelectedIncident(null)
|
setSelectedIncident(null)
|
||||||
|
|
||||||
|
// Set the district as selected
|
||||||
setSelectedDistrict(feature)
|
setSelectedDistrict(feature)
|
||||||
selectedDistrictRef.current = feature
|
selectedDistrictRef.current = feature
|
||||||
setFocusedDistrictId(feature.id)
|
setFocusedDistrictId(feature.id)
|
||||||
|
@ -196,6 +204,7 @@ export default function Layers({
|
||||||
easing: (t) => t * (2 - t),
|
easing: (t) => t * (2 - t),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Hide clusters when focusing on a district
|
||||||
if (map.getLayer("clusters")) {
|
if (map.getLayer("clusters")) {
|
||||||
map.getMap().setLayoutProperty("clusters", "visibility", "none")
|
map.getMap().setLayoutProperty("clusters", "visibility", "none")
|
||||||
}
|
}
|
||||||
|
@ -214,16 +223,16 @@ export default function Layers({
|
||||||
const customEvent = e as CustomEvent
|
const customEvent = e as CustomEvent
|
||||||
if (!map || !customEvent.detail) return
|
if (!map || !customEvent.detail) return
|
||||||
|
|
||||||
const { longitude, latitude, zoom, bearing, pitch, duration } = customEvent.detail
|
const { longitude, latitude, zoom, bearing, pitch, duration } = customEvent.detail
|
||||||
|
|
||||||
map.flyTo({
|
map.flyTo({
|
||||||
center: [longitude, latitude],
|
center: [longitude, latitude],
|
||||||
zoom: zoom || 15,
|
zoom: zoom || 15,
|
||||||
bearing: bearing || 0,
|
bearing: bearing || 0,
|
||||||
pitch: pitch || 45,
|
pitch: pitch || 45,
|
||||||
duration: duration || 2000,
|
duration: duration || 2000,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
mapboxMap.getCanvas().addEventListener("mapbox_fly_to", handleFlyToEvent as EventListener)
|
mapboxMap.getCanvas().addEventListener("mapbox_fly_to", handleFlyToEvent as EventListener)
|
||||||
|
|
||||||
|
@ -241,82 +250,93 @@ export default function Layers({
|
||||||
const customEvent = e as CustomEvent
|
const customEvent = e as CustomEvent
|
||||||
console.log("Received incident_click event in layers:", customEvent.detail)
|
console.log("Received incident_click event in layers:", customEvent.detail)
|
||||||
|
|
||||||
if (!customEvent.detail) {
|
if (!customEvent.detail) {
|
||||||
console.error("Empty incident click event data")
|
console.error("Empty incident click event data")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const incidentId = customEvent.detail.id || customEvent.detail.incidentId || customEvent.detail.incident_id
|
// Set the marker interaction flag to prevent district selection
|
||||||
|
isInteractingWithMarker.current = true
|
||||||
|
|
||||||
if (!incidentId) {
|
const incidentId = customEvent.detail.id || customEvent.detail.incidentId || customEvent.detail.incident_id
|
||||||
console.error("No incident ID found in event data:", customEvent.detail)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Looking for incident with ID:", incidentId)
|
if (!incidentId) {
|
||||||
|
console.error("No incident ID found in event data:", customEvent.detail)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let foundIncident: ICrimeIncident | undefined
|
console.log("Looking for incident with ID:", incidentId)
|
||||||
|
|
||||||
if (
|
let foundIncident: ICrimeIncident | undefined
|
||||||
customEvent.detail.latitude !== undefined &&
|
|
||||||
customEvent.detail.longitude !== undefined &&
|
|
||||||
customEvent.detail.category !== undefined
|
|
||||||
) {
|
|
||||||
foundIncident = {
|
|
||||||
id: incidentId,
|
|
||||||
district: customEvent.detail.district,
|
|
||||||
category: customEvent.detail.category,
|
|
||||||
type_category: customEvent.detail.type,
|
|
||||||
description: customEvent.detail.description,
|
|
||||||
status: customEvent.detail.status || "Unknown",
|
|
||||||
timestamp: customEvent.detail.timestamp ? new Date(customEvent.detail.timestamp) : undefined,
|
|
||||||
latitude: customEvent.detail.latitude,
|
|
||||||
longitude: customEvent.detail.longitude,
|
|
||||||
address: customEvent.detail.address,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (const crime of crimes) {
|
|
||||||
for (const incident of crime.crime_incidents) {
|
|
||||||
if (incident.id === incidentId || incident.id?.toString() === incidentId?.toString()) {
|
|
||||||
console.log("Found matching incident:", incident)
|
|
||||||
foundIncident = {
|
|
||||||
id: incident.id,
|
|
||||||
district: crime.districts.name,
|
|
||||||
description: incident.description,
|
|
||||||
status: incident.status || "unknown",
|
|
||||||
timestamp: incident.timestamp,
|
|
||||||
category: incident.crime_categories.name,
|
|
||||||
type_category: incident.crime_categories.type,
|
|
||||||
address: incident.locations.address,
|
|
||||||
latitude: incident.locations.latitude,
|
|
||||||
longitude: incident.locations.longitude,
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (foundIncident) break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundIncident) {
|
if (
|
||||||
console.error("Could not find incident with ID:", incidentId)
|
customEvent.detail.latitude !== undefined &&
|
||||||
return
|
customEvent.detail.longitude !== undefined &&
|
||||||
}
|
customEvent.detail.category !== undefined
|
||||||
|
) {
|
||||||
|
foundIncident = {
|
||||||
|
id: incidentId,
|
||||||
|
district: customEvent.detail.district,
|
||||||
|
category: customEvent.detail.category,
|
||||||
|
type_category: customEvent.detail.type,
|
||||||
|
description: customEvent.detail.description,
|
||||||
|
status: customEvent.detail.status || "Unknown",
|
||||||
|
timestamp: customEvent.detail.timestamp ? new Date(customEvent.detail.timestamp) : undefined,
|
||||||
|
latitude: customEvent.detail.latitude,
|
||||||
|
longitude: customEvent.detail.longitude,
|
||||||
|
address: customEvent.detail.address,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const crime of crimes) {
|
||||||
|
for (const incident of crime.crime_incidents) {
|
||||||
|
if (incident.id === incidentId || incident.id?.toString() === incidentId?.toString()) {
|
||||||
|
console.log("Found matching incident:", incident)
|
||||||
|
foundIncident = {
|
||||||
|
id: incident.id,
|
||||||
|
district: crime.districts.name,
|
||||||
|
description: incident.description,
|
||||||
|
status: incident.status || "unknown",
|
||||||
|
timestamp: incident.timestamp,
|
||||||
|
category: incident.crime_categories.name,
|
||||||
|
type_category: incident.crime_categories.type,
|
||||||
|
address: incident.locations.address,
|
||||||
|
latitude: incident.locations.latitude,
|
||||||
|
longitude: incident.locations.longitude,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (foundIncident) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!foundIncident.latitude || !foundIncident.longitude) {
|
if (!foundIncident) {
|
||||||
console.error("Found incident has invalid coordinates:", foundIncident)
|
console.error("Could not find incident with ID:", incidentId)
|
||||||
return
|
isInteractingWithMarker.current = false
|
||||||
}
|
return
|
||||||
|
|
||||||
console.log("Setting selected incident:", foundIncident)
|
|
||||||
|
|
||||||
setSelectedDistrict(null)
|
|
||||||
selectedDistrictRef.current = null
|
|
||||||
setFocusedDistrictId(null)
|
|
||||||
|
|
||||||
setSelectedIncident(foundIncident)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!foundIncident.latitude || !foundIncident.longitude) {
|
||||||
|
console.error("Found incident has invalid coordinates:", foundIncident)
|
||||||
|
isInteractingWithMarker.current = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Setting selected incident:", foundIncident)
|
||||||
|
|
||||||
|
// Clear district selection when showing an incident
|
||||||
|
setSelectedDistrict(null)
|
||||||
|
selectedDistrictRef.current = null
|
||||||
|
setFocusedDistrictId(null)
|
||||||
|
|
||||||
|
setSelectedIncident(foundIncident)
|
||||||
|
|
||||||
|
// Reset the marker interaction flag after a delay
|
||||||
|
setTimeout(() => {
|
||||||
|
isInteractingWithMarker.current = false
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
mapboxMap.getCanvas().addEventListener("incident_click", handleIncidentClickEvent as EventListener)
|
mapboxMap.getCanvas().addEventListener("incident_click", handleIncidentClickEvent as EventListener)
|
||||||
document.addEventListener("incident_click", handleIncidentClickEvent as EventListener)
|
document.addEventListener("incident_click", handleIncidentClickEvent as EventListener)
|
||||||
|
|
||||||
|
@ -331,6 +351,31 @@ export default function Layers({
|
||||||
}
|
}
|
||||||
}, [mapboxMap, crimes, setFocusedDistrictId])
|
}, [mapboxMap, crimes, setFocusedDistrictId])
|
||||||
|
|
||||||
|
// Add a listener for unit clicks to set the marker interaction flag
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mapboxMap) return
|
||||||
|
|
||||||
|
const handleUnitClickEvent = (e: Event) => {
|
||||||
|
// Set the marker interaction flag to prevent district selection
|
||||||
|
isInteractingWithMarker.current = true
|
||||||
|
|
||||||
|
// Reset the flag after a delay
|
||||||
|
setTimeout(() => {
|
||||||
|
isInteractingWithMarker.current = false
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
mapboxMap.getCanvas().addEventListener("unit_click", handleUnitClickEvent as EventListener)
|
||||||
|
document.addEventListener("unit_click", handleUnitClickEvent as EventListener)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (mapboxMap && mapboxMap.getCanvas()) {
|
||||||
|
mapboxMap.getCanvas().removeEventListener("unit_click", handleUnitClickEvent as EventListener)
|
||||||
|
}
|
||||||
|
document.removeEventListener("unit_click", handleUnitClickEvent as EventListener)
|
||||||
|
}
|
||||||
|
}, [mapboxMap])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedDistrictRef.current) {
|
if (selectedDistrictRef.current) {
|
||||||
const districtId = selectedDistrictRef.current.id
|
const districtId = selectedDistrictRef.current.id
|
||||||
|
@ -339,64 +384,64 @@ export default function Layers({
|
||||||
if (districtCrime) {
|
if (districtCrime) {
|
||||||
const selectedYearNum = year ? Number.parseInt(year) : new Date().getFullYear()
|
const selectedYearNum = year ? Number.parseInt(year) : new Date().getFullYear()
|
||||||
|
|
||||||
let demographics = districtCrime.districts.demographics?.find((d) => d.year === selectedYearNum)
|
let demographics = districtCrime.districts.demographics?.find((d) => d.year === selectedYearNum)
|
||||||
|
|
||||||
if (!demographics && districtCrime.districts.demographics?.length) {
|
if (!demographics && districtCrime.districts.demographics?.length) {
|
||||||
demographics = districtCrime.districts.demographics.sort((a, b) => b.year - a.year)[0]
|
demographics = districtCrime.districts.demographics.sort((a, b) => b.year - a.year)[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
let geographics = districtCrime.districts.geographics?.find((g) => g.year === selectedYearNum)
|
let geographics = districtCrime.districts.geographics?.find((g) => g.year === selectedYearNum)
|
||||||
|
|
||||||
if (!geographics && districtCrime.districts.geographics?.length) {
|
if (!geographics && districtCrime.districts.geographics?.length) {
|
||||||
const validGeographics = districtCrime.districts.geographics
|
const validGeographics = districtCrime.districts.geographics
|
||||||
.filter((g) => g.year !== null)
|
.filter((g) => g.year !== null)
|
||||||
.sort((a, b) => (b.year || 0) - (a.year || 0))
|
.sort((a, b) => (b.year || 0) - (a.year || 0))
|
||||||
|
|
||||||
geographics = validGeographics.length > 0 ? validGeographics[0] : districtCrime.districts.geographics[0]
|
geographics = validGeographics.length > 0 ? validGeographics[0] : districtCrime.districts.geographics[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!demographics || !geographics) {
|
if (!demographics || !geographics) {
|
||||||
console.error("Missing district data:", { demographics, geographics })
|
console.error("Missing district data:", { demographics, geographics })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const crime_incidents = districtCrime.crime_incidents
|
const crime_incidents = districtCrime.crime_incidents
|
||||||
.filter((incident) => filterCategory === "all" || incident.crime_categories.name === filterCategory)
|
.filter((incident) => filterCategory === "all" || incident.crime_categories.name === filterCategory)
|
||||||
.map((incident) => ({
|
.map((incident) => ({
|
||||||
id: incident.id,
|
id: incident.id,
|
||||||
timestamp: incident.timestamp,
|
timestamp: incident.timestamp,
|
||||||
description: incident.description,
|
description: incident.description,
|
||||||
status: incident.status || "",
|
status: incident.status || "",
|
||||||
category: incident.crime_categories.name,
|
category: incident.crime_categories.name,
|
||||||
type: incident.crime_categories.type || "",
|
type: incident.crime_categories.type || "",
|
||||||
address: incident.locations.address || "",
|
address: incident.locations.address || "",
|
||||||
latitude: incident.locations.latitude,
|
latitude: incident.locations.latitude,
|
||||||
longitude: incident.locations.longitude,
|
longitude: incident.locations.longitude,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const updatedDistrict: IDistrictFeature = {
|
const updatedDistrict: IDistrictFeature = {
|
||||||
...selectedDistrictRef.current,
|
...selectedDistrictRef.current,
|
||||||
number_of_crime: crimeDataByDistrict[districtId]?.number_of_crime || 0,
|
number_of_crime: crimeDataByDistrict[districtId]?.number_of_crime || 0,
|
||||||
level: crimeDataByDistrict[districtId]?.level || selectedDistrictRef.current.level,
|
level: crimeDataByDistrict[districtId]?.level || selectedDistrictRef.current.level,
|
||||||
demographics: {
|
demographics: {
|
||||||
number_of_unemployed: demographics.number_of_unemployed,
|
number_of_unemployed: demographics.number_of_unemployed,
|
||||||
population: demographics.population,
|
population: demographics.population,
|
||||||
population_density: demographics.population_density,
|
population_density: demographics.population_density,
|
||||||
year: demographics.year,
|
year: demographics.year,
|
||||||
},
|
},
|
||||||
geographics: {
|
geographics: {
|
||||||
address: geographics.address || "",
|
address: geographics.address || "",
|
||||||
land_area: geographics.land_area || 0,
|
land_area: geographics.land_area || 0,
|
||||||
year: geographics.year || 0,
|
year: geographics.year || 0,
|
||||||
latitude: geographics.latitude,
|
latitude: geographics.latitude,
|
||||||
longitude: geographics.longitude,
|
longitude: geographics.longitude,
|
||||||
},
|
},
|
||||||
crime_incidents,
|
crime_incidents,
|
||||||
selectedYear: year,
|
selectedYear: year,
|
||||||
selectedMonth: month,
|
selectedMonth: month,
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedDistrictRef.current = updatedDistrict
|
selectedDistrictRef.current = updatedDistrict
|
||||||
|
|
||||||
setSelectedDistrict((prevDistrict) => {
|
setSelectedDistrict((prevDistrict) => {
|
||||||
if (
|
if (
|
||||||
|
@ -414,20 +459,40 @@ export default function Layers({
|
||||||
|
|
||||||
const handleSetFocusedDistrictId = useCallback((id: string | null, isMarkerClick = false) => {
|
const handleSetFocusedDistrictId = useCallback((id: string | null, isMarkerClick = false) => {
|
||||||
console.log("Setting focused district ID:", id, "from marker click:", isMarkerClick)
|
console.log("Setting focused district ID:", id, "from marker click:", isMarkerClick)
|
||||||
|
|
||||||
|
// If this is from a marker click, set the marker interaction flag
|
||||||
|
if (isMarkerClick) {
|
||||||
|
isInteractingWithMarker.current = true
|
||||||
|
|
||||||
|
// Reset the flag after a delay
|
||||||
|
setTimeout(() => {
|
||||||
|
isInteractingWithMarker.current = false
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
setFocusedDistrictId(id)
|
setFocusedDistrictId(id)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (!visible) return null
|
const crimesVisible = activeControl === "incidents"
|
||||||
|
|
||||||
const crimesVisible = activeControl === "incidents"
|
|
||||||
const showHeatmapLayer = activeControl === "heatmap" && sourceType !== "cbu"
|
const showHeatmapLayer = activeControl === "heatmap" && sourceType !== "cbu"
|
||||||
const showUnitsLayer = activeControl === "units"
|
const showUnitsLayer = activeControl === "units"
|
||||||
const showTimelineLayer = activeControl === "timeline"
|
const showTimelineLayer = activeControl === "timeline"
|
||||||
const showHistoricalLayer = activeControl === "historical"
|
const showHistoricalLayer = activeControl === "historical"
|
||||||
const showRecentIncidents = activeControl === "recents"
|
const showRecentIncidents = activeControl === "recents"
|
||||||
const showDistrictFill = activeControl === "incidents" || activeControl === "clusters" || activeControl === "historical" || activeControl === "recents"
|
const showDistrictFill =
|
||||||
|
activeControl === "incidents" ||
|
||||||
|
activeControl === "clusters" ||
|
||||||
|
activeControl === "historical" ||
|
||||||
|
activeControl === "recents"
|
||||||
const showIncidentMarkers = activeControl !== "heatmap" && activeControl !== "timeline" && sourceType !== "cbu"
|
const showIncidentMarkers = activeControl !== "heatmap" && activeControl !== "timeline" && sourceType !== "cbu"
|
||||||
|
|
||||||
|
// Ensure showPanicDemo is always defined
|
||||||
|
// const [displayPanicDemo, setDisplayPanicDemo] = useState(showEWS && showPanicDemo)
|
||||||
|
|
||||||
|
// Always render the DistrictExtrusionLayer when a district is focused
|
||||||
|
// This ensures it's available when needed
|
||||||
|
const shouldShowExtrusion = focusedDistrictId !== null && !isInteractingWithMarker.current
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DistrictFillLineLayer
|
<DistrictFillLineLayer
|
||||||
|
@ -446,12 +511,19 @@ export default function Layers({
|
||||||
onDistrictClick={handleDistrictClick}
|
onDistrictClick={handleDistrictClick}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Always render the extrusion layer when a district is focused */}
|
||||||
|
{shouldShowExtrusion && (
|
||||||
|
<DistrictExtrusionLayer
|
||||||
|
visible={true}
|
||||||
|
map={mapboxMap}
|
||||||
|
tilesetId={tilesetId}
|
||||||
|
focusedDistrictId={focusedDistrictId}
|
||||||
|
crimeDataByDistrict={crimeDataByDistrict}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Recent Incidents Layer (24 hours) */}
|
{/* Recent Incidents Layer (24 hours) */}
|
||||||
<RecentIncidentsLayer
|
<RecentIncidentsLayer visible={showRecentIncidents} map={mapboxMap} incidents={recentIncidents} />
|
||||||
visible={showRecentIncidents}
|
|
||||||
map={mapboxMap}
|
|
||||||
incidents={recentIncidents}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<HeatmapLayer
|
<HeatmapLayer
|
||||||
crimes={crimes}
|
crimes={crimes}
|
||||||
|
@ -501,48 +573,30 @@ export default function Layers({
|
||||||
focusedDistrictId={focusedDistrictId}
|
focusedDistrictId={focusedDistrictId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{selectedDistrict && !selectedIncident && (
|
{selectedDistrict && !selectedIncident && !isInteractingWithMarker.current && (
|
||||||
<>
|
<DistrictPopup
|
||||||
<DistrictPopup
|
longitude={selectedDistrict.longitude || 0}
|
||||||
longitude={selectedDistrict.longitude || 0}
|
latitude={selectedDistrict.latitude || 0}
|
||||||
latitude={selectedDistrict.latitude || 0}
|
onClose={handleCloseDistrictPopup}
|
||||||
onClose={handleCloseDistrictPopup}
|
district={selectedDistrict}
|
||||||
district={selectedDistrict}
|
year={year}
|
||||||
year={year}
|
month={month}
|
||||||
month={month}
|
filterCategory={filterCategory}
|
||||||
filterCategory={filterCategory}
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<DistrictExtrusionLayer
|
|
||||||
visible={visible}
|
|
||||||
map={mapboxMap}
|
|
||||||
tilesetId={tilesetId}
|
|
||||||
focusedDistrictId={focusedDistrictId}
|
|
||||||
crimeDataByDistrict={crimeDataByDistrict}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TimezoneLayer map={mapboxMap} />
|
<TimezoneLayer map={mapboxMap} />
|
||||||
|
|
||||||
<FaultLinesLayer map={mapboxMap} />
|
<FaultLinesLayer map={mapboxMap} />
|
||||||
|
|
||||||
{/* <CoastlineLayer map={mapboxMap} /> */}
|
{showEWS && <EWSAlertLayer map={mapboxMap} incidents={ewsIncidents} onIncidentResolved={handleResolveIncident} />}
|
||||||
|
|
||||||
{showEWS && (
|
{showEWS && displayPanicDemo && (
|
||||||
<EWSAlertLayer
|
|
||||||
map={mapboxMap}
|
|
||||||
incidents={ewsIncidents}
|
|
||||||
onIncidentResolved={handleResolveIncident}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showEWS && showPanicDemo && (
|
|
||||||
<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}
|
||||||
onResolveAllAlerts={handleResolveAllAlerts}
|
onResolveAllAlerts={handleResolveAllAlerts}
|
||||||
activeIncidents={ewsIncidents.filter(inc => inc.status === 'active')}
|
activeIncidents={ewsIncidents.filter((inc) => inc.status === "active")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -9,234 +9,229 @@ interface RecentIncidentsLayerProps {
|
||||||
incidents?: IIncidentLogs[]
|
incidents?: IIncidentLogs[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RecentIncidentsLayer({
|
export default function RecentIncidentsLayer({ visible = false, map, incidents = [] }: RecentIncidentsLayerProps) {
|
||||||
visible = false,
|
const isInteractingWithMarker = useRef(false)
|
||||||
map,
|
|
||||||
incidents = [],
|
|
||||||
}: RecentIncidentsLayerProps) {
|
|
||||||
const isInteractingWithMarker = useRef(false);
|
|
||||||
|
|
||||||
// Filter incidents from the last 24 hours
|
// Filter incidents from the last 24 hours
|
||||||
const recentIncidents = incidents.filter(incident => {
|
const recentIncidents = incidents.filter((incident) => {
|
||||||
if (!incident.timestamp) return false;
|
if (!incident.timestamp) return false
|
||||||
const incidentDate = new Date(incident.timestamp);
|
const incidentDate = new Date(incident.timestamp)
|
||||||
const now = new Date();
|
const now = new Date()
|
||||||
const timeDiff = now.getTime() - incidentDate.getTime();
|
const timeDiff = now.getTime() - incidentDate.getTime()
|
||||||
// 86400000 = 24 hours in milliseconds
|
// 86400000 = 24 hours in milliseconds
|
||||||
return timeDiff <= 86400000;
|
return timeDiff <= 86400000
|
||||||
});
|
})
|
||||||
|
|
||||||
const handleIncidentClick = useCallback(
|
const handleIncidentClick = useCallback(
|
||||||
(e: any) => {
|
(e: any) => {
|
||||||
if (!map) return;
|
if (!map) return
|
||||||
|
|
||||||
const features = map.queryRenderedFeatures(e.point, { layers: ["recent-incidents"] });
|
const features = map.queryRenderedFeatures(e.point, { layers: ["recent-incidents"] })
|
||||||
if (!features || features.length === 0) return;
|
if (!features || features.length === 0) return
|
||||||
|
|
||||||
isInteractingWithMarker.current = true;
|
// Stop event propagation
|
||||||
|
e.originalEvent.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
const incident = features[0];
|
isInteractingWithMarker.current = true
|
||||||
if (!incident.properties) return;
|
|
||||||
|
|
||||||
e.originalEvent.stopPropagation();
|
const incident = features[0]
|
||||||
e.preventDefault();
|
if (!incident.properties) return
|
||||||
|
|
||||||
const incidentDetails = {
|
e.originalEvent.stopPropagation()
|
||||||
id: incident.properties.id,
|
e.preventDefault()
|
||||||
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);
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure markers stay visible
|
console.log("Recent incident clicked:", incidentDetails)
|
||||||
if (map.getLayer("recent-incidents")) {
|
|
||||||
map.setLayoutProperty("recent-incidents", "visibility", "visible");
|
|
||||||
}
|
|
||||||
|
|
||||||
// First fly to the incident location
|
// Ensure markers stay visible
|
||||||
map.flyTo({
|
if (map.getLayer("recent-incidents")) {
|
||||||
center: [incidentDetails.longitude, incidentDetails.latitude],
|
map.setLayoutProperty("recent-incidents", "visibility", "visible")
|
||||||
zoom: 15,
|
}
|
||||||
bearing: 0,
|
|
||||||
pitch: 45,
|
|
||||||
duration: 2000,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Dispatch the incident_click event to show the popup
|
// First fly to the incident location
|
||||||
const customEvent = new CustomEvent("incident_click", {
|
map.flyTo({
|
||||||
detail: incidentDetails,
|
center: [incidentDetails.longitude, incidentDetails.latitude],
|
||||||
bubbles: true,
|
zoom: 15,
|
||||||
});
|
bearing: 0,
|
||||||
|
pitch: 45,
|
||||||
|
duration: 2000,
|
||||||
|
})
|
||||||
|
|
||||||
map.getCanvas().dispatchEvent(customEvent);
|
// Dispatch the incident_click event to show the popup
|
||||||
document.dispatchEvent(customEvent);
|
const customEvent = new CustomEvent("incident_click", {
|
||||||
|
detail: incidentDetails,
|
||||||
|
bubbles: true,
|
||||||
|
})
|
||||||
|
|
||||||
// Reset the flag after a delay
|
map.getCanvas().dispatchEvent(customEvent)
|
||||||
setTimeout(() => {
|
document.dispatchEvent(customEvent)
|
||||||
isInteractingWithMarker.current = false;
|
|
||||||
}, 5000);
|
// Reset the flag after a delay
|
||||||
},
|
setTimeout(() => {
|
||||||
[map]
|
isInteractingWithMarker.current = false
|
||||||
);
|
}, 5000)
|
||||||
|
},
|
||||||
|
[map],
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map || !visible) return;
|
if (!map || !visible) return
|
||||||
|
|
||||||
console.log(`Setting up recent incidents layer with ${recentIncidents.length} incidents from the last 24 hours`);
|
console.log(`Setting up recent incidents layer with ${recentIncidents.length} incidents from the last 24 hours`)
|
||||||
|
|
||||||
// Convert incidents to GeoJSON
|
// Convert incidents to GeoJSON
|
||||||
const recentData = {
|
const recentData = {
|
||||||
type: "FeatureCollection" as const,
|
type: "FeatureCollection" as const,
|
||||||
features: recentIncidents.map(incident => ({
|
features: recentIncidents.map((incident) => ({
|
||||||
type: "Feature" as const,
|
type: "Feature" as const,
|
||||||
geometry: {
|
geometry: {
|
||||||
type: "Point" as const,
|
type: "Point" as const,
|
||||||
coordinates: [incident.longitude, incident.latitude],
|
coordinates: [incident.longitude, incident.latitude],
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
id: incident.id,
|
id: incident.id,
|
||||||
user_id: incident.user_id,
|
user_id: incident.user_id,
|
||||||
address: incident.address,
|
address: incident.address,
|
||||||
description: incident.description,
|
description: incident.description,
|
||||||
timestamp: incident.timestamp ? incident.timestamp.toString() : new Date().toString(),
|
timestamp: incident.timestamp ? incident.timestamp.toString() : new Date().toString(),
|
||||||
category: incident.category,
|
category: incident.category,
|
||||||
district: incident.district,
|
district: incident.district,
|
||||||
severity: incident.severity,
|
severity: incident.severity,
|
||||||
status: incident.verified,
|
status: incident.verified,
|
||||||
source: incident.source,
|
source: incident.source,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
};
|
}
|
||||||
|
|
||||||
const setupLayerAndSource = () => {
|
const setupLayerAndSource = () => {
|
||||||
try {
|
try {
|
||||||
// Check if source exists and update it
|
// Check if source exists and update it
|
||||||
if (map.getSource("recent-incidents-source")) {
|
if (map.getSource("recent-incidents-source")) {
|
||||||
(map.getSource("recent-incidents-source") as any).setData(recentData);
|
; (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 {
|
} else {
|
||||||
map.once("style.load", setupLayerAndSource);
|
// If not, add source
|
||||||
|
map.addSource("recent-incidents-source", {
|
||||||
// Fallback
|
type: "geojson",
|
||||||
setTimeout(() => {
|
data: recentData,
|
||||||
if (map.isStyleLoaded()) {
|
})
|
||||||
setupLayerAndSource();
|
|
||||||
} else {
|
|
||||||
console.warn("Map style still not loaded after timeout");
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
// Find first symbol layer for proper layering
|
||||||
if (map) {
|
const layers = map.getStyle().layers
|
||||||
map.off("click", "recent-incidents", handleIncidentClick);
|
let firstSymbolId: string | undefined
|
||||||
|
for (const layer of layers) {
|
||||||
|
if (layer.type === "symbol") {
|
||||||
|
firstSymbolId = layer.id
|
||||||
|
break
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}, [map, visible, recentIncidents, handleIncidentClick]);
|
|
||||||
|
|
||||||
return null;
|
// 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import type mapboxgl from "mapbox-gl"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
import { calculateAverageTimeOfDay } from "@/app/_utils/time"
|
import { calculateAverageTimeOfDay } from "@/app/_utils/time"
|
||||||
import TimelinePopup from "../pop-up/timeline-popup"
|
import TimelinePopup from "../pop-up/timeline-popup"
|
||||||
import TimeZonesDisplay from "./timezone"
|
|
||||||
|
|
||||||
interface TimelineLayerProps {
|
interface TimelineLayerProps {
|
||||||
crimes: ICrimes[]
|
crimes: ICrimes[]
|
||||||
|
@ -45,74 +44,74 @@ export default function TimelineLayer({
|
||||||
}
|
}
|
||||||
>()
|
>()
|
||||||
|
|
||||||
crimes.forEach((crime) => {
|
crimes.forEach((crime) => {
|
||||||
if (!crime.districts || !crime.district_id) return
|
if (!crime.districts || !crime.district_id) return
|
||||||
|
|
||||||
// Initialize district group if not exists
|
// Initialize district group if not exists
|
||||||
if (!districtGroups.has(crime.district_id)) {
|
if (!districtGroups.has(crime.district_id)) {
|
||||||
// Find a central location for the district from any incident
|
// Find a central location for the district from any incident
|
||||||
const centerIncident = crime.crime_incidents.find((inc) => inc.locations?.latitude && inc.locations?.longitude)
|
const centerIncident = crime.crime_incidents.find((inc) => inc.locations?.latitude && inc.locations?.longitude)
|
||||||
|
|
||||||
const center: [number, number] = centerIncident
|
const center: [number, number] = centerIncident
|
||||||
? [centerIncident.locations.longitude, centerIncident.locations.latitude]
|
? [centerIncident.locations.longitude, centerIncident.locations.latitude]
|
||||||
: [0, 0]
|
: [0, 0]
|
||||||
|
|
||||||
districtGroups.set(crime.district_id, {
|
districtGroups.set(crime.district_id, {
|
||||||
districtId: crime.district_id,
|
districtId: crime.district_id,
|
||||||
districtName: crime.districts.name,
|
districtName: crime.districts.name,
|
||||||
incidents: [],
|
incidents: [],
|
||||||
center,
|
center,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter incidents appropriately before adding
|
||||||
|
crime.crime_incidents.forEach((incident) => {
|
||||||
|
// Skip invalid incidents
|
||||||
|
if (!incident.timestamp) return
|
||||||
|
if (filterCategory !== "all" && incident.crime_categories.name !== filterCategory) return
|
||||||
|
|
||||||
|
// Add to appropriate district group
|
||||||
|
const group = districtGroups.get(crime.district_id)
|
||||||
|
if (group) {
|
||||||
|
group.incidents.push({
|
||||||
|
timestamp: new Date(incident.timestamp),
|
||||||
|
category: incident.crime_categories.name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// Filter incidents appropriately before adding
|
// Calculate average time for each district
|
||||||
crime.crime_incidents.forEach((incident) => {
|
const result = Array.from(districtGroups.values())
|
||||||
// Skip invalid incidents
|
.filter((group) => group.incidents.length > 0 && group.center[0] !== 0)
|
||||||
if (!incident.timestamp) return
|
.map((group) => {
|
||||||
if (filterCategory !== "all" && incident.crime_categories.name !== filterCategory) return
|
const avgTimeInfo = calculateAverageTimeOfDay(group.incidents.map((inc) => inc.timestamp))
|
||||||
|
|
||||||
// Add to appropriate district group
|
return {
|
||||||
const group = districtGroups.get(crime.district_id)
|
id: group.districtId,
|
||||||
if (group) {
|
name: group.districtName,
|
||||||
group.incidents.push({
|
center: group.center,
|
||||||
timestamp: new Date(incident.timestamp),
|
avgHour: avgTimeInfo.hour,
|
||||||
category: incident.crime_categories.name,
|
avgMinute: avgTimeInfo.minute,
|
||||||
})
|
formattedTime: avgTimeInfo.formattedTime,
|
||||||
}
|
timeDescription: avgTimeInfo.description,
|
||||||
})
|
totalIncidents: group.incidents.length,
|
||||||
})
|
timeOfDay: avgTimeInfo.timeOfDay,
|
||||||
|
earliestTime: format(avgTimeInfo.earliest, "p"),
|
||||||
|
latestTime: format(avgTimeInfo.latest, "p"),
|
||||||
|
mostFrequentHour: avgTimeInfo.mostFrequentHour,
|
||||||
|
categoryCounts: group.incidents.reduce(
|
||||||
|
(acc, inc) => {
|
||||||
|
acc[inc.category] = (acc[inc.category] || 0) + 1
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{} as Record<string, number>,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Calculate average time for each district
|
return result
|
||||||
const result = Array.from(districtGroups.values())
|
}, [crimes, filterCategory, year, month])
|
||||||
.filter((group) => group.incidents.length > 0 && group.center[0] !== 0)
|
|
||||||
.map((group) => {
|
|
||||||
const avgTimeInfo = calculateAverageTimeOfDay(group.incidents.map((inc) => inc.timestamp))
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: group.districtId,
|
|
||||||
name: group.districtName,
|
|
||||||
center: group.center,
|
|
||||||
avgHour: avgTimeInfo.hour,
|
|
||||||
avgMinute: avgTimeInfo.minute,
|
|
||||||
formattedTime: avgTimeInfo.formattedTime,
|
|
||||||
timeDescription: avgTimeInfo.description,
|
|
||||||
totalIncidents: group.incidents.length,
|
|
||||||
timeOfDay: avgTimeInfo.timeOfDay,
|
|
||||||
earliestTime: format(avgTimeInfo.earliest, "p"),
|
|
||||||
latestTime: format(avgTimeInfo.latest, "p"),
|
|
||||||
mostFrequentHour: avgTimeInfo.mostFrequentHour,
|
|
||||||
categoryCounts: group.incidents.reduce(
|
|
||||||
(acc, inc) => {
|
|
||||||
acc[inc.category] = (acc[inc.category] || 0) + 1
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
{} as Record<string, number>,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return result
|
|
||||||
}, [crimes, filterCategory, year, month])
|
|
||||||
|
|
||||||
// Convert processed data to GeoJSON for display
|
// Convert processed data to GeoJSON for display
|
||||||
const timelineGeoJSON = useMemo(() => {
|
const timelineGeoJSON = useMemo(() => {
|
||||||
|
@ -143,30 +142,34 @@ export default function TimelineLayer({
|
||||||
(e: mapboxgl.MapMouseEvent & { features?: mapboxgl.MapboxGeoJSONFeature[] }) => {
|
(e: mapboxgl.MapMouseEvent & { features?: mapboxgl.MapboxGeoJSONFeature[] }) => {
|
||||||
if (!e.features || e.features.length === 0) return
|
if (!e.features || e.features.length === 0) return
|
||||||
|
|
||||||
const feature = e.features[0]
|
// Stop event propagation
|
||||||
const props = feature.properties
|
e.originalEvent.stopPropagation()
|
||||||
if (!props) return
|
e.preventDefault()
|
||||||
|
|
||||||
// Get the corresponding district data for detailed info
|
const feature = e.features[0]
|
||||||
const districtData = districtTimeData.find((d) => d.id === props.id)
|
const props = feature.properties
|
||||||
if (!districtData) return
|
if (!props) return
|
||||||
|
|
||||||
// Fly to the location
|
// Get the corresponding district data for detailed info
|
||||||
if (map) {
|
const districtData = districtTimeData.find((d) => d.id === props.id)
|
||||||
map.flyTo({
|
if (!districtData) return
|
||||||
center: districtData.center,
|
|
||||||
zoom: 12,
|
|
||||||
duration: 1000,
|
|
||||||
pitch: 45,
|
|
||||||
bearing: 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the selected district for popup
|
// Fly to the location
|
||||||
setSelectedDistrict(districtData)
|
if (map) {
|
||||||
},
|
map.flyTo({
|
||||||
[map, districtTimeData],
|
center: districtData.center,
|
||||||
)
|
zoom: 12,
|
||||||
|
duration: 1000,
|
||||||
|
pitch: 45,
|
||||||
|
bearing: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the selected district for popup
|
||||||
|
setSelectedDistrict(districtData)
|
||||||
|
},
|
||||||
|
[map, districtTimeData],
|
||||||
|
)
|
||||||
|
|
||||||
// Handle popup close
|
// Handle popup close
|
||||||
const handleClosePopup = useCallback(() => {
|
const handleClosePopup = useCallback(() => {
|
||||||
|
@ -177,45 +180,45 @@ export default function TimelineLayer({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map || !visible) return
|
if (!map || !visible) return
|
||||||
|
|
||||||
// Hide incident markers when timeline mode is activated
|
// Hide incident markers when timeline mode is activated
|
||||||
if (map.getLayer("unclustered-point")) {
|
if (map.getLayer("unclustered-point")) {
|
||||||
map.setLayoutProperty("unclustered-point", "visibility", "none")
|
map.setLayoutProperty("unclustered-point", "visibility", "none")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide clusters when timeline mode is activated
|
// Hide clusters when timeline mode is activated
|
||||||
if (map.getLayer("clusters")) {
|
if (map.getLayer("clusters")) {
|
||||||
map.setLayoutProperty("clusters", "visibility", "none")
|
map.setLayoutProperty("clusters", "visibility", "none")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (map.getLayer("cluster-count")) {
|
if (map.getLayer("cluster-count")) {
|
||||||
map.setLayoutProperty("cluster-count", "visibility", "none")
|
map.setLayoutProperty("cluster-count", "visibility", "none")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up event handlers
|
// Set up event handlers
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
if (map) map.getCanvas().style.cursor = "pointer"
|
if (map) map.getCanvas().style.cursor = "pointer"
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
const handleMouseLeave = () => {
|
||||||
if (map) map.getCanvas().style.cursor = ""
|
if (map) map.getCanvas().style.cursor = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add event listeners
|
// Add event listeners
|
||||||
if (map.getLayer("timeline-markers")) {
|
if (map.getLayer("timeline-markers")) {
|
||||||
map.on("click", "timeline-markers", handleMarkerClick)
|
map.on("click", "timeline-markers", handleMarkerClick)
|
||||||
map.on("mouseenter", "timeline-markers", handleMouseEnter)
|
map.on("mouseenter", "timeline-markers", handleMouseEnter)
|
||||||
map.on("mouseleave", "timeline-markers", handleMouseLeave)
|
map.on("mouseleave", "timeline-markers", handleMouseLeave)
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// Clean up event listeners
|
// Clean up event listeners
|
||||||
if (map) {
|
if (map) {
|
||||||
map.off("click", "timeline-markers", handleMarkerClick)
|
map.off("click", "timeline-markers", handleMarkerClick)
|
||||||
map.off("mouseenter", "timeline-markers", handleMouseEnter)
|
map.off("mouseenter", "timeline-markers", handleMouseEnter)
|
||||||
map.off("mouseleave", "timeline-markers", handleMouseLeave)
|
map.off("mouseleave", "timeline-markers", handleMouseLeave)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [map, visible, handleMarkerClick])
|
}, [map, visible, handleMarkerClick])
|
||||||
|
|
||||||
// Clean up on unmount or when visibility changes
|
// Clean up on unmount or when visibility changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -254,44 +257,42 @@ export default function TimelineLayer({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Digital clock display */}
|
{/* Digital clock display */}
|
||||||
<Layer
|
<Layer
|
||||||
id="timeline-markers"
|
id="timeline-markers"
|
||||||
type="symbol"
|
type="symbol"
|
||||||
layout={{
|
layout={{
|
||||||
"text-field": "{avgTime}",
|
"text-field": "{avgTime}",
|
||||||
"text-font": ["DIN Offc Pro Medium", "Arial Unicode MS Bold"],
|
"text-font": ["DIN Offc Pro Medium", "Arial Unicode MS Bold"],
|
||||||
"text-size": 12,
|
"text-size": 12,
|
||||||
"text-anchor": "center",
|
"text-anchor": "center",
|
||||||
"text-allow-overlap": true,
|
"text-allow-overlap": true,
|
||||||
}}
|
}}
|
||||||
paint={{
|
paint={{
|
||||||
"text-color": [
|
"text-color": [
|
||||||
"match",
|
"match",
|
||||||
["get", "timeOfDay"],
|
["get", "timeOfDay"],
|
||||||
"night",
|
"night",
|
||||||
"#FFFFFF",
|
"#FFFFFF",
|
||||||
"evening",
|
"evening",
|
||||||
"#FFFFFF",
|
"#FFFFFF",
|
||||||
"#000000", // Default text color
|
"#000000", // Default text color
|
||||||
],
|
],
|
||||||
"text-halo-color": "#000000",
|
"text-halo-color": "#000000",
|
||||||
"text-halo-width": 0.5,
|
"text-halo-width": 0.5,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Source>
|
</Source>
|
||||||
|
|
||||||
{/* Custom Popup Component */}
|
{/* Custom Popup Component */}
|
||||||
{selectedDistrict && (
|
{selectedDistrict && (
|
||||||
<TimelinePopup
|
<TimelinePopup
|
||||||
longitude={selectedDistrict.center[0]}
|
longitude={selectedDistrict.center[0]}
|
||||||
latitude={selectedDistrict.center[1]}
|
latitude={selectedDistrict.center[1]}
|
||||||
onClose={handleClosePopup}
|
onClose={handleClosePopup}
|
||||||
district={selectedDistrict}
|
district={selectedDistrict}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,40 +64,39 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
|
|
||||||
// Process units data to GeoJSON format
|
// Process units data to GeoJSON format
|
||||||
const unitsGeoJSON = useMemo(() => {
|
const unitsGeoJSON = useMemo(() => {
|
||||||
console.log("Units data being processed:", unitsData); // Debug log
|
console.log("Units data being processed:", unitsData) // Debug log
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "FeatureCollection" as const,
|
type: "FeatureCollection" as const,
|
||||||
features: unitsData
|
features: unitsData.map((unit) => {
|
||||||
.map((unit) => {
|
// Debug log for individual units
|
||||||
// Debug log for individual units
|
console.log("Processing unit:", unit.code_unit, unit.name, {
|
||||||
console.log("Processing unit:", unit.code_unit, unit.name, {
|
longitude: unit.longitude,
|
||||||
longitude: unit.longitude,
|
latitude: unit.latitude,
|
||||||
latitude: unit.latitude,
|
district: unit.district_name,
|
||||||
district: unit.district_name
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "Feature" as const,
|
|
||||||
properties: {
|
|
||||||
id: unit.code_unit,
|
|
||||||
name: unit.name,
|
|
||||||
address: unit.address,
|
|
||||||
phone: unit.phone,
|
|
||||||
type: unit.type,
|
|
||||||
district: unit.district_name || "",
|
|
||||||
district_id: unit.district_id,
|
|
||||||
},
|
|
||||||
geometry: {
|
|
||||||
type: "Point" as const,
|
|
||||||
coordinates: [
|
|
||||||
parseFloat(String(unit.longitude)) || 0,
|
|
||||||
parseFloat(String(unit.latitude)) || 0
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
};
|
|
||||||
|
return {
|
||||||
|
type: "Feature" as const,
|
||||||
|
properties: {
|
||||||
|
id: unit.code_unit,
|
||||||
|
name: unit.name,
|
||||||
|
address: unit.address,
|
||||||
|
phone: unit.phone,
|
||||||
|
type: unit.type,
|
||||||
|
district: unit.district_name || "",
|
||||||
|
district_id: unit.district_id,
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
type: "Point" as const,
|
||||||
|
coordinates: [
|
||||||
|
Number.parseFloat(String(unit.longitude)) || 0,
|
||||||
|
Number.parseFloat(String(unit.latitude)) || 0,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
}, [unitsData])
|
}, [unitsData])
|
||||||
|
|
||||||
// Process incident data to GeoJSON format
|
// Process incident data to GeoJSON format
|
||||||
|
@ -225,6 +224,7 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
(e: mapboxgl.MapMouseEvent & { features?: mapboxgl.MapboxGeoJSONFeature[] }) => {
|
(e: mapboxgl.MapMouseEvent & { features?: mapboxgl.MapboxGeoJSONFeature[] }) => {
|
||||||
if (!e.features || e.features.length === 0) return
|
if (!e.features || e.features.length === 0) return
|
||||||
|
|
||||||
|
// Stop event propagation to prevent district layer from handling this click
|
||||||
e.originalEvent.stopPropagation()
|
e.originalEvent.stopPropagation()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
|
@ -236,27 +236,26 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
// Find the unit in our data
|
// Find the unit in our data
|
||||||
const unit = unitsData.find((u) => u.code_unit === properties.id)
|
const unit = unitsData.find((u) => u.code_unit === properties.id)
|
||||||
if (!unit) {
|
if (!unit) {
|
||||||
console.log("Unit not found in data:", properties.id);
|
console.log("Unit not found in data:", properties.id)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
// Find all incidents in the same district as the unit
|
// Find all incidents in the same district as the unit
|
||||||
const districtIncidents: IDistrictIncidents[] = []
|
const districtIncidents: IDistrictIncidents[] = []
|
||||||
crimes.forEach(crime => {
|
crimes.forEach((crime) => {
|
||||||
// Check if this crime is in the same district as the unit
|
// Check if this crime is in the same district as the unit
|
||||||
|
|
||||||
console.log("Checking district ID:", crime.district_id, "against unit district ID:", unit.district_id);
|
if (selectedUnit?.code_unit === unit.code_unit) {
|
||||||
|
crime.crime_incidents.forEach((incident) => {
|
||||||
if (crime.districts.name === unit.district_name) {
|
if (incident.locations && typeof incident.locations.distance_to_unit !== "undefined") {
|
||||||
crime.crime_incidents.forEach(incident => {
|
|
||||||
if (incident.locations && typeof incident.locations.distance_to_unit !== 'undefined') {
|
|
||||||
districtIncidents.push({
|
districtIncidents.push({
|
||||||
incident_id: incident.id,
|
incident_id: incident.id,
|
||||||
category_name: incident.crime_categories.name,
|
category_name: incident.crime_categories.name,
|
||||||
incident_description: incident.description || 'No description',
|
incident_description: incident.description || "No description",
|
||||||
distance_meters: incident.locations.distance_to_unit!,
|
distance_meters: incident.locations.distance_to_unit!,
|
||||||
timestamp: incident.timestamp
|
timestamp: incident.timestamp,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -266,12 +265,11 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
// 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)
|
||||||
console.log("Sorted district incidents:", districtIncidents);
|
|
||||||
|
|
||||||
// Update the state with the distance results
|
// Update the state with the distance results
|
||||||
setUnitIncident(districtIncidents)
|
setUnitIncident(districtIncidents)
|
||||||
|
setIsLoading(false)
|
||||||
|
|
||||||
// Fly to the unit location
|
// Fly to the unit location
|
||||||
map.flyTo({
|
map.flyTo({
|
||||||
|
@ -325,6 +323,7 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
(e: mapboxgl.MapMouseEvent & { features?: mapboxgl.MapboxGeoJSONFeature[] }) => {
|
(e: mapboxgl.MapMouseEvent & { features?: mapboxgl.MapboxGeoJSONFeature[] }) => {
|
||||||
if (!e.features || e.features.length === 0) return
|
if (!e.features || e.features.length === 0) return
|
||||||
|
|
||||||
|
// Stop event propagation
|
||||||
e.originalEvent.stopPropagation()
|
e.originalEvent.stopPropagation()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
|
@ -430,7 +429,10 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
|
||||||
if (!map || !visible) return
|
if (!map || !visible) return
|
||||||
|
|
||||||
// Debug log to confirm map layers
|
// Debug log to confirm map layers
|
||||||
console.log("Available map layers:", map.getStyle().layers?.map(l => l.id));
|
console.log(
|
||||||
|
"Available map layers:",
|
||||||
|
map.getStyle().layers?.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 = () => {
|
||||||
|
|
|
@ -62,12 +62,12 @@ export default function UnitPopup({
|
||||||
closeOnClick={false}
|
closeOnClick={false}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
anchor="top"
|
anchor="top"
|
||||||
maxWidth="420px"
|
maxWidth="320px"
|
||||||
className="unit-popup z-50"
|
className="unit-popup z-50"
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Card
|
<Card
|
||||||
className="bg-background p-0 w-full max-w-[420px] shadow-xl border-0 overflow-hidden border-l-4 border-l-blue-700"
|
className="bg-background p-0 w-full max-w-[320px] shadow-xl border-0 overflow-hidden border-l-4 border-l-blue-700"
|
||||||
>
|
>
|
||||||
<div className="p-4 relative">
|
<div className="p-4 relative">
|
||||||
{/* Custom close button */}
|
{/* Custom close button */}
|
||||||
|
@ -172,7 +172,28 @@ export default function UnitPopup({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
{/* Connection line */}
|
||||||
|
<div
|
||||||
|
className="absolute bottom-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 bottom-0 left-1/2 transform -translate-x-1/2 translate-y-full"
|
||||||
|
style={{
|
||||||
|
width: '6px',
|
||||||
|
height: '6px',
|
||||||
|
backgroundColor: 'red',
|
||||||
|
borderRadius: '50%',
|
||||||
|
marginTop: '20px',
|
||||||
|
boxShadow: '0 0 4px rgba(0, 0, 0, 0.3)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
)
|
)
|
||||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue