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

263 lines
9.6 KiB
TypeScript

"use client"
import { useState, useRef, useEffect } 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 type { ICrimes } from "@/app/_utils/types/crimes"
import { IDistrictFeature } from "@/app/_utils/types/map"
import { createFillColorExpression, processCrimeDataByDistrict } from "@/app/_utils/map"
import DistrictFillLineLayer from "./district-layer"
import UnclusteredPointLayer from "./uncluster-layer"
import FlyToHandler from "../fly-to"
import { toast } from "sonner"
// District layer props
export interface IDistrictLayerProps {
visible?: boolean
onClick?: (feature: IDistrictFeature) => void
year: string
month: string
filterCategory: string | "all"
crimes: ICrimes[]
tilesetId?: string
}
export default function Layers({
visible = true,
onClick,
year,
month,
filterCategory = "all",
crimes = [],
tilesetId = MAPBOX_TILESET_ID,
}: IDistrictLayerProps) {
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 [focusedDistrictId, setFocusedDistrictId] = useState<string | null>(null)
const selectedDistrictRef = useRef<IDistrictFeature | null>(null)
const crimeDataByDistrict = processCrimeDataByDistrict(crimes)
// Set up custom event handler for cluster clicks to ensure it works across components
useEffect(() => {
if (!mapboxMap) return;
const handleClusterClickEvent = (e: CustomEvent) => {
if (!e.detail) return;
const { center, zoom } = e.detail;
if (center && zoom) {
mapboxMap.flyTo({
center: center,
zoom: zoom,
duration: 1000,
easing: (t) => t * (2 - t)
});
}
};
mapboxMap.getCanvas().addEventListener('cluster_click', handleClusterClickEvent as EventListener);
return () => {
mapboxMap.getCanvas().removeEventListener('cluster_click', handleClusterClickEvent as EventListener);
};
}, [mapboxMap]);
// Handle district selection
const handleDistrictClick = (district: IDistrictFeature) => {
selectedDistrictRef.current = district
if (onClick) {
onClick(district)
} else {
setSelectedDistrict(district)
}
}
// Handle popup close
const handleCloseDistrictPopup = () => {
console.log("Closing district popup")
selectedDistrictRef.current = null
setSelectedDistrict(null)
setFocusedDistrictId(null)
// Reset pitch and bearing
if (map) {
map.easeTo({
zoom: BASE_ZOOM,
pitch: BASE_PITCH,
bearing: BASE_BEARING,
duration: 1500,
easing: (t) => t * (2 - t), // easeOutQuad
})
// Show all clusters again when closing popup
if (map.getLayer("clusters")) {
map.getMap().setLayoutProperty("clusters", "visibility", "visible")
}
if (map.getLayer("unclustered-point")) {
map.getMap().setLayoutProperty("unclustered-point", "visibility", "visible")
}
// Explicitly update fill color for all districts
if (map.getLayer("district-fill")) {
const fillColorExpression = createFillColorExpression(null, crimeDataByDistrict)
map.getMap().setPaintProperty("district-fill", "fill-color", fillColorExpression as any)
}
}
}
// Update selected district when year/month/filter changes
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])
if (!visible) return null
return (
<>
<DistrictFillLineLayer
visible={visible}
map={mapboxMap}
onClick={handleDistrictClick}
year={year}
month={month}
filterCategory={filterCategory}
crimes={crimes}
tilesetId={tilesetId}
focusedDistrictId={focusedDistrictId}
setFocusedDistrictId={setFocusedDistrictId}
crimeDataByDistrict={crimeDataByDistrict}
/>
<DistrictExtrusionLayer
visible={visible}
map={mapboxMap}
tilesetId={tilesetId}
focusedDistrictId={focusedDistrictId}
crimeDataByDistrict={crimeDataByDistrict}
/>
<ClusterLayer
visible={visible}
map={mapboxMap}
crimes={crimes}
filterCategory={filterCategory}
focusedDistrictId={focusedDistrictId}
/>
<UnclusteredPointLayer
visible={visible}
map={mapboxMap}
crimes={crimes}
filterCategory={filterCategory}
focusedDistrictId={focusedDistrictId}
/>
<FlyToHandler map={mapboxMap} />
{selectedDistrict && (
<DistrictPopup
longitude={selectedDistrict.longitude || 0}
latitude={selectedDistrict.latitude || 0}
onClose={handleCloseDistrictPopup}
district={selectedDistrict}
year={year}
month={month}
filterCategory={filterCategory}
/>
)}
</>
)
}