327 lines
12 KiB
TypeScript
327 lines
12 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useRef, useEffect, useCallback } 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 DistrictLayer from "./district-layer-old"
|
|
import TimelineLayer from "./timeline-layer"
|
|
|
|
import type { ICrimes } from "@/app/_utils/types/crimes"
|
|
import { IDistrictFeature } from "@/app/_utils/types/map"
|
|
import { createFillColorExpression, processCrimeDataByDistrict } from "@/app/_utils/map"
|
|
import UnclusteredPointLayer from "./uncluster-layer"
|
|
import FlyToHandler from "../fly-to"
|
|
import { toast } from "sonner"
|
|
import { ITooltips } from "../controls/top/tooltips"
|
|
import { IUnits } from "@/app/_utils/types/units"
|
|
import UnitsLayer from "./units-layer"
|
|
import DistrictFillLineLayer from "./district-layer"
|
|
|
|
// District layer props
|
|
export interface IDistrictLayerProps {
|
|
visible?: boolean
|
|
onClick?: (feature: IDistrictFeature) => void
|
|
year: string
|
|
month: string
|
|
filterCategory: string | "all"
|
|
crimes: ICrimes[]
|
|
units?: IUnits[]
|
|
tilesetId?: string
|
|
}
|
|
|
|
interface LayersProps {
|
|
visible?: boolean;
|
|
crimes: ICrimes[];
|
|
units?: IUnits[];
|
|
year: string;
|
|
month: string;
|
|
filterCategory: string | "all";
|
|
activeControl: ITooltips;
|
|
tilesetId?: string;
|
|
useAllData?: boolean; // New prop to indicate if we're showing all data
|
|
}
|
|
|
|
export default function Layers({
|
|
visible = true,
|
|
crimes,
|
|
units,
|
|
year,
|
|
month,
|
|
filterCategory,
|
|
activeControl,
|
|
tilesetId = MAPBOX_TILESET_ID,
|
|
useAllData = false,
|
|
}: 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 [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 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])
|
|
|
|
// Make sure we have a defined handler for setFocusedDistrictId
|
|
const handleSetFocusedDistrictId = useCallback((id: string | null) => {
|
|
setFocusedDistrictId(id);
|
|
}, []);
|
|
|
|
if (!visible) return null
|
|
|
|
// Determine which layers should be visible based on the active control
|
|
const showDistrictLayer = activeControl === "incidents";
|
|
const showHeatmapLayer = activeControl === "heatmap";
|
|
const showClustersLayer = activeControl === "clusters";
|
|
const showUnitsLayer = activeControl === "units";
|
|
const showTimelineLayer = activeControl === "timeline";
|
|
|
|
// District fill should only be visible for incidents and clusters
|
|
const showDistrictFill = activeControl === "incidents" || activeControl === "clusters";
|
|
|
|
// Only show incident markers for incidents and clusters views - exclude timeline mode
|
|
const showIncidentMarkers = (activeControl === "incidents" || activeControl === "clusters")
|
|
|
|
return (
|
|
<>
|
|
{/* Standard District Layer with incident points */}
|
|
<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}
|
|
/>
|
|
|
|
{/* Heatmap Layer */}
|
|
<HeatmapLayer
|
|
crimes={crimes}
|
|
year={year}
|
|
month={month}
|
|
filterCategory={filterCategory}
|
|
visible={showHeatmapLayer}
|
|
useAllData={useAllData}
|
|
/>
|
|
|
|
{/* Timeline Layer - make sure this is the only visible layer in timeline mode */}
|
|
<TimelineLayer
|
|
crimes={crimes}
|
|
year={year}
|
|
month={month}
|
|
filterCategory={filterCategory}
|
|
visible={showTimelineLayer}
|
|
map={mapboxMap}
|
|
useAllData={useAllData}
|
|
/>
|
|
|
|
{/* Units Layer */}
|
|
<UnitsLayer
|
|
crimes={crimes}
|
|
units={units}
|
|
filterCategory={filterCategory}
|
|
visible={showUnitsLayer}
|
|
map={mapboxMap}
|
|
/>
|
|
|
|
{/* District base layer */}
|
|
<DistrictExtrusionLayer
|
|
visible={visible}
|
|
map={mapboxMap}
|
|
tilesetId={tilesetId}
|
|
focusedDistrictId={focusedDistrictId}
|
|
crimeDataByDistrict={crimeDataByDistrict}
|
|
/>
|
|
|
|
{/* Cluster Layer - only enable when the clusters control is active and NOT in timeline mode */}
|
|
<ClusterLayer
|
|
visible={visible && !showTimelineLayer}
|
|
map={mapboxMap}
|
|
crimes={crimes}
|
|
filterCategory={filterCategory}
|
|
focusedDistrictId={focusedDistrictId}
|
|
clusteringEnabled={showClustersLayer}
|
|
showClusters={showClustersLayer}
|
|
/>
|
|
|
|
{/* Unclustered Points Layer - explicitly hide in timeline mode */}
|
|
<UnclusteredPointLayer
|
|
visible={visible && showIncidentMarkers && !focusedDistrictId && !showTimelineLayer}
|
|
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}
|
|
/>
|
|
)}
|
|
</>
|
|
)
|
|
}
|