"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { useMap } from "react-map-gl/mapbox"; import { BASE_BEARING, BASE_DURATION, BASE_PITCH, BASE_ZOOM, MAPBOX_TILESET_ID, PITCH_3D, ZOOM_3D, } 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 { ICrimeSourceTypes, IDistrictFeature } from "@/app/_utils/types/map"; import { createFillColorExpression, getCrimeRateColor, processCrimeDataByDistrict, } from "@/app/_utils/map/common"; 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 TimezoneLayer from "./timezone"; import FaultLinesLayer from "./fault-lines"; import RecentIncidentsLayer from "./recent-incidents-layer"; import IncidentPopup from "../pop-up/incident-popup"; import { manageLayerVisibility } from "@/app/_utils/map/layer-visibility"; import AllIncidentsLayer from "./all-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?: ICrimeSourceTypes; } 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(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( 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 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, ], ); map.getMap().setPaintProperty( "district-extrusion", "fill-extrusion-color", [ "case", ["has", "kode_kec"], [ "match", ["get", "kode_kec"], focusedDistrictId || "", "transparent", "transparent", ], "transparent", ], ); 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(() => { animateExtrusionDown(); handlePopupClose(); }, [handlePopupClose, animateExtrusionDown]); const handleDistrictClick = useCallback( (feature: IDistrictFeature) => { if (isInteractingWithMarker.current) { return; } setSelectedIncident(null); setSelectedDistrict(feature); selectedDistrictRef.current = feature; setFocusedDistrictId(feature.id); if (map && feature.longitude && feature.latitude) { map.flyTo({ center: [feature.longitude, feature.latitude], zoom: ZOOM_3D, pitch: PITCH_3D, bearing: BASE_BEARING, duration: BASE_DURATION, easing: (t) => t * (2 - t), }); 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 (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) => { if (isMarkerClick) { isInteractingWithMarker.current = true; setTimeout(() => { isInteractingWithMarker.current = false; }, 1000); } setFocusedDistrictId(id); }, [], ); const showHeatmapLayer = activeControl === "heatmap" && sourceType !== "cbu"; const showUnitsLayer = activeControl === "units"; const showTimelineLayer = activeControl === "timeline"; const showRecentIncidents = activeControl === "recents"; const showAllIncidents = activeControl === "incidents"; // Use the "incidents" tooltip for all incidents const showDistrictFill = activeControl === "clusters"; const showIncidentMarkers = activeControl !== "heatmap" && activeControl !== "timeline" && sourceType !== "cbu"; const shouldShowExtrusion = focusedDistrictId !== null && !isInteractingWithMarker.current; useEffect(() => { if (!mapboxMap) return; const recentLayerIds = [ "very-recent-incidents-pulse", "recent-incidents-glow", "recent-incidents", ]; const timelineLayerIds = ["timeline-markers-bg", "timeline-markers"]; const heatmapLayerIds = ["heatmap-layer"]; const unitsLayerIds = [ "units-points", "incidents-points", "units-labels", "units-connection-lines", ]; const clusterLayerIds = [ "clusters", "cluster-count", "crime-points", "crime-count-labels", ]; const allIncidentsLayerIds = [ "all-incidents-pulse", "all-incidents-circles", "all-incidents", ]; if (activeControl !== "recents") { manageLayerVisibility(mapboxMap, recentLayerIds, false); } if (activeControl !== "timeline") { manageLayerVisibility(mapboxMap, timelineLayerIds, false); } if (activeControl !== "heatmap") { manageLayerVisibility(mapboxMap, heatmapLayerIds, false); } if (activeControl !== "units") { manageLayerVisibility(mapboxMap, unitsLayerIds, false); } if (activeControl !== "clusters") { manageLayerVisibility(mapboxMap, clusterLayerIds, false); } if (activeControl !== "incidents") { manageLayerVisibility(mapboxMap, allIncidentsLayerIds, false); } }, [activeControl, mapboxMap]); return ( <> {shouldShowExtrusion && ( )} {selectedDistrict && !selectedIncident && !isInteractingWithMarker.current && ( )} ); }