"use client" import { useEffect, useMemo, useRef, useState } from 'react' import { Layer, Source } from "react-map-gl/mapbox" import { ICrimes, IDistanceResult } from "@/app/_utils/types/crimes" import { IUnits } from "@/app/_utils/types/units" import mapboxgl from 'mapbox-gl' import { useQuery } from '@tanstack/react-query' import { generateCategoryColorMap, getCategoryColor } from '@/app/_utils/colors' import UnitPopup from '../pop-up/unit-popup' import IncidentPopup from '../pop-up/incident-popup' import { calculateDistances } from '@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/action' interface UnitsLayerProps { crimes: ICrimes[] units?: IUnits[] filterCategory: string | "all" visible?: boolean map?: mapboxgl.Map | null } // Custom hook for fetching distance data const useDistanceData = (entityId?: string, isUnit: boolean = false, districtId?: string) => { // Skip the query when no entity is selected return useQuery({ queryKey: ['distance-incidents', entityId, isUnit, districtId], queryFn: async () => { if (!entityId) return []; const unitId = isUnit ? entityId : undefined; const result = await calculateDistances(unitId, districtId); return result; }, enabled: !!entityId, // Only run query when there's an entityId staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes gcTime: 10 * 60 * 1000, // Keep cache for 10 minutes }); }; 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 react-query for distance data const { data: distances = [], isLoading: isLoadingDistances } = useDistanceData( selectedEntityId, isUnitSelected, selectedDistrictId ); // 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(() => { return { type: "FeatureCollection" as const, features: unitsData.map(unit => ({ type: "Feature" as const, properties: { id: unit.code_unit, name: unit.name, address: unit.address, phone: unit.phone, type: unit.type, district: unit.districts?.name || "", district_id: unit.district_id, }, geometry: { type: "Point" as const, coordinates: [unit.longitude || 0, unit.latitude || 0] } })).filter(feature => feature.geometry.coordinates[0] !== 0 && feature.geometry.coordinates[1] !== 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', }, 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]) useEffect(() => { if (!map || !visible) return const handleUnitClick = (e: mapboxgl.MapMouseEvent & { features?: mapboxgl.MapboxGeoJSONFeature[] }) => { if (!e.features || e.features.length === 0) return 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) return; // 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 ]) } } const handleIncidentClick = (e: mapboxgl.MapMouseEvent & { features?: mapboxgl.MapboxGeoJSONFeature[] }) => { if (!e.features || e.features.length === 0) return; const feature = e.features[0]; const properties = feature.properties; if (!properties) return; // 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, }; // Set the selected incident and query parameters setSelectedIncident({ ...incident, latitude: feature.geometry.type === 'Point' ? (feature.geometry as any).coordinates[1] : 0, longitude: feature.geometry.type === 'Point' ? (feature.geometry as any).coordinates[0] : 0 }); setSelectedUnit(null); 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 ]); } }; // 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.on('click', 'units-points', handleUnitClick) // 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.on('click', 'incidents-points', handleIncidentClick) // Change cursor on hover map.on('mouseenter', 'incidents-points', handleMouseEnter) map.on('mouseleave', 'incidents-points', handleMouseLeave) } return () => { if (map.getLayer('units-points')) { map.off('click', 'units-points', handleUnitClick) map.off('mouseenter', 'units-points', handleMouseEnter) map.off('mouseleave', 'units-points', handleMouseLeave) } if (map.getLayer('incidents-points')) { map.off('click', 'incidents-points', handleIncidentClick) map.off('mouseenter', 'incidents-points', handleMouseEnter) map.off('mouseleave', 'incidents-points', handleMouseLeave) } } }, [map, visible, unitsData]) // Reset map filters when popup is closed const handleClosePopup = () => { setSelectedUnit(null); setSelectedIncident(null); setSelectedEntityId(undefined); setSelectedDistrictId(undefined); if (map && map.getLayer('units-connection-lines')) { map.setFilter('units-connection-lines', ['has', 'unit_id']); } }; if (!visible) return null return ( <> {/* Units Points */} {/* Units Symbols */} {/* Incidents Points */} {/* Connection Lines */} {/* Custom Unit Popup */} {selectedUnit && ( )} {/* Custom Incident Popup */} {selectedIncident && ( )} ) }