MIF_E31221222/sigap-website/app/_components/map/layers/units-layer.tsx

275 lines
10 KiB
TypeScript

"use client"
import { useEffect, useMemo, useRef, useState } from 'react'
import { Layer, Source } from "react-map-gl/mapbox"
import { ICrimes } from "@/app/_utils/types/crimes"
import { IUnits } from "@/app/_utils/types/units"
import mapboxgl from 'mapbox-gl'
import { generateCategoryColorMap, getCategoryColor } from '@/app/_utils/colors'
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<IUnits[]>([])
const loadedUnitsRef = useRef<IUnits[]>([])
// 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<string>();
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])
// 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<string, IUnits[]>()
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])
// Map click handler code and the rest remains the same...
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
// Create a popup for the unit
const popup = new mapboxgl.Popup()
.setLngLat(feature.geometry.type === 'Point' ?
(feature.geometry as any).coordinates as [number, number] :
[0, 0]) // Fallback coordinates if not a Point geometry
.setHTML(`
<div class="p-2">
<h3 class="font-bold text-base">${properties.name}</h3>
<p class="text-sm">${properties.type}</p>
<p class="text-sm">${properties.address || 'No address provided'}</p>
<p class="text-xs mt-2">Staff: ${properties.staff_count || 'N/A'}</p>
<p class="text-xs">Phone: ${properties.phone || 'N/A'}</p>
<p class="text-xs">District: ${properties.district || 'N/A'}</p>
</div>
`)
.addTo(map)
// Highlight the connected lines for this unit
if (map.getLayer('units-connection-lines')) {
map.setFilter('units-connection-lines', [
'==',
['get', 'unit_id'],
properties.id
])
}
// When popup closes, reset the lines filter
popup.on('close', () => {
if (map.getLayer('units-connection-lines')) {
map.setFilter('units-connection-lines', ['has', 'unit_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)
}
return () => {
if (map.getLayer('units-points')) {
map.off('click', 'units-points', handleUnitClick)
map.off('mouseenter', 'units-points', handleMouseEnter)
map.off('mouseleave', 'units-points', handleMouseLeave)
}
}
}, [map, visible])
if (!visible) return null
return (
<>
{/* Units Points */}
<Source id="units-source" type="geojson" data={unitsGeoJSON}>
<Layer
id="units-points"
type="circle"
paint={{
'circle-radius': 8,
'circle-color': '#1e40af', // Deep blue for police units
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff',
'circle-opacity': 0.8
}}
/>
{/* Units Symbols */}
<Layer
id="units-symbols"
type="symbol"
layout={{
'text-field': ['get', 'name'],
'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
'text-size': 12,
'text-offset': [0, -2],
'text-anchor': 'bottom',
'text-allow-overlap': false,
'text-ignore-placement': false
}}
paint={{
'text-color': '#ffffff',
'text-halo-color': '#000000',
'text-halo-width': 1
}}
/>
</Source>
{/* Connection Lines */}
<Source id="units-lines-source" type="geojson" data={connectionLinesGeoJSON}>
<Layer
id="units-connection-lines"
type="line"
paint={{
// Use the pre-computed color stored in the properties
'line-color': ['get', 'lineColor'],
'line-width': 1.5,
'line-opacity': 0.7,
'line-dasharray': [1, 2] // Dashed line
}}
/>
</Source>
</>
)
}