fix: fix bug district pop up always show while selected district is not focus

This commit is contained in:
vergiLgood1 2025-05-14 10:06:09 +07:00
parent 834d4b02cf
commit 9c6e005839
9 changed files with 1915 additions and 1931 deletions

View File

@ -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();

View File

@ -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,6 +21,8 @@ export default function DistrictExtrusionLayer({
useEffect(() => { useEffect(() => {
if (!map || !visible) return if (!map || !visible) return
console.log("DistrictExtrusionLayer effect running, focusedDistrictId:", focusedDistrictId)
const onStyleLoad = () => { const onStyleLoad = () => {
if (!map) return if (!map) return
@ -43,8 +45,8 @@ export default function DistrictExtrusionLayer({
// Make sure the districts source exists // Make sure the districts source exists
if (!map.getSource("districts")) { if (!map.getSource("districts")) {
if (!tilesetId) { if (!tilesetId) {
console.error("No tileset ID provided for districts source"); console.error("No tileset ID provided for districts source")
return; return
} }
map.addSource("districts", { map.addSource("districts", {
@ -84,13 +86,15 @@ export default function DistrictExtrusionLayer({
}, },
filter: ["==", ["get", "kode_kec"], focusedDistrictId || ""], filter: ["==", ["get", "kode_kec"], focusedDistrictId || ""],
}, },
firstSymbolId firstSymbolId,
) )
extrusionCreatedRef.current = true extrusionCreatedRef.current = true
console.log("District extrusion layer created")
// If a district is focused, start the animation // If a district is focused, start the animation
if (focusedDistrictId) { if (focusedDistrictId) {
console.log("Starting animation for district:", focusedDistrictId)
lastFocusedDistrictRef.current = focusedDistrictId lastFocusedDistrictRef.current = focusedDistrictId
animateExtrusion() animateExtrusion()
} }
@ -111,14 +115,16 @@ export default function DistrictExtrusionLayer({
animationRef.current = null animationRef.current = null
} }
} }
}, [map, visible, tilesetId]) }, [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
console.log("Updating district extrusion for district:", focusedDistrictId)
// Skip unnecessary updates if nothing has changed // Skip unnecessary updates if nothing has changed
if (lastFocusedDistrictRef.current === focusedDistrictId) return; if (lastFocusedDistrictRef.current === focusedDistrictId) return
// If we're unfocusing a district // If we're unfocusing a district
if (!focusedDistrictId) { if (!focusedDistrictId) {
@ -131,17 +137,17 @@ export default function DistrictExtrusionLayer({
// Animate height down // Animate height down
const animateHeightDown = () => { const animateHeightDown = () => {
if (!map || !map.getLayer("district-extrusion")) return; if (!map || !map.getLayer("district-extrusion")) return
let currentHeight = 800; const currentHeight = 800
const duration = 500; const duration = 500
const startTime = performance.now(); const startTime = performance.now()
const animate = (time: number) => { const animate = (time: number) => {
const elapsed = time - startTime; const elapsed = time - startTime
const progress = Math.min(elapsed / duration, 1); const progress = Math.min(elapsed / duration, 1)
const easedProgress = progress * (2 - progress); // easeOutQuad const easedProgress = progress * (2 - progress) // easeOutQuad
const height = 800 - (800 * easedProgress); const height = 800 - 800 * easedProgress
try { try {
map.setPaintProperty("district-extrusion", "fill-extrusion-height", [ map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
@ -149,10 +155,10 @@ export default function DistrictExtrusionLayer({
["has", "kode_kec"], ["has", "kode_kec"],
["match", ["get", "kode_kec"], lastFocusedDistrictRef.current || "", height, 0], ["match", ["get", "kode_kec"], lastFocusedDistrictRef.current || "", height, 0],
0, 0,
]); ])
if (progress < 1) { if (progress < 1) {
animationRef.current = requestAnimationFrame(animate); animationRef.current = requestAnimationFrame(animate)
} else { } else {
// Reset when animation completes // Reset when animation completes
map.setPaintProperty("district-extrusion", "fill-extrusion-height", [ map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
@ -160,36 +166,36 @@ export default function DistrictExtrusionLayer({
["has", "kode_kec"], ["has", "kode_kec"],
["match", ["get", "kode_kec"], "", 0, 0], ["match", ["get", "kode_kec"], "", 0, 0],
0, 0,
]); ])
map.setFilter("district-extrusion", ["==", ["get", "kode_kec"], ""]); map.setFilter("district-extrusion", ["==", ["get", "kode_kec"], ""])
lastFocusedDistrictRef.current = null; lastFocusedDistrictRef.current = null
// Ensure bearing is reset // Ensure bearing is reset
map.setBearing(0); map.setBearing(0)
} }
} catch (error) { } catch (error) {
console.error("Error animating extrusion down:", error); console.error("Error animating extrusion down:", error)
if (animationRef.current) { if (animationRef.current) {
cancelAnimationFrame(animationRef.current); cancelAnimationFrame(animationRef.current)
animationRef.current = null; animationRef.current = null
}
} }
} }
};
if (animationRef.current) { if (animationRef.current) {
cancelAnimationFrame(animationRef.current); cancelAnimationFrame(animationRef.current)
}
animationRef.current = requestAnimationFrame(animate)
} }
animationRef.current = requestAnimationFrame(animate);
};
animateHeightDown(); animateHeightDown()
return; return
} }
try { try {
// Update filter for the new district // Update filter for the new district
map.setFilter("district-extrusion", ["==", ["get", "kode_kec"], focusedDistrictId]); map.setFilter("district-extrusion", ["==", ["get", "kode_kec"], focusedDistrictId])
// Update the extrusion color // Update the extrusion color
map.setPaintProperty("district-extrusion", "fill-extrusion-color", [ map.setPaintProperty("district-extrusion", "fill-extrusion-color", [
@ -203,7 +209,7 @@ export default function DistrictExtrusionLayer({
"transparent", "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", [
@ -211,27 +217,27 @@ export default function DistrictExtrusionLayer({
["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)
} }
@ -253,7 +259,12 @@ 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
}
console.log("Animating extrusion for district:", focusedDistrictId)
if (animationRef.current) { if (animationRef.current) {
cancelAnimationFrame(animationRef.current) cancelAnimationFrame(animationRef.current)
@ -282,6 +293,7 @@ export default function DistrictExtrusionLayer({
if (progress < 1) { if (progress < 1) {
animationRef.current = requestAnimationFrame(animate) animationRef.current = requestAnimationFrame(animate)
} else { } else {
console.log("Extrusion animation complete, starting rotation")
// Start rotation after extrusion completes // Start rotation after extrusion completes
startRotation() startRotation()
} }

View File

@ -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,16 +20,27 @@ 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) => {
// First check if the click was on a marker or cluster
const incidentFeatures = map.queryRenderedFeatures(e.point, { const incidentFeatures = map.queryRenderedFeatures(e.point, {
layers: ["unclustered-point", "clusters"], layers: [
"unclustered-point",
"clusters",
"crime-points",
"units-points",
"incidents-points",
"timeline-markers",
"recent-incidents",
],
}) })
if (incidentFeatures && incidentFeatures.length > 0) { if (incidentFeatures && incidentFeatures.length > 0) {
// Click was on a marker or cluster, so don't process it as a district click
return return
} }
@ -164,7 +174,7 @@ export default function DistrictFillLineLayer({
const fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict) const fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict)
// Determine fill opacity based on active control // Determine fill opacity based on active control
const fillOpacity = getFillOpacity(activeControl, showFill); const fillOpacity = getFillOpacity(activeControl, showFill)
if (!map.getLayer("district-fill")) { if (!map.getLayer("district-fill")) {
map.addLayer( map.addLayer(
@ -215,8 +225,8 @@ export default function DistrictFillLineLayer({
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) {
@ -254,15 +264,15 @@ export default function DistrictFillLineLayer({
// 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)
} }
@ -273,18 +283,18 @@ export default function DistrictFillLineLayer({
// 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
} }

View File

@ -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({
@ -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")
} }
@ -246,6 +255,9 @@ export default function Layers({
return return
} }
// Set the marker interaction flag to prevent district selection
isInteractingWithMarker.current = true
const incidentId = customEvent.detail.id || customEvent.detail.incidentId || customEvent.detail.incident_id const incidentId = customEvent.detail.id || customEvent.detail.incidentId || customEvent.detail.incident_id
if (!incidentId) { if (!incidentId) {
@ -300,21 +312,29 @@ export default function Layers({
if (!foundIncident) { if (!foundIncident) {
console.error("Could not find incident with ID:", incidentId) console.error("Could not find incident with ID:", incidentId)
isInteractingWithMarker.current = false
return return
} }
if (!foundIncident.latitude || !foundIncident.longitude) { if (!foundIncident.latitude || !foundIncident.longitude) {
console.error("Found incident has invalid coordinates:", foundIncident) console.error("Found incident has invalid coordinates:", foundIncident)
isInteractingWithMarker.current = false
return return
} }
console.log("Setting selected incident:", foundIncident) console.log("Setting selected incident:", foundIncident)
// Clear district selection when showing an incident
setSelectedDistrict(null) setSelectedDistrict(null)
selectedDistrictRef.current = null selectedDistrictRef.current = null
setFocusedDistrictId(null) setFocusedDistrictId(null)
setSelectedIncident(foundIncident) 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)
@ -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
@ -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}
/> />
{/* Recent Incidents Layer (24 hours) */} {/* Always render the extrusion layer when a district is focused */}
<RecentIncidentsLayer {shouldShowExtrusion && (
visible={showRecentIncidents} <DistrictExtrusionLayer
visible={true}
map={mapboxMap} map={mapboxMap}
incidents={recentIncidents} tilesetId={tilesetId}
focusedDistrictId={focusedDistrictId}
crimeDataByDistrict={crimeDataByDistrict}
/> />
)}
{/* Recent Incidents Layer (24 hours) */}
<RecentIncidentsLayer visible={showRecentIncidents} map={mapboxMap} incidents={recentIncidents} />
<HeatmapLayer <HeatmapLayer
crimes={crimes} crimes={crimes}
@ -501,8 +573,7 @@ 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}
@ -512,37 +583,20 @@ export default function Layers({
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>
)} )}

View File

@ -9,37 +9,37 @@ 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
e.originalEvent.stopPropagation()
e.preventDefault()
const incidentDetails = { const incidentDetails = {
id: incident.properties.id, id: incident.properties.id,
@ -49,13 +49,13 @@ export default function RecentIncidentsLayer({
latitude: (incident.geometry as any).coordinates[1], latitude: (incident.geometry as any).coordinates[1],
timestamp: new Date(incident.properties.timestamp || Date.now()), timestamp: new Date(incident.properties.timestamp || Date.now()),
category: incident.properties.category, category: incident.properties.category,
}; }
console.log("Recent incident clicked:", incidentDetails); console.log("Recent incident clicked:", incidentDetails)
// Ensure markers stay visible // Ensure markers stay visible
if (map.getLayer("recent-incidents")) { if (map.getLayer("recent-incidents")) {
map.setLayoutProperty("recent-incidents", "visibility", "visible"); map.setLayoutProperty("recent-incidents", "visibility", "visible")
} }
// First fly to the incident location // First fly to the incident location
@ -65,34 +65,34 @@ export default function RecentIncidentsLayer({
bearing: 0, bearing: 0,
pitch: 45, pitch: 45,
duration: 2000, duration: 2000,
}); })
// Dispatch the incident_click event to show the popup // Dispatch the incident_click event to show the popup
const customEvent = new CustomEvent("incident_click", { const customEvent = new CustomEvent("incident_click", {
detail: incidentDetails, detail: incidentDetails,
bubbles: true, bubbles: true,
}); })
map.getCanvas().dispatchEvent(customEvent); map.getCanvas().dispatchEvent(customEvent)
document.dispatchEvent(customEvent); document.dispatchEvent(customEvent)
// Reset the flag after a delay // Reset the flag after a delay
setTimeout(() => { setTimeout(() => {
isInteractingWithMarker.current = false; isInteractingWithMarker.current = false
}, 5000); }, 5000)
}, },
[map] [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,
@ -111,34 +111,35 @@ export default function RecentIncidentsLayer({
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 { } else {
// If not, add source // If not, add source
map.addSource("recent-incidents-source", { map.addSource("recent-incidents-source", {
type: "geojson", type: "geojson",
data: recentData, data: recentData,
}); })
} }
// Find first symbol layer for proper layering // Find first symbol layer for proper layering
const layers = map.getStyle().layers; const layers = map.getStyle().layers
let firstSymbolId: string | undefined; let firstSymbolId: string | undefined
for (const layer of layers) { for (const layer of layers) {
if (layer.type === "symbol") { if (layer.type === "symbol") {
firstSymbolId = layer.id; firstSymbolId = layer.id
break; break
} }
} }
// Check if layer exists already // Check if layer exists already
if (!map.getLayer("recent-incidents")) { if (!map.getLayer("recent-incidents")) {
map.addLayer({ map.addLayer(
{
id: "recent-incidents", id: "recent-incidents",
type: "circle", type: "circle",
source: "recent-incidents-source", source: "recent-incidents-source",
@ -148,95 +149,89 @@ export default function RecentIncidentsLayer({
"interpolate", "interpolate",
["linear"], ["linear"],
["zoom"], ["zoom"],
7, 4, // Slightly larger at lower zooms for visibility 7,
12, 8, 4, // Slightly larger at lower zooms for visibility
15, 12, // Larger maximum size 12,
8,
15,
12, // Larger maximum size
], ],
"circle-stroke-width": 2, "circle-stroke-width": 2,
"circle-stroke-color": "#FFFFFF", "circle-stroke-color": "#FFFFFF",
"circle-opacity": 0.8, "circle-opacity": 0.8,
// Add a pulsing effect // Add a pulsing effect
"circle-stroke-opacity": [ "circle-stroke-opacity": ["interpolate", ["linear"], ["zoom"], 7, 0.5, 15, 0.8],
"interpolate",
["linear"],
["zoom"],
7, 0.5,
15, 0.8
],
}, },
layout: { layout: {
visibility: visible ? "visible" : "none", visibility: visible ? "visible" : "none",
} },
}, firstSymbolId); },
firstSymbolId,
)
// Add a glow effect with a larger circle behind // Add a glow effect with a larger circle behind
map.addLayer({ map.addLayer(
{
id: "recent-incidents-glow", id: "recent-incidents-glow",
type: "circle", type: "circle",
source: "recent-incidents-source", source: "recent-incidents-source",
paint: { paint: {
"circle-color": "#FF5252", "circle-color": "#FF5252",
"circle-radius": [ "circle-radius": ["interpolate", ["linear"], ["zoom"], 7, 6, 12, 12, 15, 18],
"interpolate",
["linear"],
["zoom"],
7, 6,
12, 12,
15, 18,
],
"circle-opacity": 0.2, "circle-opacity": 0.2,
"circle-blur": 1, "circle-blur": 1,
}, },
layout: { layout: {
visibility: visible ? "visible" : "none", visibility: visible ? "visible" : "none",
} },
}, "recent-incidents"); },
"recent-incidents",
)
// Add mouse events // Add mouse events
map.on("mouseenter", "recent-incidents", () => { map.on("mouseenter", "recent-incidents", () => {
map.getCanvas().style.cursor = "pointer"; map.getCanvas().style.cursor = "pointer"
}); })
map.on("mouseleave", "recent-incidents", () => { map.on("mouseleave", "recent-incidents", () => {
map.getCanvas().style.cursor = ""; map.getCanvas().style.cursor = ""
}); })
} else { } else {
// Update existing layer visibility // Update existing layer visibility
map.setLayoutProperty("recent-incidents", "visibility", visible ? "visible" : "none"); map.setLayoutProperty("recent-incidents", "visibility", visible ? "visible" : "none")
map.setLayoutProperty("recent-incidents-glow", "visibility", visible ? "visible" : "none"); map.setLayoutProperty("recent-incidents-glow", "visibility", visible ? "visible" : "none")
} }
// Ensure click handler is properly registered // Ensure click handler is properly registered
map.off("click", "recent-incidents", handleIncidentClick); map.off("click", "recent-incidents", handleIncidentClick)
map.on("click", "recent-incidents", handleIncidentClick); map.on("click", "recent-incidents", handleIncidentClick)
} catch (error) { } catch (error) {
console.error("Error setting up recent incidents layer:", error); console.error("Error setting up recent incidents layer:", error)
}
} }
};
// Check if style is loaded and set up layer accordingly // Check if style is loaded and set up layer accordingly
if (map.isStyleLoaded()) { if (map.isStyleLoaded()) {
setupLayerAndSource(); setupLayerAndSource()
} else { } else {
map.once("style.load", setupLayerAndSource); map.once("style.load", setupLayerAndSource)
// Fallback // Fallback
setTimeout(() => { setTimeout(() => {
if (map.isStyleLoaded()) { if (map.isStyleLoaded()) {
setupLayerAndSource(); setupLayerAndSource()
} else { } else {
console.warn("Map style still not loaded after timeout"); console.warn("Map style still not loaded after timeout")
} }
}, 1000); }, 1000)
} }
return () => { return () => {
if (map) { if (map) {
map.off("click", "recent-incidents", handleIncidentClick); map.off("click", "recent-incidents", handleIncidentClick)
} }
}; }
}, [map, visible, recentIncidents, handleIncidentClick]); }, [map, visible, recentIncidents, handleIncidentClick])
return null; return null
} }

View File

@ -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[]
@ -143,6 +142,10 @@ 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
// Stop event propagation
e.originalEvent.stopPropagation()
e.preventDefault()
const feature = e.features[0] const feature = e.features[0]
const props = feature.properties const props = feature.properties
if (!props) return if (!props) return
@ -290,8 +293,6 @@ export default function TimelineLayer({
district={selectedDistrict} district={selectedDistrict}
/> />
)} )}
</> </>
) )
} }

View File

@ -64,18 +64,17 @@ 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 { return {
type: "Feature" as const, type: "Feature" as const,
@ -91,13 +90,13 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible
geometry: { geometry: {
type: "Point" as const, type: "Point" as const,
coordinates: [ coordinates: [
parseFloat(String(unit.longitude)) || 0, Number.parseFloat(String(unit.longitude)) || 0,
parseFloat(String(unit.latitude)) || 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 = () => {

View File

@ -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