"use client" import { useState, useRef, useEffect } 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 DistrictLayer from "./district-layer-old" import type { ICrimes } from "@/app/_utils/types/crimes" import { IDistrictFeature } from "@/app/_utils/types/map" import { createFillColorExpression, processCrimeDataByDistrict } from "@/app/_utils/map" import UnclusteredPointLayer from "./uncluster-layer" import FlyToHandler from "../fly-to" import { toast } from "sonner" import { ITooltips } from "../controls/top/tooltips" // District layer props export interface IDistrictLayerProps { visible?: boolean onClick?: (feature: IDistrictFeature) => void year: string month: string filterCategory: string | "all" crimes: ICrimes[] tilesetId?: string } interface LayersProps { visible?: boolean; crimes: ICrimes[]; year: string; month: string; filterCategory: string | "all"; activeControl: ITooltips; tilesetId?: string; } export default function Layers({ visible = true, crimes, year, month, filterCategory, activeControl, tilesetId = MAPBOX_TILESET_ID, }: LayersProps) { const { current: map } = useMap() if (!map) { toast.error("Map not found") return null } const mapboxMap = map.getMap() const [selectedDistrict, setSelectedDistrict] = useState(null) const [focusedDistrictId, setFocusedDistrictId] = useState(null) const selectedDistrictRef = useRef(null) const crimeDataByDistrict = processCrimeDataByDistrict(crimes) // Set up custom event handler for cluster clicks to ensure it works across components useEffect(() => { if (!mapboxMap) return; const handleClusterClickEvent = (e: CustomEvent) => { if (!e.detail) return; const { center, zoom } = e.detail; if (center && zoom) { mapboxMap.flyTo({ center: center, zoom: zoom, duration: 1000, easing: (t) => t * (2 - t) }); } }; mapboxMap.getCanvas().addEventListener('cluster_click', handleClusterClickEvent as EventListener); return () => { mapboxMap.getCanvas().removeEventListener('cluster_click', handleClusterClickEvent as EventListener); }; }, [mapboxMap]); // Handle popup close const handleCloseDistrictPopup = () => { console.log("Closing district popup") selectedDistrictRef.current = null setSelectedDistrict(null) setFocusedDistrictId(null) // Reset pitch and bearing if (map) { map.easeTo({ zoom: BASE_ZOOM, pitch: BASE_PITCH, bearing: BASE_BEARING, duration: 1500, easing: (t) => t * (2 - t), // easeOutQuad }) // Show all clusters again when closing popup if (map.getLayer("clusters")) { map.getMap().setLayoutProperty("clusters", "visibility", "visible") } if (map.getLayer("unclustered-point")) { map.getMap().setLayoutProperty("unclustered-point", "visibility", "visible") } // Explicitly update fill color for all districts if (map.getLayer("district-fill")) { const fillColorExpression = createFillColorExpression(null, crimeDataByDistrict) map.getMap().setPaintProperty("district-fill", "fill-color", fillColorExpression as any) } } } // Update selected district when year/month/filter changes 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]) if (!visible) return null // Determine which layers should be visible based on the active control const showDistrictLayer = activeControl === "incidents"; const showHeatmapLayer = activeControl === "heatmap"; const showClustersLayer = activeControl === "clusters"; return ( <> {/* Standard District Layer with incident points */} {/* Heatmap Layer */} {/* District base layer is always needed */} {/* Cluster Layer - only enable clustering and make visible when the clusters control is active */} {/* Unclustered Points Layer - hide when in cluster mode */} {selectedDistrict && ( )} ) }