"use client" import { useEffect, useMemo, useRef, useState, useCallback } from "react" import { Layer, Source } from "react-map-gl/mapbox" import type { ICrimes } from "@/app/_utils/types/crimes" import type { IUnits } from "@/app/_utils/types/units" import type mapboxgl from "mapbox-gl" import { generateCategoryColorMap } from "@/app/_utils/colors" import UnitPopup from "../pop-up/unit-popup" import IncidentPopup from "../pop-up/incident-popup" interface UnitsLayerProps { crimes: ICrimes[] units?: IUnits[] filterCategory: string | "all" visible?: boolean map?: mapboxgl.Map | null } export default function UnitsLayer({ crimes, units = [], filterCategory, visible = false, map }: UnitsLayerProps) { const [loadedUnits, setLoadedUnits] = useState([]) const loadedUnitsRef = useRef([]) // For popups const [selectedUnit, setSelectedUnit] = useState(null) const [selectedIncident, setSelectedIncident] = useState(null) const [selectedEntityId, setSelectedEntityId] = useState() const [isUnitSelected, setIsUnitSelected] = useState(false) const [selectedDistrictId, setSelectedDistrictId] = useState() // Use either provided units or loaded units const unitsData = useMemo(() => { return units.length > 0 ? units : loadedUnits || [] }, [units, loadedUnits]) // Extract all unique crime categories for color generation const uniqueCategories = useMemo(() => { const categories = new Set() crimes.forEach((crime) => { crime.crime_incidents.forEach((incident) => { if (incident.crime_categories?.name) { categories.add(incident.crime_categories.name) } }) }) return Array.from(categories) }, [crimes]) // Generate color map for all categories const categoryColorMap = useMemo(() => { return generateCategoryColorMap(uniqueCategories) }, [uniqueCategories]) // Process units data to GeoJSON format const unitsGeoJSON = useMemo(() => { console.log("Units data being processed:", unitsData); // Debug log return { type: "FeatureCollection" as const, features: unitsData .map((unit) => { // Debug log for individual units console.log("Processing unit:", unit.code_unit, unit.name, { longitude: unit.longitude, latitude: unit.latitude, district: unit.district_name }); return { type: "Feature" as const, properties: { id: unit.code_unit, name: unit.name, address: unit.address, phone: unit.phone, type: unit.type, district: unit.district_name || "", district_id: unit.district_id, }, geometry: { type: "Point" as const, coordinates: [ parseFloat(String(unit.longitude)) || 0, parseFloat(String(unit.latitude)) || 0 ], }, }; }) }; }, [unitsData]) // Process incident data to GeoJSON format const incidentsGeoJSON = useMemo(() => { const features: any[] = [] crimes.forEach((crime) => { crime.crime_incidents.forEach((incident) => { // Skip incidents without location data or filtered by category if ( !incident.locations?.latitude || !incident.locations?.longitude || (filterCategory !== "all" && incident.crime_categories.name !== filterCategory) ) return features.push({ type: "Feature" as const, properties: { id: incident.id, description: incident.description || "No description", category: incident.crime_categories.name, date: incident.timestamp, district: crime.districts.name, district_id: crime.district_id, categoryColor: categoryColorMap[incident.crime_categories.name] || "#22c55e", distance_to_unit: incident.locations.distance_to_unit || "Unknown", }, geometry: { type: "Point" as const, coordinates: [incident.locations.longitude, incident.locations.latitude], }, }) }) }) return { type: "FeatureCollection" as const, features, } }, [crimes, filterCategory, categoryColorMap]) // Create lines between units and incidents within their districts const connectionLinesGeoJSON = useMemo(() => { if (!unitsData.length || !crimes.length) return { type: "FeatureCollection" as const, features: [], } // Map district IDs to their units const districtUnitsMap = new Map() unitsData.forEach((unit) => { if (!unit.district_id || !unit.longitude || !unit.latitude) return if (!districtUnitsMap.has(unit.district_id)) { districtUnitsMap.set(unit.district_id, []) } districtUnitsMap.get(unit.district_id)!.push(unit) }) // Create lines from units to incidents in their district const lineFeatures: any[] = [] crimes.forEach((crime) => { // Get all units in this district const districtUnits = districtUnitsMap.get(crime.district_id) || [] if (!districtUnits.length) return // For each incident in this district crime.crime_incidents.forEach((incident) => { // Skip incidents without location data or filtered by category if ( !incident.locations?.latitude || !incident.locations?.longitude || (filterCategory !== "all" && incident.crime_categories.name !== filterCategory) ) return // Create a line from each unit in this district to this incident districtUnits.forEach((unit) => { if (!unit.longitude || !unit.latitude) return lineFeatures.push({ type: "Feature" as const, properties: { unit_id: unit.code_unit, unit_name: unit.name, incident_id: incident.id, district_id: crime.district_id, district_name: crime.districts.name, category: incident.crime_categories.name, lineColor: categoryColorMap[incident.crime_categories.name] || "#22c55e", }, geometry: { type: "LineString" as const, coordinates: [ [unit.longitude, unit.latitude], [incident.locations.longitude, incident.locations.latitude], ], }, }) }) }) }) return { type: "FeatureCollection" as const, features: lineFeatures, } }, [unitsData, crimes, filterCategory, categoryColorMap]) // Handle unit click const handleUnitClick = useCallback( ( map: mapboxgl.Map, unitsData: IUnits[], setSelectedUnit: (unit: IUnits | null) => void, setSelectedIncident: (incident: any | null) => void, setSelectedEntityId: (id: string | undefined) => void, setIsUnitSelected: (isSelected: boolean) => void, setSelectedDistrictId: (id: string | undefined) => void, ) => (e: mapboxgl.MapMouseEvent & { features?: mapboxgl.MapboxGeoJSONFeature[] }) => { if (!e.features || e.features.length === 0) return e.originalEvent.stopPropagation() e.preventDefault() const feature = e.features[0] const properties = feature.properties if (!properties) return // Find the unit in our data const unit = unitsData.find((u) => u.code_unit === properties.id) if (!unit) { console.log("Unit not found in data:", properties.id); return; } // Fly to the unit location map.flyTo({ center: [unit.longitude || 0, unit.latitude || 0], zoom: 14, pitch: 45, bearing: 0, duration: 2000, }) // Set the selected unit and query parameters setSelectedUnit(unit) setSelectedIncident(null) // Clear any selected incident setSelectedEntityId(properties.id) setIsUnitSelected(true) setSelectedDistrictId(properties.district_id) // Highlight the connected lines for this unit if (map.getLayer("units-connection-lines")) { map.setFilter("units-connection-lines", ["==", ["get", "unit_id"], properties.id]) } // Dispatch a custom event for other components to react to const customEvent = new CustomEvent("unit_click", { detail: { unitId: properties.id, districtId: properties.district_id, name: properties.name, longitude: feature.geometry.type === "Point" ? (feature.geometry as any).coordinates[0] : 0, latitude: feature.geometry.type === "Point" ? (feature.geometry as any).coordinates[1] : 0, }, bubbles: true, }) map.getCanvas().dispatchEvent(customEvent) document.dispatchEvent(customEvent) }, [], ) // Handle incident click const handleIncidentClick = useCallback( ( map: mapboxgl.Map, setSelectedIncident: (incident: any | null) => void, setSelectedUnit: (unit: IUnits | null) => void, setSelectedEntityId: (id: string | undefined) => void, setIsUnitSelected: (isSelected: boolean) => void, setSelectedDistrictId: (id: string | undefined) => void, ) => (e: mapboxgl.MapMouseEvent & { features?: mapboxgl.MapboxGeoJSONFeature[] }) => { if (!e.features || e.features.length === 0) return e.originalEvent.stopPropagation() e.preventDefault() const feature = e.features[0] const properties = feature.properties if (!properties) return // Get coordinates const longitude = feature.geometry.type === "Point" ? (feature.geometry as any).coordinates[0] : 0 const latitude = feature.geometry.type === "Point" ? (feature.geometry as any).coordinates[1] : 0 // Fly to the incident location map.flyTo({ center: [longitude, latitude], zoom: 15, pitch: 45, bearing: 0, duration: 2000, }) // Create incident object from properties const incident = { id: properties.id, category: properties.category, description: properties.description, date: properties.date, district: properties.district, district_id: properties.district_id, distance_to_unit: properties.distance_to_unit, longitude, latitude, } // Set the selected incident and query parameters setSelectedIncident(incident) setSelectedUnit(null) // Clear any selected unit setSelectedEntityId(properties.id) setIsUnitSelected(false) setSelectedDistrictId(properties.district_id) // Highlight the connected lines for this incident if (map.getLayer("units-connection-lines")) { map.setFilter("units-connection-lines", ["==", ["get", "incident_id"], properties.id]) } // Dispatch a custom event for other components to react to const customEvent = new CustomEvent("incident_click", { detail: { id: properties.id, district: properties.district, category: properties.category, description: properties.description, longitude, latitude, }, bubbles: true, }) map.getCanvas().dispatchEvent(customEvent) document.dispatchEvent(customEvent) }, [], ) const unitClickHandler = useMemo( () => handleUnitClick( map as mapboxgl.Map, unitsData, setSelectedUnit, setSelectedIncident, setSelectedEntityId, setIsUnitSelected, setSelectedDistrictId, ), [ map, unitsData, setSelectedUnit, setSelectedIncident, setSelectedEntityId, setIsUnitSelected, setSelectedDistrictId, ], ) const incidentClickHandler = useMemo( () => handleIncidentClick( map as mapboxgl.Map, setSelectedIncident, setSelectedUnit, setSelectedEntityId, setIsUnitSelected, setSelectedDistrictId, ), [map, setSelectedIncident, setSelectedUnit, setSelectedEntityId, setIsUnitSelected, setSelectedDistrictId], ) // Set up event handlers useEffect(() => { if (!map || !visible) return // Debug log to confirm map layers console.log("Available map layers:", map.getStyle().layers?.map(l => l.id)); // Define event handlers that can be referenced for both adding and removing const handleMouseEnter = () => { map.getCanvas().style.cursor = "pointer" } const handleMouseLeave = () => { map.getCanvas().style.cursor = "" } // Add click event for units-points layer if (map.getLayer("units-points")) { map.off("click", "units-points", unitClickHandler) map.on("click", "units-points", unitClickHandler) // Change cursor on hover map.on("mouseenter", "units-points", handleMouseEnter) map.on("mouseleave", "units-points", handleMouseLeave) } // Add click event for incidents-points layer if (map.getLayer("incidents-points")) { map.off("click", "incidents-points", incidentClickHandler) map.on("click", "incidents-points", incidentClickHandler) // Change cursor on hover map.on("mouseenter", "incidents-points", handleMouseEnter) map.on("mouseleave", "incidents-points", handleMouseLeave) } return () => { if (map) { if (map.getLayer("units-points")) { map.off("click", "units-points", unitClickHandler) map.off("mouseenter", "units-points", handleMouseEnter) map.off("mouseleave", "units-points", handleMouseLeave) } if (map.getLayer("incidents-points")) { map.off("click", "incidents-points", incidentClickHandler) map.off("mouseenter", "incidents-points", handleMouseEnter) map.off("mouseleave", "incidents-points", handleMouseLeave) } } } }, [map, visible, unitClickHandler, incidentClickHandler]) // Reset map filters when popup is closed const handleClosePopup = useCallback(() => { setSelectedUnit(null) setSelectedIncident(null) setSelectedEntityId(undefined) setSelectedDistrictId(undefined) if (map && map.getLayer("units-connection-lines")) { map.setFilter("units-connection-lines", ["has", "unit_id"]) } }, [map]) // Clean up on unmount or when visibility changes useEffect(() => { if (!visible) { handleClosePopup() } }, [visible, handleClosePopup]) if (!visible) return null return ( <> {/* Units Points */} {/* Units Symbols */} {/* Incidents Points */} {/* Connection Lines */} {/* Custom Unit Popup */} {selectedUnit && ( )} {/* Custom Incident Popup */} {selectedIncident && ( )} ) }