"use client" import { useState, useRef, useEffect, useCallback } from "react" import { useMap } from "react-map-gl/mapbox" import { BASE_BEARING, 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 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 { current: map } = useMap() if (!map) { toast.error("Map not found") return null } const mapboxMap = map.getMap() const [selectedDistrict, setSelectedDistrict] = useState(null) const [selectedIncident, setSelectedIncident] = useState(null) const [focusedDistrictId, setFocusedDistrictId] = useState(null) const selectedDistrictRef = useRef(null) // Track if we're currently interacting with a marker to prevent district selection const isInteractingWithMarker = useRef(false) const crimeDataByDistrict = processCrimeDataByDistrict(crimes) const [ewsIncidents, setEwsIncidents] = useState([]) 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: 1500, 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 handleCloseDistrictPopup = useCallback(() => { console.log("Closing district popup") handlePopupClose() }, [handlePopupClose]) 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, pitch: 45, bearing: 0, duration: 1500, 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 ( <> {/* Always render the extrusion layer when a district is focused */} {shouldShowExtrusion && ( )} {/* Recent Incidents Layer (24 hours) */} {selectedDistrict && !selectedIncident && !isInteractingWithMarker.current && ( )} {showEWS && } {showEWS && displayPanicDemo && (
inc.status === "active")} />
)} {selectedIncident && selectedIncident.latitude && selectedIncident.longitude && ( )} ) }