681 lines
25 KiB
TypeScript
681 lines
25 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useRef, useEffect, useCallback } from "react"
|
|
import { useMap } from "react-map-gl/mapbox"
|
|
import { BASE_BEARING, BASE_DURATION, BASE_PITCH, BASE_ZOOM, MAPBOX_TILESET_ID } from "@/app/_utils/const/map"
|
|
import DistrictPopup from "../pop-up/district-popup"
|
|
import DistrictExtrusionLayer from "./district-extrusion-layer"
|
|
import ClusterLayer from "./cluster-layer"
|
|
import HeatmapLayer from "./heatmap-layer"
|
|
import TimelineLayer from "./timeline-layer"
|
|
|
|
import type { ICrimes, IIncidentLogs } from "@/app/_utils/types/crimes"
|
|
import type { IDistrictFeature } from "@/app/_utils/types/map"
|
|
import { createFillColorExpression, processCrimeDataByDistrict } from "@/app/_utils/map"
|
|
import UnclusteredPointLayer from "./uncluster-layer"
|
|
|
|
import { toast } from "sonner"
|
|
import type { ITooltipsControl } from "../controls/top/tooltips"
|
|
import type { IUnits } from "@/app/_utils/types/units"
|
|
import UnitsLayer from "./units-layer"
|
|
import DistrictFillLineLayer from "./district-layer"
|
|
import CrimePopup from "../pop-up/crime-popup"
|
|
import TimezoneLayer from "./timezone"
|
|
import FaultLinesLayer from "./fault-lines"
|
|
import EWSAlertLayer from "./ews-alert-layer"
|
|
import PanicButtonDemo from "../controls/panic-button-demo"
|
|
|
|
import type { IIncidentLog } from "@/app/_utils/types/ews"
|
|
import { addMockIncident, getAllIncidents, resolveIncident } from "@/app/_utils/mock/ews-data"
|
|
import RecentIncidentsLayer from "./recent-incidents-layer"
|
|
|
|
// Interface for crime incident
|
|
interface ICrimeIncident {
|
|
id: string
|
|
district?: string
|
|
category?: string
|
|
type_category?: string | null
|
|
description?: string
|
|
status: string
|
|
address?: string | null
|
|
timestamp?: Date
|
|
latitude?: number
|
|
longitude?: number
|
|
}
|
|
|
|
// District layer props
|
|
export interface IDistrictLayerProps {
|
|
visible?: boolean
|
|
onClick?: (feature: IDistrictFeature) => void
|
|
onDistrictClick?: (feature: IDistrictFeature) => void
|
|
map?: any
|
|
year: string
|
|
month: string
|
|
filterCategory: string | "all"
|
|
crimes: ICrimes[]
|
|
units?: IUnits[]
|
|
tilesetId?: string
|
|
focusedDistrictId?: string | null
|
|
setFocusedDistrictId?: (id: string | null) => void
|
|
crimeDataByDistrict?: Record<string, any>
|
|
showFill?: boolean
|
|
activeControl?: ITooltipsControl
|
|
}
|
|
|
|
interface LayersProps {
|
|
visible?: boolean
|
|
crimes: ICrimes[]
|
|
units?: IUnits[]
|
|
recentIncidents: IIncidentLogs[]
|
|
year: string
|
|
month: string
|
|
filterCategory: string | "all"
|
|
activeControl: ITooltipsControl
|
|
tilesetId?: string
|
|
useAllData?: boolean
|
|
showEWS?: boolean
|
|
sourceType?: string
|
|
}
|
|
|
|
export default function Layers({
|
|
visible = true,
|
|
crimes,
|
|
recentIncidents,
|
|
units,
|
|
year,
|
|
month,
|
|
filterCategory,
|
|
activeControl,
|
|
tilesetId = MAPBOX_TILESET_ID,
|
|
useAllData = false,
|
|
showEWS = true,
|
|
sourceType = "cbt",
|
|
}: LayersProps) {
|
|
const animationRef = useRef<number | null>(null)
|
|
const { current: map } = useMap()
|
|
|
|
if (!map) {
|
|
toast.error("Map not found")
|
|
return null
|
|
}
|
|
|
|
const mapboxMap = map.getMap()
|
|
|
|
const [selectedDistrict, setSelectedDistrict] = useState<IDistrictFeature | null>(null)
|
|
const [selectedIncident, setSelectedIncident] = useState<ICrimeIncident | null>(null)
|
|
const [focusedDistrictId, setFocusedDistrictId] = useState<string | 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 [ewsIncidents, setEwsIncidents] = useState<IIncidentLog[]>([])
|
|
const [showPanicDemo, setShowPanicDemo] = useState(true)
|
|
const [displayPanicDemo, setDisplayPanicDemo] = useState(showEWS && showPanicDemo)
|
|
|
|
useEffect(() => {
|
|
setEwsIncidents(getAllIncidents())
|
|
}, [])
|
|
|
|
const handleTriggerAlert = useCallback((priority: "high" | "medium" | "low") => {
|
|
const newIncident = addMockIncident({ priority })
|
|
setEwsIncidents(getAllIncidents())
|
|
}, [])
|
|
|
|
const handleResolveIncident = useCallback((id: string) => {
|
|
resolveIncident(id)
|
|
setEwsIncidents(getAllIncidents())
|
|
}, [])
|
|
|
|
const handleResolveAllAlerts = useCallback(() => {
|
|
ewsIncidents.forEach((incident) => {
|
|
if (incident.status === "active") {
|
|
resolveIncident(incident.id)
|
|
}
|
|
})
|
|
setEwsIncidents(getAllIncidents())
|
|
}, [ewsIncidents])
|
|
|
|
const handlePopupClose = useCallback(() => {
|
|
selectedDistrictRef.current = null
|
|
setSelectedDistrict(null)
|
|
setSelectedIncident(null)
|
|
setFocusedDistrictId(null)
|
|
isInteractingWithMarker.current = false
|
|
|
|
if (map) {
|
|
map.easeTo({
|
|
zoom: BASE_ZOOM,
|
|
pitch: BASE_PITCH,
|
|
bearing: BASE_BEARING,
|
|
duration: BASE_DURATION,
|
|
easing: (t) => t * (2 - t),
|
|
})
|
|
|
|
if (map.getLayer("clusters")) {
|
|
map.getMap().setLayoutProperty("clusters", "visibility", "visible")
|
|
}
|
|
if (map.getLayer("unclustered-point")) {
|
|
map.getMap().setLayoutProperty("unclustered-point", "visibility", "visible")
|
|
}
|
|
|
|
if (map.getLayer("district-fill")) {
|
|
const fillColorExpression = createFillColorExpression(null, crimeDataByDistrict)
|
|
map.getMap().setPaintProperty("district-fill", "fill-color", fillColorExpression as any)
|
|
}
|
|
}
|
|
}, [map, crimeDataByDistrict])
|
|
|
|
|
|
const animateExtrusionDown = () => {
|
|
if (!map || !map.getLayer("district-extrusion") || !focusedDistrictId) {
|
|
return
|
|
}
|
|
|
|
if (animationRef.current) {
|
|
cancelAnimationFrame(animationRef.current)
|
|
animationRef.current = null
|
|
}
|
|
|
|
// Get the current height from the layer (default to 800 if not found)
|
|
let currentHeight = 800
|
|
|
|
try {
|
|
const paint = map.getPaintProperty("district-extrusion", "fill-extrusion-height")
|
|
if (Array.isArray(paint) && paint.length > 0) {
|
|
// Try to extract the current height from the expression
|
|
const idx = paint.findIndex((v) => v === focusedDistrictId)
|
|
if (idx !== -1 && typeof paint[idx + 1] === "number") {
|
|
currentHeight = paint[idx + 1]
|
|
}
|
|
}
|
|
} catch {
|
|
// fallback to default
|
|
}
|
|
|
|
const startHeight = currentHeight
|
|
const targetHeight = 0
|
|
const duration = 700
|
|
const startTime = performance.now()
|
|
|
|
const animate = (currentTime: number) => {
|
|
const elapsed = currentTime - startTime
|
|
const progress = Math.min(elapsed / duration, 1)
|
|
const easedProgress = progress * (2 - progress)
|
|
const newHeight = startHeight + (targetHeight - startHeight) * easedProgress
|
|
|
|
try {
|
|
map.getMap().setPaintProperty("district-extrusion", "fill-extrusion-height", [
|
|
"case",
|
|
["has", "kode_kec"],
|
|
["match", ["get", "kode_kec"], focusedDistrictId, newHeight, 0],
|
|
0,
|
|
])
|
|
|
|
if (progress < 1) {
|
|
animationRef.current = requestAnimationFrame(animate)
|
|
} else {
|
|
animationRef.current = null
|
|
}
|
|
|
|
} catch (error) {
|
|
if (animationRef.current) {
|
|
cancelAnimationFrame(animationRef.current)
|
|
animationRef.current = null
|
|
}
|
|
}
|
|
}
|
|
|
|
animationRef.current = requestAnimationFrame(animate)
|
|
}
|
|
|
|
const handleCloseDistrictPopup = useCallback(() => {
|
|
console.log("Closing district popup")
|
|
|
|
animateExtrusionDown()
|
|
handlePopupClose()
|
|
}, [handlePopupClose, animateExtrusionDown])
|
|
|
|
const handleCloseIncidentPopup = useCallback(() => {
|
|
console.log("Closing incident popup")
|
|
handlePopupClose()
|
|
}, [handlePopupClose])
|
|
|
|
const handleDistrictClick = useCallback(
|
|
(feature: IDistrictFeature) => {
|
|
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)
|
|
|
|
// Set the district as selected
|
|
setSelectedDistrict(feature)
|
|
selectedDistrictRef.current = feature
|
|
setFocusedDistrictId(feature.id)
|
|
|
|
if (map && feature.longitude && feature.latitude) {
|
|
map.flyTo({
|
|
center: [feature.longitude, feature.latitude],
|
|
zoom: 12.5,
|
|
pitch: 60,
|
|
bearing: 0,
|
|
duration: BASE_DURATION,
|
|
easing: (t) => t * (2 - t),
|
|
})
|
|
|
|
// Hide clusters when focusing on a district
|
|
if (map.getLayer("clusters")) {
|
|
map.getMap().setLayoutProperty("clusters", "visibility", "none")
|
|
}
|
|
if (map.getLayer("unclustered-point")) {
|
|
map.getMap().setLayoutProperty("unclustered-point", "visibility", "none")
|
|
}
|
|
}
|
|
},
|
|
[map],
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (!mapboxMap) return
|
|
|
|
const handleFlyToEvent = (e: Event) => {
|
|
const customEvent = e as CustomEvent
|
|
if (!map || !customEvent.detail) return
|
|
|
|
const { longitude, latitude, zoom, bearing, pitch, duration } = customEvent.detail
|
|
|
|
map.flyTo({
|
|
center: [longitude, latitude],
|
|
zoom: zoom || 15,
|
|
bearing: bearing || 0,
|
|
pitch: pitch || 45,
|
|
duration: duration || 2000,
|
|
})
|
|
}
|
|
|
|
mapboxMap.getCanvas().addEventListener("mapbox_fly_to", handleFlyToEvent as EventListener)
|
|
|
|
return () => {
|
|
if (mapboxMap && mapboxMap.getCanvas()) {
|
|
mapboxMap.getCanvas().removeEventListener("mapbox_fly_to", handleFlyToEvent as EventListener)
|
|
}
|
|
}
|
|
}, [mapboxMap, map])
|
|
|
|
useEffect(() => {
|
|
if (!mapboxMap) return
|
|
|
|
const handleIncidentClickEvent = (e: Event) => {
|
|
const customEvent = e as CustomEvent
|
|
console.log("Received incident_click event in layers:", customEvent.detail)
|
|
|
|
if (!customEvent.detail) {
|
|
console.error("Empty incident click event data")
|
|
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
|
|
|
|
if (!incidentId) {
|
|
console.error("No incident ID found in event data:", customEvent.detail)
|
|
return
|
|
}
|
|
|
|
console.log("Looking for incident with ID:", incidentId)
|
|
|
|
let foundIncident: ICrimeIncident | undefined
|
|
|
|
if (
|
|
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) {
|
|
console.error("Could not find incident with ID:", incidentId)
|
|
isInteractingWithMarker.current = false
|
|
return
|
|
}
|
|
|
|
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)
|
|
document.addEventListener("incident_click", handleIncidentClickEvent as EventListener)
|
|
|
|
console.log("Set up incident click event listener")
|
|
|
|
return () => {
|
|
console.log("Removing incident click event listener")
|
|
if (mapboxMap && mapboxMap.getCanvas()) {
|
|
mapboxMap.getCanvas().removeEventListener("incident_click", handleIncidentClickEvent as EventListener)
|
|
}
|
|
document.removeEventListener("incident_click", handleIncidentClickEvent as EventListener)
|
|
}
|
|
}, [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(() => {
|
|
if (selectedDistrictRef.current) {
|
|
const districtId = selectedDistrictRef.current.id
|
|
const districtCrime = crimes.find((crime) => crime.district_id === districtId)
|
|
|
|
if (districtCrime) {
|
|
const selectedYearNum = year ? Number.parseInt(year) : new Date().getFullYear()
|
|
|
|
let demographics = districtCrime.districts.demographics?.find((d) => d.year === selectedYearNum)
|
|
|
|
if (!demographics && districtCrime.districts.demographics?.length) {
|
|
demographics = districtCrime.districts.demographics.sort((a, b) => b.year - a.year)[0]
|
|
}
|
|
|
|
let geographics = districtCrime.districts.geographics?.find((g) => g.year === selectedYearNum)
|
|
|
|
if (!geographics && districtCrime.districts.geographics?.length) {
|
|
const validGeographics = districtCrime.districts.geographics
|
|
.filter((g) => g.year !== null)
|
|
.sort((a, b) => (b.year || 0) - (a.year || 0))
|
|
|
|
geographics = validGeographics.length > 0 ? validGeographics[0] : districtCrime.districts.geographics[0]
|
|
}
|
|
|
|
if (!demographics || !geographics) {
|
|
console.error("Missing district data:", { demographics, geographics })
|
|
return
|
|
}
|
|
|
|
const crime_incidents = districtCrime.crime_incidents
|
|
.filter((incident) => filterCategory === "all" || incident.crime_categories.name === filterCategory)
|
|
.map((incident) => ({
|
|
id: incident.id,
|
|
timestamp: incident.timestamp,
|
|
description: incident.description,
|
|
status: incident.status || "",
|
|
category: incident.crime_categories.name,
|
|
type: incident.crime_categories.type || "",
|
|
address: incident.locations.address || "",
|
|
latitude: incident.locations.latitude,
|
|
longitude: incident.locations.longitude,
|
|
}))
|
|
|
|
const updatedDistrict: IDistrictFeature = {
|
|
...selectedDistrictRef.current,
|
|
number_of_crime: crimeDataByDistrict[districtId]?.number_of_crime || 0,
|
|
level: crimeDataByDistrict[districtId]?.level || selectedDistrictRef.current.level,
|
|
demographics: {
|
|
number_of_unemployed: demographics.number_of_unemployed,
|
|
population: demographics.population,
|
|
population_density: demographics.population_density,
|
|
year: demographics.year,
|
|
},
|
|
geographics: {
|
|
address: geographics.address || "",
|
|
land_area: geographics.land_area || 0,
|
|
year: geographics.year || 0,
|
|
latitude: geographics.latitude,
|
|
longitude: geographics.longitude,
|
|
},
|
|
crime_incidents,
|
|
selectedYear: year,
|
|
selectedMonth: month,
|
|
}
|
|
|
|
selectedDistrictRef.current = updatedDistrict
|
|
|
|
setSelectedDistrict((prevDistrict) => {
|
|
if (
|
|
prevDistrict?.id === updatedDistrict.id &&
|
|
prevDistrict?.selectedYear === updatedDistrict.selectedYear &&
|
|
prevDistrict?.selectedMonth === updatedDistrict.selectedMonth
|
|
) {
|
|
return prevDistrict
|
|
}
|
|
return updatedDistrict
|
|
})
|
|
}
|
|
}
|
|
}, [crimes, filterCategory, year, month, crimeDataByDistrict])
|
|
|
|
const handleSetFocusedDistrictId = useCallback((id: string | null, isMarkerClick = false) => {
|
|
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)
|
|
}, [])
|
|
|
|
const crimesVisible = activeControl === "incidents"
|
|
const showHeatmapLayer = activeControl === "heatmap" && sourceType !== "cbu"
|
|
const showUnitsLayer = activeControl === "units"
|
|
const showTimelineLayer = activeControl === "timeline"
|
|
const showHistoricalLayer = activeControl === "historical"
|
|
const showRecentIncidents = activeControl === "recents"
|
|
const showDistrictFill =
|
|
activeControl === "incidents" ||
|
|
activeControl === "clusters" ||
|
|
activeControl === "historical" ||
|
|
activeControl === "recents"
|
|
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 (
|
|
<>
|
|
<DistrictFillLineLayer
|
|
visible={true}
|
|
map={mapboxMap}
|
|
year={year}
|
|
month={month}
|
|
filterCategory={filterCategory}
|
|
crimes={crimes}
|
|
tilesetId={tilesetId}
|
|
focusedDistrictId={focusedDistrictId}
|
|
setFocusedDistrictId={handleSetFocusedDistrictId}
|
|
crimeDataByDistrict={crimeDataByDistrict}
|
|
showFill={showDistrictFill}
|
|
activeControl={activeControl}
|
|
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) */}
|
|
<RecentIncidentsLayer visible={showRecentIncidents} map={mapboxMap} incidents={recentIncidents} />
|
|
|
|
<HeatmapLayer
|
|
crimes={crimes}
|
|
year={year}
|
|
month={month}
|
|
filterCategory={filterCategory}
|
|
visible={showHeatmapLayer}
|
|
useAllData={useAllData}
|
|
enableInteractions={true}
|
|
setFocusedDistrictId={handleSetFocusedDistrictId}
|
|
/>
|
|
|
|
<TimelineLayer
|
|
crimes={crimes}
|
|
year={year}
|
|
month={month}
|
|
filterCategory={filterCategory}
|
|
visible={showTimelineLayer}
|
|
map={mapboxMap}
|
|
useAllData={useAllData}
|
|
/>
|
|
|
|
<UnitsLayer
|
|
crimes={crimes}
|
|
units={units}
|
|
filterCategory={filterCategory}
|
|
visible={showUnitsLayer}
|
|
map={mapboxMap}
|
|
/>
|
|
|
|
<ClusterLayer
|
|
visible={visible && activeControl === "clusters"}
|
|
map={mapboxMap}
|
|
crimes={crimes}
|
|
filterCategory={filterCategory}
|
|
focusedDistrictId={focusedDistrictId}
|
|
clusteringEnabled={activeControl === "clusters"}
|
|
showClusters={activeControl === "clusters"}
|
|
sourceType={sourceType}
|
|
/>
|
|
|
|
<UnclusteredPointLayer
|
|
visible={visible && showIncidentMarkers && !focusedDistrictId}
|
|
map={mapboxMap}
|
|
crimes={crimes}
|
|
filterCategory={filterCategory}
|
|
focusedDistrictId={focusedDistrictId}
|
|
/>
|
|
|
|
{selectedDistrict && !selectedIncident && !isInteractingWithMarker.current && (
|
|
<DistrictPopup
|
|
longitude={selectedDistrict.longitude || 0}
|
|
latitude={selectedDistrict.latitude || 0}
|
|
onClose={handleCloseDistrictPopup}
|
|
district={selectedDistrict}
|
|
year={year}
|
|
month={month}
|
|
filterCategory={filterCategory}
|
|
/>
|
|
)}
|
|
|
|
<TimezoneLayer map={mapboxMap} />
|
|
|
|
<FaultLinesLayer map={mapboxMap} />
|
|
|
|
{showEWS && <EWSAlertLayer map={mapboxMap} incidents={ewsIncidents} onIncidentResolved={handleResolveIncident} />}
|
|
|
|
{showEWS && displayPanicDemo && (
|
|
<div className="absolute top-0 right-20 z-50 p-2">
|
|
<PanicButtonDemo
|
|
onTriggerAlert={handleTriggerAlert}
|
|
onResolveAllAlerts={handleResolveAllAlerts}
|
|
activeIncidents={ewsIncidents.filter((inc) => inc.status === "active")}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{selectedIncident && selectedIncident.latitude && selectedIncident.longitude && (
|
|
<CrimePopup
|
|
longitude={selectedIncident.longitude}
|
|
latitude={selectedIncident.latitude}
|
|
onClose={handleCloseIncidentPopup}
|
|
incident={selectedIncident}
|
|
/>
|
|
)}
|
|
</>
|
|
)
|
|
}
|