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

561 lines
21 KiB
TypeScript

"use client"
import { useState, useRef, useEffect, useCallback, act } 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 TimelineLayer from "./timeline-layer"
import type { ICrimes, IIncidentLogs } from "@/app/_utils/types/crimes"
import type { IDistrictFeature } from "@/app/_utils/types/map"
import { createFillColorExpression, processCrimeDataByDistrict } from "@/app/_utils/map"
import UnclusteredPointLayer from "./uncluster-layer"
import { toast } from "sonner"
import type { ITooltips } from "../controls/top/tooltips"
import type { IUnits } from "@/app/_utils/types/units"
import UnitsLayer from "./units-layer"
import DistrictFillLineLayer from "./district-layer"
import CrimePopup from "../pop-up/crime-popup"
import TimeZonesDisplay from "./timezone"
import TimezoneLayer from "./timezone"
import FaultLinesLayer from "./fault-lines"
import CoastlineLayer from "./coastline"
import EWSAlertLayer from "./ews-alert-layer"
import PanicButtonDemo from "../controls/panic-button-demo"
import { IIncidentLog } from "@/app/_utils/types/ews"
import { addMockIncident, getAllIncidents, resolveIncident } from "@/app/_utils/mock/ews-data"
import HistoricalIncidentsLayer from "./historical-incidents-layer"
import RecentIncidentsLayer from "./recent-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<string, any>
showFill?: boolean
activeControl?: ITooltips
}
interface LayersProps {
visible?: boolean
crimes: ICrimes[]
units?: IUnits[]
recentIncidents: IIncidentLogs[]
year: string
month: string
filterCategory: string | "all"
activeControl: ITooltips
tilesetId?: string
useAllData?: boolean
showEWS?: boolean
sourceType?: string
}
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 { 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<string | null>(null)
const selectedDistrictRef = useRef<IDistrictFeature | null>(null)
const crimeDataByDistrict = processCrimeDataByDistrict(crimes)
const [ewsIncidents, setEwsIncidents] = useState<IIncidentLog[]>([]);
const [showPanicDemo, setShowPanicDemo] = useState(true);
useEffect(() => {
setEwsIncidents(getAllIncidents());
}, []);
const handleTriggerAlert = useCallback((priority: 'high' | 'medium' | 'low') => {
const newIncident = addMockIncident({ priority });
setEwsIncidents(getAllIncidents());
}, []);
const handleResolveIncident = useCallback((id: string) => {
resolveIncident(id);
setEwsIncidents(getAllIncidents());
}, []);
const handleResolveAllAlerts = useCallback(() => {
ewsIncidents.forEach(incident => {
if (incident.status === 'active') {
resolveIncident(incident.id);
}
});
setEwsIncidents(getAllIncidents());
}, [ewsIncidents]);
const handlePopupClose = useCallback(() => {
selectedDistrictRef.current = null
setSelectedDistrict(null)
setSelectedIncident(null)
setFocusedDistrictId(null)
if (map) {
map.easeTo({
zoom: BASE_ZOOM,
pitch: BASE_PITCH,
bearing: BASE_BEARING,
duration: 1500,
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 handleCloseDistrictPopup = useCallback(() => {
console.log("Closing district popup")
handlePopupClose()
}, [handlePopupClose])
const handleCloseIncidentPopup = useCallback(() => {
console.log("Closing incident popup")
handlePopupClose()
}, [handlePopupClose])
const handleDistrictClick = useCallback(
(feature: IDistrictFeature) => {
console.log("District clicked:", feature)
setSelectedIncident(null)
setSelectedDistrict(feature)
selectedDistrictRef.current = feature
setFocusedDistrictId(feature.id)
if (map && feature.longitude && feature.latitude) {
map.flyTo({
center: [feature.longitude, feature.latitude],
zoom: 12,
pitch: 45,
bearing: 0,
duration: 1500,
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 (!mapboxMap) return
const handleIncidentClickEvent = (e: Event) => {
const customEvent = e as CustomEvent
console.log("Received incident_click event in layers:", customEvent.detail)
if (!customEvent.detail) {
console.error("Empty incident click event data")
return
}
const incidentId = customEvent.detail.id || customEvent.detail.incidentId || customEvent.detail.incident_id
if (!incidentId) {
console.error("No incident ID found in event data:", customEvent.detail)
return
}
console.log("Looking for incident with ID:", incidentId)
let foundIncident: ICrimeIncident | undefined
if (
customEvent.detail.latitude !== undefined &&
customEvent.detail.longitude !== undefined &&
customEvent.detail.category !== undefined
) {
foundIncident = {
id: incidentId,
district: customEvent.detail.district,
category: customEvent.detail.category,
type_category: customEvent.detail.type,
description: customEvent.detail.description,
status: customEvent.detail.status || "Unknown",
timestamp: customEvent.detail.timestamp ? new Date(customEvent.detail.timestamp) : undefined,
latitude: customEvent.detail.latitude,
longitude: customEvent.detail.longitude,
address: customEvent.detail.address,
}
} else {
for (const crime of crimes) {
for (const incident of crime.crime_incidents) {
if (incident.id === incidentId || incident.id?.toString() === incidentId?.toString()) {
console.log("Found matching incident:", incident)
foundIncident = {
id: incident.id,
district: crime.districts.name,
description: incident.description,
status: incident.status || "unknown",
timestamp: incident.timestamp,
category: incident.crime_categories.name,
type_category: incident.crime_categories.type,
address: incident.locations.address,
latitude: incident.locations.latitude,
longitude: incident.locations.longitude,
}
break
}
}
if (foundIncident) break
}
}
if (!foundIncident) {
console.error("Could not find incident with ID:", incidentId)
return
}
if (!foundIncident.latitude || !foundIncident.longitude) {
console.error("Found incident has invalid coordinates:", foundIncident)
return
}
console.log("Setting selected incident:", foundIncident)
setSelectedDistrict(null)
selectedDistrictRef.current = null
setFocusedDistrictId(null)
setSelectedIncident(foundIncident)
}
mapboxMap.getCanvas().addEventListener("incident_click", handleIncidentClickEvent as EventListener)
document.addEventListener("incident_click", handleIncidentClickEvent as EventListener)
console.log("Set up incident click event listener")
return () => {
console.log("Removing incident click event listener")
if (mapboxMap && mapboxMap.getCanvas()) {
mapboxMap.getCanvas().removeEventListener("incident_click", handleIncidentClickEvent as EventListener)
}
document.removeEventListener("incident_click", handleIncidentClickEvent as EventListener)
}
}, [mapboxMap, crimes, setFocusedDistrictId])
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) => {
console.log("Setting focused district ID:", id, "from marker click:", isMarkerClick)
setFocusedDistrictId(id)
}, [])
if (!visible) return null
const crimesVisible = activeControl === "incidents"
const showHeatmapLayer = activeControl === "heatmap" && sourceType !== "cbu"
const showUnitsLayer = activeControl === "units"
const showTimelineLayer = activeControl === "timeline"
const showHistoricalLayer = activeControl === "historical"
const showRecentIncidents = activeControl === "recents"
const showDistrictFill = activeControl === "incidents" || activeControl === "clusters" || activeControl === "historical" || activeControl === "recents"
const showIncidentMarkers = activeControl !== "heatmap" && activeControl !== "timeline" && sourceType !== "cbu"
return (
<>
<DistrictFillLineLayer
visible={true}
map={mapboxMap}
year={year}
month={month}
filterCategory={filterCategory}
crimes={crimes}
tilesetId={tilesetId}
focusedDistrictId={focusedDistrictId}
setFocusedDistrictId={handleSetFocusedDistrictId}
crimeDataByDistrict={crimeDataByDistrict}
showFill={showDistrictFill}
activeControl={activeControl}
onDistrictClick={handleDistrictClick}
/>
{/* Recent Incidents Layer (24 hours) */}
<RecentIncidentsLayer
visible={showRecentIncidents}
map={mapboxMap}
incidents={recentIncidents}
/>
<HeatmapLayer
crimes={crimes}
year={year}
month={month}
filterCategory={filterCategory}
visible={showHeatmapLayer}
useAllData={useAllData}
enableInteractions={true}
setFocusedDistrictId={handleSetFocusedDistrictId}
/>
<TimelineLayer
crimes={crimes}
year={year}
month={month}
filterCategory={filterCategory}
visible={showTimelineLayer}
map={mapboxMap}
useAllData={useAllData}
/>
<UnitsLayer
crimes={crimes}
units={units}
filterCategory={filterCategory}
visible={showUnitsLayer}
map={mapboxMap}
/>
<ClusterLayer
visible={visible && activeControl === "clusters"}
map={mapboxMap}
crimes={crimes}
filterCategory={filterCategory}
focusedDistrictId={focusedDistrictId}
clusteringEnabled={activeControl === "clusters"}
showClusters={activeControl === "clusters"}
sourceType={sourceType}
/>
<UnclusteredPointLayer
visible={visible && showIncidentMarkers && !focusedDistrictId}
map={mapboxMap}
crimes={crimes}
filterCategory={filterCategory}
focusedDistrictId={focusedDistrictId}
/>
{selectedDistrict && !selectedIncident && (
<>
<DistrictPopup
longitude={selectedDistrict.longitude || 0}
latitude={selectedDistrict.latitude || 0}
onClose={handleCloseDistrictPopup}
district={selectedDistrict}
year={year}
month={month}
filterCategory={filterCategory}
/>
<DistrictExtrusionLayer
visible={visible}
map={mapboxMap}
tilesetId={tilesetId}
focusedDistrictId={focusedDistrictId}
crimeDataByDistrict={crimeDataByDistrict}
/>
</>
)}
<TimezoneLayer map={mapboxMap} />
<FaultLinesLayer map={mapboxMap} />
{/* <CoastlineLayer map={mapboxMap} /> */}
{showEWS && (
<EWSAlertLayer
map={mapboxMap}
incidents={ewsIncidents}
onIncidentResolved={handleResolveIncident}
/>
)}
{showEWS && showPanicDemo && (
<div className="absolute top-0 right-20 z-50 p-2">
<PanicButtonDemo
onTriggerAlert={handleTriggerAlert}
onResolveAllAlerts={handleResolveAllAlerts}
activeIncidents={ewsIncidents.filter(inc => inc.status === 'active')}
/>
</div>
)}
{selectedIncident && selectedIncident.latitude && selectedIncident.longitude && (
<CrimePopup
longitude={selectedIncident.longitude}
latitude={selectedIncident.latitude}
onClose={handleCloseIncidentPopup}
incident={selectedIncident}
/>
)}
</>
)
}