From 5808758855c9877508221f35ca8965c1e87a7bf8 Mon Sep 17 00:00:00 2001 From: vergiLgood1 Date: Wed, 14 May 2025 12:11:34 +0700 Subject: [PATCH] Refactor map layer animations to use BASE_DURATION and introduce new constants for 3D view settings - Updated DistrictFillLineLayer to use BASE_DURATION for animation duration instead of a hardcoded value. - Commented out the flyTo animation in DistrictFillLineLayer for potential future use. - Modified Layers component to utilize ZOOM_3D and PITCH_3D constants for flyTo animations, enhancing readability and maintainability. --- .../app/_components/map/crime-map.tsx | 5 +- .../map/layers/district-layer-old.tsx | 2368 ++++++++--------- .../_components/map/layers/district-layer.tsx | 20 +- .../app/_components/map/layers/layers.tsx | 8 +- 4 files changed, 1201 insertions(+), 1200 deletions(-) diff --git a/sigap-website/app/_components/map/crime-map.tsx b/sigap-website/app/_components/map/crime-map.tsx index c4e737d..c06d61e 100644 --- a/sigap-website/app/_components/map/crime-map.tsx +++ b/sigap-website/app/_components/map/crime-map.tsx @@ -22,12 +22,13 @@ import { ITooltipsControl } from "./controls/top/tooltips" import CrimeSidebar from "./controls/left/sidebar/map-sidebar" import Tooltips from "./controls/top/tooltips" import Layers from "./layers/layers" -import DistrictLayer, { DistrictFeature } from "./layers/district-layer-old" + import { useGetUnitsQuery } from "@/app/(pages)/(admin)/dashboard/crime-management/units/_queries/queries" +import { IDistrictFeature } from "@/app/_utils/types/map" export default function CrimeMap() { const [sidebarCollapsed, setSidebarCollapsed] = useState(true) - const [selectedDistrict, setSelectedDistrict] = useState(null) + const [selectedDistrict, setSelectedDistrict] = useState(null) const [showLegend, setShowLegend] = useState(true) const [activeControl, setActiveControl] = useState("clusters") const [selectedSourceType, setSelectedSourceType] = useState("cbu") diff --git a/sigap-website/app/_components/map/layers/district-layer-old.tsx b/sigap-website/app/_components/map/layers/district-layer-old.tsx index 45ac2bc..d4ddb84 100644 --- a/sigap-website/app/_components/map/layers/district-layer-old.tsx +++ b/sigap-website/app/_components/map/layers/district-layer-old.tsx @@ -1,1198 +1,1198 @@ -"use client" - -import { useEffect, useState, useRef, useCallback } from "react" -import { useMap } from "react-map-gl/mapbox" -import { BASE_BEARING, BASE_PITCH, BASE_ZOOM, CRIME_RATE_COLORS, MAPBOX_TILESET_ID } from "@/app/_utils/const/map" - -import { $Enums } from "@prisma/client" -import DistrictPopup from "../pop-up/district-popup" -import type { ICrimes } from "@/app/_utils/types/crimes" - -// Types for district properties -export interface DistrictFeature { - id: string - name: string - longitude: number - latitude: number - number_of_crime: number - level: $Enums.crime_rates - demographics: { - number_of_unemployed: number - population: number - population_density: number - year: number - } - geographics: { - address: string - land_area: number - year: number - latitude: number - longitude: number - } - crime_incidents: Array<{ - id: string - timestamp: Date - description: string - status: string - category: string - type: string - address: string - latitude: number - longitude: number - }> - selectedYear?: string - selectedMonth?: string - isFocused?: boolean // Add a property to track if district is focused -} - -// District layer props -export interface DistrictLayerProps { - visible?: boolean // New prop to control visibility - onClick?: (feature: DistrictFeature) => void - year: string - month: string - filterCategory: string | "all" - crimes: ICrimes[] - tilesetId?: string -} - -export default function DistrictLayer({ - visible = true, - onClick, - year, - month, - filterCategory = "all", - crimes = [], - tilesetId = MAPBOX_TILESET_ID, -}: DistrictLayerProps) { - const { current: map } = useMap() - - const [hoverInfo, setHoverInfo] = useState<{ - x: number - y: number - feature: any - } | null>(null) - - const selectedDistrictRef = useRef(null) - const [selectedDistrict, setSelectedDistrict] = useState(null) - const [focusedDistrictId, setFocusedDistrictId] = useState(null) - const rotationAnimationRef = useRef(null) - const bearingRef = useRef(0) - const layersAdded = useRef(false) - - const crimeDataByDistrict = crimes.reduce( - (acc, crime) => { - const districtId = crime.district_id - - acc[districtId] = { - number_of_crime: crime.number_of_crime, - level: crime.level, - } - return acc - }, - {} as Record, - ) - - const handleDistrictClick = (e: any) => { - const incidentFeatures = map?.queryRenderedFeatures(e.point, { layers: ["unclustered-point", "clusters"] }) - - if (incidentFeatures && incidentFeatures.length > 0) { - return - } - - if (!map || !e.features || e.features.length === 0) return - - const feature = e.features[0] - const districtId = feature.properties.kode_kec - - // If clicking the same district, deselect it - if (focusedDistrictId === districtId) { - setFocusedDistrictId(null) - selectedDistrictRef.current = null - setSelectedDistrict(null) - - // Reset animation and map view when deselecting - if (rotationAnimationRef.current) { - cancelAnimationFrame(rotationAnimationRef.current) - rotationAnimationRef.current = null - } - - bearingRef.current = 0 - - // Reset pitch and bearing with animation - map.easeTo({ - pitch: BASE_PITCH, - bearing: 0, - duration: 1500, - easing: (t) => t * (2 - t), // easeOutQuad - }) - - // Show all clusters again when unfocusing - if (map.getMap().getLayer("clusters")) { - map.getMap().setLayoutProperty("clusters", "visibility", "visible") - } - if (map.getMap().getLayer("unclustered-point")) { - map.getMap().setLayoutProperty("unclustered-point", "visibility", "visible") - } - - return - } - - const crimeData = crimeDataByDistrict[districtId] || {} - - let crime_incidents: Array<{ - id: string - timestamp: Date - description: string - status: string - category: string - type: string - address: string - latitude: number - longitude: number - }> = [] - - const districtCrimes = crimes.filter((crime) => crime.district_id === districtId) - - districtCrimes.forEach((crimeRecord) => { - if (crimeRecord && crimeRecord.crime_incidents) { - const incidents = crimeRecord.crime_incidents.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 || 0, - longitude: incident.locations?.longitude || 0, - })) - - crime_incidents = [...crime_incidents, ...incidents] - } - }) - - const firstDistrictCrime = districtCrimes.length > 0 ? districtCrimes[0] : null - - const selectedYearNum = year ? Number.parseInt(year) : new Date().getFullYear() - - let demographics = firstDistrictCrime?.districts.demographics?.find((d) => d.year === selectedYearNum) - - if (!demographics && firstDistrictCrime?.districts.demographics?.length) { - demographics = firstDistrictCrime.districts.demographics.sort((a, b) => b.year - a.year)[0] - console.log( - `Tidak ada data demografis untuk tahun ${selectedYearNum}, menggunakan data tahun ${demographics.year}`, - ) - } - - let geographics = firstDistrictCrime?.districts.geographics?.find((g) => g.year === selectedYearNum) - - if (!geographics && firstDistrictCrime?.districts.geographics?.length) { - const validGeographics = firstDistrictCrime.districts.geographics - .filter((g) => g.year !== null) - .sort((a, b) => (b.year || 0) - (a.year || 0)) - - geographics = validGeographics.length > 0 ? validGeographics[0] : firstDistrictCrime.districts.geographics[0] - - console.log( - `Tidak ada data geografis untuk tahun ${selectedYearNum}, menggunakan data ${geographics.year ? `tahun ${geographics.year}` : "tanpa tahun"}`, - ) - } - - const clickLng = e.lngLat ? e.lngLat.lng : null - const clickLat = e.lngLat ? e.lngLat.lat : null - - if (!geographics) { - console.error("Missing geographics data for district:", districtId) - return - } - - if (!demographics) { - console.error("Missing demographics data for district:", districtId) - return - } - - const district: DistrictFeature = { - id: districtId, - name: feature.properties.nama || feature.properties.kecamatan || "Unknown District", - longitude: geographics.longitude || clickLng || 0, - latitude: geographics.latitude || clickLat || 0, - number_of_crime: crimeData.number_of_crime || 0, - level: crimeData.level || $Enums.crime_rates.low, - 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: crime_incidents || [], - selectedYear: year, - selectedMonth: month, - isFocused: true, // Mark this district as focused - } - - if (!district.longitude || !district.latitude) { - console.error("Invalid district coordinates:", district) - return - } - - selectedDistrictRef.current = district - setFocusedDistrictId(district.id) - console.log("District clicked, selectedDistrictRef set to:", selectedDistrictRef.current) - - // Hide clusters when focusing on a district - if (map.getMap().getLayer("clusters")) { - map.getMap().setLayoutProperty("clusters", "visibility", "none") - } - if (map.getMap().getLayer("unclustered-point")) { - map.getMap().setLayoutProperty("unclustered-point", "visibility", "none") - } - - // Reset bearing before animation - bearingRef.current = 0 - - // Animate to a pitched view focused on the district - map.flyTo({ - center: [district.longitude, district.latitude], - zoom: 12.5, - pitch: 75, - bearing: 0, - duration: 1500, - easing: (t) => t * (2 - t), // easeOutQuad - }) - - // Stop any existing rotation animation - if (rotationAnimationRef.current) { - cancelAnimationFrame(rotationAnimationRef.current) - rotationAnimationRef.current = null - } - - // Improved continuous bearing rotation function - const startRotation = () => { - if (!map || !focusedDistrictId) return - - const rotationSpeed = 0.05 // degrees per frame - - const animate = () => { - if (!map || !focusedDistrictId) { - if (rotationAnimationRef.current) { - cancelAnimationFrame(rotationAnimationRef.current) - rotationAnimationRef.current = null - } - return - } - - // Update bearing with smooth increment - bearingRef.current = (bearingRef.current + rotationSpeed) % 360 - map.setBearing(bearingRef.current) - - // Continue the animation - rotationAnimationRef.current = requestAnimationFrame(animate) - } - - // Start the animation loop - if (rotationAnimationRef.current) { - cancelAnimationFrame(rotationAnimationRef.current) - } - rotationAnimationRef.current = requestAnimationFrame(animate) - } - - // Start rotation after the initial flyTo completes - setTimeout(startRotation, 1600) - - if (onClick) { - onClick(district) - } else { - setSelectedDistrict(district) - } - } - - const handleIncidentClick = useCallback( - (e: any) => { - if (!map) return - - const features = map.queryRenderedFeatures(e.point, { layers: ["unclustered-point"] }) - if (!features || features.length === 0) return - - const incident = features[0] - if (!incident.properties) return - - e.originalEvent.stopPropagation() - e.preventDefault() - - const incidentDetails = { - id: incident.properties.id, - district: incident.properties.district, - category: incident.properties.category, - type: incident.properties.incidentType, - description: incident.properties.description, - status: incident.properties?.status || "Unknown", - longitude: (incident.geometry as any).coordinates[0], - latitude: (incident.geometry as any).coordinates[1], - timestamp: new Date(), - } - - // console.log("Incident clicked:", incidentDetails) - - // Dispatch mapbox_fly_to event instead of direct flyTo - // const flyToEvent = new CustomEvent("mapbox_fly_to", { - // detail: { - // longitude: incidentDetails.longitude, - // latitude: incidentDetails.latitude, - // zoom: 15, - // bearing: 0, - // pitch: 45, - // duration: 2000, - // }, - // bubbles: true, - // }) - - // if (map.getMap().getCanvas()) { - // map.getMap().getCanvas().dispatchEvent(flyToEvent) - // } else { - // document.dispatchEvent(flyToEvent) - // } - - // Dispatch incident_click event after a short delay to allow fly animation - // const customEvent = new CustomEvent("incident_click", { - // detail: incidentDetails, - // bubbles: true, - // }) - // if (map.getMap().getCanvas()) { - // map.getMap().getCanvas().dispatchEvent(customEvent) - // } else { - // document.dispatchEvent(customEvent) - // } - - }, - [map], - ) - - const handleClusterClick = useCallback( - (e: any) => { - if (!map) return - - e.originalEvent.stopPropagation() - e.preventDefault() - - const features = map.queryRenderedFeatures(e.point, { layers: ["clusters"] }) - - if (!features || features.length === 0) return - - const clusterId: number = features[0].properties?.cluster_id as number - ; (map.getSource("crime-incidents") as mapboxgl.GeoJSONSource).getClusterExpansionZoom(clusterId, (err, zoom) => { - if (err) return - - map.easeTo({ - center: (features[0].geometry as any).coordinates, - zoom: zoom ?? undefined, - }) - }) - }, - [map], - ) - - const handleCloseDistrictPopup = useCallback(() => { - console.log("Closing district popup") - selectedDistrictRef.current = null - setSelectedDistrict(null) - setFocusedDistrictId(null) // Clear the focus when popup is closed - - // Cancel rotation animation when closing popup - if (rotationAnimationRef.current) { - cancelAnimationFrame(rotationAnimationRef.current) - rotationAnimationRef.current = null - } - - bearingRef.current = 0 - - // 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.getMap().getLayer("clusters")) { - map.getMap().setLayoutProperty("clusters", "visibility", "visible") - } - if (map.getMap().getLayer("unclustered-point")) { - map.getMap().setLayoutProperty("unclustered-point", "visibility", "visible") - } - } - }, [map]) - - // Add handler for fly-to events - useEffect(() => { - if (!map) return; +// "use client" + +// import { useEffect, useState, useRef, useCallback } from "react" +// import { useMap } from "react-map-gl/mapbox" +// import { BASE_BEARING, BASE_PITCH, BASE_ZOOM, CRIME_RATE_COLORS, MAPBOX_TILESET_ID } from "@/app/_utils/const/map" + +// import { $Enums } from "@prisma/client" +// import DistrictPopup from "../pop-up/district-popup" +// import type { ICrimes } from "@/app/_utils/types/crimes" + +// // Types for district properties +// export interface DistrictFeature { +// id: string +// name: string +// longitude: number +// latitude: number +// number_of_crime: number +// level: $Enums.crime_rates +// demographics: { +// number_of_unemployed: number +// population: number +// population_density: number +// year: number +// } +// geographics: { +// address: string +// land_area: number +// year: number +// latitude: number +// longitude: number +// } +// crime_incidents: Array<{ +// id: string +// timestamp: Date +// description: string +// status: string +// category: string +// type: string +// address: string +// latitude: number +// longitude: number +// }> +// selectedYear?: string +// selectedMonth?: string +// isFocused?: boolean // Add a property to track if district is focused +// } + +// // District layer props +// export interface DistrictLayerProps { +// visible?: boolean // New prop to control visibility +// onClick?: (feature: DistrictFeature) => void +// year: string +// month: string +// filterCategory: string | "all" +// crimes: ICrimes[] +// tilesetId?: string +// } + +// export default function DistrictLayer({ +// visible = true, +// onClick, +// year, +// month, +// filterCategory = "all", +// crimes = [], +// tilesetId = MAPBOX_TILESET_ID, +// }: DistrictLayerProps) { +// const { current: map } = useMap() + +// const [hoverInfo, setHoverInfo] = useState<{ +// x: number +// y: number +// feature: any +// } | null>(null) + +// const selectedDistrictRef = useRef(null) +// const [selectedDistrict, setSelectedDistrict] = useState(null) +// const [focusedDistrictId, setFocusedDistrictId] = useState(null) +// const rotationAnimationRef = useRef(null) +// const bearingRef = useRef(0) +// const layersAdded = useRef(false) + +// const crimeDataByDistrict = crimes.reduce( +// (acc, crime) => { +// const districtId = crime.district_id + +// acc[districtId] = { +// number_of_crime: crime.number_of_crime, +// level: crime.level, +// } +// return acc +// }, +// {} as Record, +// ) + +// const handleDistrictClick = (e: any) => { +// const incidentFeatures = map?.queryRenderedFeatures(e.point, { layers: ["unclustered-point", "clusters"] }) + +// if (incidentFeatures && incidentFeatures.length > 0) { +// return +// } + +// if (!map || !e.features || e.features.length === 0) return + +// const feature = e.features[0] +// const districtId = feature.properties.kode_kec + +// // If clicking the same district, deselect it +// if (focusedDistrictId === districtId) { +// setFocusedDistrictId(null) +// selectedDistrictRef.current = null +// setSelectedDistrict(null) + +// // Reset animation and map view when deselecting +// if (rotationAnimationRef.current) { +// cancelAnimationFrame(rotationAnimationRef.current) +// rotationAnimationRef.current = null +// } + +// bearingRef.current = 0 + +// // Reset pitch and bearing with animation +// map.easeTo({ +// pitch: BASE_PITCH, +// bearing: 0, +// duration: 1500, +// easing: (t) => t * (2 - t), // easeOutQuad +// }) + +// // Show all clusters again when unfocusing +// if (map.getMap().getLayer("clusters")) { +// map.getMap().setLayoutProperty("clusters", "visibility", "visible") +// } +// if (map.getMap().getLayer("unclustered-point")) { +// map.getMap().setLayoutProperty("unclustered-point", "visibility", "visible") +// } + +// return +// } + +// const crimeData = crimeDataByDistrict[districtId] || {} + +// let crime_incidents: Array<{ +// id: string +// timestamp: Date +// description: string +// status: string +// category: string +// type: string +// address: string +// latitude: number +// longitude: number +// }> = [] + +// const districtCrimes = crimes.filter((crime) => crime.district_id === districtId) + +// districtCrimes.forEach((crimeRecord) => { +// if (crimeRecord && crimeRecord.crime_incidents) { +// const incidents = crimeRecord.crime_incidents.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 || 0, +// longitude: incident.locations?.longitude || 0, +// })) + +// crime_incidents = [...crime_incidents, ...incidents] +// } +// }) + +// const firstDistrictCrime = districtCrimes.length > 0 ? districtCrimes[0] : null + +// const selectedYearNum = year ? Number.parseInt(year) : new Date().getFullYear() + +// let demographics = firstDistrictCrime?.districts.demographics?.find((d) => d.year === selectedYearNum) + +// if (!demographics && firstDistrictCrime?.districts.demographics?.length) { +// demographics = firstDistrictCrime.districts.demographics.sort((a, b) => b.year - a.year)[0] +// console.log( +// `Tidak ada data demografis untuk tahun ${selectedYearNum}, menggunakan data tahun ${demographics.year}`, +// ) +// } + +// let geographics = firstDistrictCrime?.districts.geographics?.find((g) => g.year === selectedYearNum) + +// if (!geographics && firstDistrictCrime?.districts.geographics?.length) { +// const validGeographics = firstDistrictCrime.districts.geographics +// .filter((g) => g.year !== null) +// .sort((a, b) => (b.year || 0) - (a.year || 0)) + +// geographics = validGeographics.length > 0 ? validGeographics[0] : firstDistrictCrime.districts.geographics[0] + +// console.log( +// `Tidak ada data geografis untuk tahun ${selectedYearNum}, menggunakan data ${geographics.year ? `tahun ${geographics.year}` : "tanpa tahun"}`, +// ) +// } + +// const clickLng = e.lngLat ? e.lngLat.lng : null +// const clickLat = e.lngLat ? e.lngLat.lat : null + +// if (!geographics) { +// console.error("Missing geographics data for district:", districtId) +// return +// } + +// if (!demographics) { +// console.error("Missing demographics data for district:", districtId) +// return +// } + +// const district: DistrictFeature = { +// id: districtId, +// name: feature.properties.nama || feature.properties.kecamatan || "Unknown District", +// longitude: geographics.longitude || clickLng || 0, +// latitude: geographics.latitude || clickLat || 0, +// number_of_crime: crimeData.number_of_crime || 0, +// level: crimeData.level || $Enums.crime_rates.low, +// 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: crime_incidents || [], +// selectedYear: year, +// selectedMonth: month, +// isFocused: true, // Mark this district as focused +// } + +// if (!district.longitude || !district.latitude) { +// console.error("Invalid district coordinates:", district) +// return +// } + +// selectedDistrictRef.current = district +// setFocusedDistrictId(district.id) +// console.log("District clicked, selectedDistrictRef set to:", selectedDistrictRef.current) + +// // Hide clusters when focusing on a district +// if (map.getMap().getLayer("clusters")) { +// map.getMap().setLayoutProperty("clusters", "visibility", "none") +// } +// if (map.getMap().getLayer("unclustered-point")) { +// map.getMap().setLayoutProperty("unclustered-point", "visibility", "none") +// } + +// // Reset bearing before animation +// bearingRef.current = 0 + +// // Animate to a pitched view focused on the district +// map.flyTo({ +// center: [district.longitude, district.latitude], +// zoom: 12.5, +// pitch: 75, +// bearing: 0, +// duration: 1500, +// easing: (t) => t * (2 - t), // easeOutQuad +// }) + +// // Stop any existing rotation animation +// if (rotationAnimationRef.current) { +// cancelAnimationFrame(rotationAnimationRef.current) +// rotationAnimationRef.current = null +// } + +// // Improved continuous bearing rotation function +// const startRotation = () => { +// if (!map || !focusedDistrictId) return + +// const rotationSpeed = 0.05 // degrees per frame + +// const animate = () => { +// if (!map || !focusedDistrictId) { +// if (rotationAnimationRef.current) { +// cancelAnimationFrame(rotationAnimationRef.current) +// rotationAnimationRef.current = null +// } +// return +// } + +// // Update bearing with smooth increment +// bearingRef.current = (bearingRef.current + rotationSpeed) % 360 +// map.setBearing(bearingRef.current) + +// // Continue the animation +// rotationAnimationRef.current = requestAnimationFrame(animate) +// } + +// // Start the animation loop +// if (rotationAnimationRef.current) { +// cancelAnimationFrame(rotationAnimationRef.current) +// } +// rotationAnimationRef.current = requestAnimationFrame(animate) +// } + +// // Start rotation after the initial flyTo completes +// setTimeout(startRotation, 1600) + +// if (onClick) { +// onClick(district) +// } else { +// setSelectedDistrict(district) +// } +// } + +// const handleIncidentClick = useCallback( +// (e: any) => { +// if (!map) return + +// const features = map.queryRenderedFeatures(e.point, { layers: ["unclustered-point"] }) +// if (!features || features.length === 0) return + +// const incident = features[0] +// if (!incident.properties) return + +// e.originalEvent.stopPropagation() +// e.preventDefault() + +// const incidentDetails = { +// id: incident.properties.id, +// district: incident.properties.district, +// category: incident.properties.category, +// type: incident.properties.incidentType, +// description: incident.properties.description, +// status: incident.properties?.status || "Unknown", +// longitude: (incident.geometry as any).coordinates[0], +// latitude: (incident.geometry as any).coordinates[1], +// timestamp: new Date(), +// } + +// // console.log("Incident clicked:", incidentDetails) + +// // Dispatch mapbox_fly_to event instead of direct flyTo +// // const flyToEvent = new CustomEvent("mapbox_fly_to", { +// // detail: { +// // longitude: incidentDetails.longitude, +// // latitude: incidentDetails.latitude, +// // zoom: 15, +// // bearing: 0, +// // pitch: 45, +// // duration: 2000, +// // }, +// // bubbles: true, +// // }) + +// // if (map.getMap().getCanvas()) { +// // map.getMap().getCanvas().dispatchEvent(flyToEvent) +// // } else { +// // document.dispatchEvent(flyToEvent) +// // } + +// // Dispatch incident_click event after a short delay to allow fly animation +// // const customEvent = new CustomEvent("incident_click", { +// // detail: incidentDetails, +// // bubbles: true, +// // }) +// // if (map.getMap().getCanvas()) { +// // map.getMap().getCanvas().dispatchEvent(customEvent) +// // } else { +// // document.dispatchEvent(customEvent) +// // } + +// }, +// [map], +// ) + +// const handleClusterClick = useCallback( +// (e: any) => { +// if (!map) return + +// e.originalEvent.stopPropagation() +// e.preventDefault() + +// const features = map.queryRenderedFeatures(e.point, { layers: ["clusters"] }) + +// if (!features || features.length === 0) return + +// const clusterId: number = features[0].properties?.cluster_id as number +// ; (map.getSource("crime-incidents") as mapboxgl.GeoJSONSource).getClusterExpansionZoom(clusterId, (err, zoom) => { +// if (err) return + +// map.easeTo({ +// center: (features[0].geometry as any).coordinates, +// zoom: zoom ?? undefined, +// }) +// }) +// }, +// [map], +// ) + +// const handleCloseDistrictPopup = useCallback(() => { +// console.log("Closing district popup") +// selectedDistrictRef.current = null +// setSelectedDistrict(null) +// setFocusedDistrictId(null) // Clear the focus when popup is closed + +// // Cancel rotation animation when closing popup +// if (rotationAnimationRef.current) { +// cancelAnimationFrame(rotationAnimationRef.current) +// rotationAnimationRef.current = null +// } + +// bearingRef.current = 0 + +// // 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.getMap().getLayer("clusters")) { +// map.getMap().setLayoutProperty("clusters", "visibility", "visible") +// } +// if (map.getMap().getLayer("unclustered-point")) { +// map.getMap().setLayoutProperty("unclustered-point", "visibility", "visible") +// } +// } +// }, [map]) + +// // Add handler for fly-to events +// useEffect(() => { +// if (!map) return; - const handleFlyToEvent = (e: Event) => { - const customEvent = e as CustomEvent; - if (!map || !customEvent.detail) return; +// const handleFlyToEvent = (e: Event) => { +// const customEvent = e as CustomEvent; +// if (!map || !customEvent.detail) return; - const { longitude, latitude, zoom, bearing, pitch, duration } = customEvent.detail; +// 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 - }); +// map.flyTo({ +// center: [longitude, latitude], +// zoom: zoom || 15, +// bearing: bearing || 0, +// pitch: pitch || 45, +// duration: duration || 2000 +// }); - // Add a highlight or pulse effect to the target incident - // This could be implemented by adding a temporary marker or animation - // at the target coordinates - if (map.getMap().getLayer('target-incident-highlight')) { - map.getMap().removeLayer('target-incident-highlight'); - } +// // Add a highlight or pulse effect to the target incident +// // This could be implemented by adding a temporary marker or animation +// // at the target coordinates +// if (map.getMap().getLayer('target-incident-highlight')) { +// map.getMap().removeLayer('target-incident-highlight'); +// } - if (map.getMap().getSource('target-incident-highlight')) { - map.getMap().removeSource('target-incident-highlight'); - } +// if (map.getMap().getSource('target-incident-highlight')) { +// map.getMap().removeSource('target-incident-highlight'); +// } - map.getMap().addSource('target-incident-highlight', { - type: 'geojson', - data: { - type: 'Feature', - geometry: { - type: 'Point', - coordinates: [longitude, latitude] - }, - properties: {} - } - }); +// map.getMap().addSource('target-incident-highlight', { +// type: 'geojson', +// data: { +// type: 'Feature', +// geometry: { +// type: 'Point', +// coordinates: [longitude, latitude] +// }, +// properties: {} +// } +// }); - map.getMap().addLayer({ - id: 'target-incident-highlight', - source: 'target-incident-highlight', - type: 'circle', - paint: { - 'circle-radius': [ - 'interpolate', ['linear'], ['zoom'], - 10, 10, - 15, 15, - 20, 20 - ], - 'circle-color': '#ff0000', - 'circle-opacity': 0.7, - 'circle-stroke-width': 2, - 'circle-stroke-color': '#ffffff' - } - }); +// map.getMap().addLayer({ +// id: 'target-incident-highlight', +// source: 'target-incident-highlight', +// type: 'circle', +// paint: { +// 'circle-radius': [ +// 'interpolate', ['linear'], ['zoom'], +// 10, 10, +// 15, 15, +// 20, 20 +// ], +// 'circle-color': '#ff0000', +// 'circle-opacity': 0.7, +// 'circle-stroke-width': 2, +// 'circle-stroke-color': '#ffffff' +// } +// }); - // Add a pulsing effect using animations - let size = 10; - const animatePulse = () => { - if (!map || !map.getMap().getLayer('target-incident-highlight')) return; +// // Add a pulsing effect using animations +// let size = 10; +// const animatePulse = () => { +// if (!map || !map.getMap().getLayer('target-incident-highlight')) return; - size = (size % 20) + 1; +// size = (size % 20) + 1; - map.getMap().setPaintProperty('target-incident-highlight', 'circle-radius', [ - 'interpolate', ['linear'], ['zoom'], - 10, size, - 15, size * 1.5, - 20, size * 2 - ]); +// map.getMap().setPaintProperty('target-incident-highlight', 'circle-radius', [ +// 'interpolate', ['linear'], ['zoom'], +// 10, size, +// 15, size * 1.5, +// 20, size * 2 +// ]); - requestAnimationFrame(animatePulse); - }; +// requestAnimationFrame(animatePulse); +// }; - requestAnimationFrame(animatePulse); - }; +// requestAnimationFrame(animatePulse); +// }; - map.getMap().getCanvas().addEventListener('mapbox_fly_to', handleFlyToEvent as EventListener); +// map.getMap().getCanvas().addEventListener('mapbox_fly_to', handleFlyToEvent as EventListener); - return () => { - if (map && map.getMap() && map.getMap().getCanvas()) { - map.getMap().getCanvas().removeEventListener('mapbox_fly_to', handleFlyToEvent as EventListener); - } - }; - }, [map]); - - useEffect(() => { - if (!map || !visible) return - - const onStyleLoad = () => { - if (!map) return - - try { - if (!map.getMap().getSource("districts")) { - const layers = map.getStyle().layers - let firstSymbolId: string | undefined - for (const layer of layers) { - if (layer.type === "symbol") { - firstSymbolId = layer.id - break - } - } - - map.getMap().addSource("districts", { - type: "vector", - url: `mapbox://${tilesetId}`, - }) - - const fillColorExpression: any = [ - "case", - ["has", "kode_kec"], - [ - "match", - ["get", "kode_kec"], - ...(focusedDistrictId - ? [ - [ - focusedDistrictId, - crimeDataByDistrict[focusedDistrictId]?.level === "low" - ? CRIME_RATE_COLORS.low - : crimeDataByDistrict[focusedDistrictId]?.level === "medium" - ? CRIME_RATE_COLORS.medium - : crimeDataByDistrict[focusedDistrictId]?.level === "high" - ? CRIME_RATE_COLORS.high - : CRIME_RATE_COLORS.default, - ], - "rgba(0,0,0,0.05)", - ] - : Object.entries(crimeDataByDistrict).flatMap(([districtId, data]) => { - return [ - districtId, - data.level === "low" - ? CRIME_RATE_COLORS.low - : data.level === "medium" - ? CRIME_RATE_COLORS.medium - : data.level === "high" - ? CRIME_RATE_COLORS.high - : CRIME_RATE_COLORS.default, - ] - })), - focusedDistrictId ? "rgba(0,0,0,0.05)" : CRIME_RATE_COLORS.default, - ], - CRIME_RATE_COLORS.default, - ] - - if (!map.getMap().getLayer("district-fill")) { - map.getMap().addLayer( - { - id: "district-fill", - type: "fill", - source: "districts", - "source-layer": "Districts", - paint: { - "fill-color": fillColorExpression, - "fill-opacity": 0.6, - }, - }, - firstSymbolId, - ) - } - - if (!map.getMap().getLayer("district-line")) { - map.getMap().addLayer( - { - id: "district-line", - type: "line", - source: "districts", - "source-layer": "Districts", - paint: { - "line-color": "#ffffff", - "line-width": 1, - "line-opacity": 0.5, - }, - }, - firstSymbolId, - ) - } - - if (!map.getMap().getLayer("district-extrusion")) { - map.getMap().addLayer( - { - id: "district-extrusion", - type: "fill-extrusion", - source: "districts", - "source-layer": "Districts", - paint: { - "fill-extrusion-color": [ - "case", - ["has", "kode_kec"], - [ - "match", - ["get", "kode_kec"], - focusedDistrictId || "", - crimeDataByDistrict[focusedDistrictId || ""]?.level === "low" - ? CRIME_RATE_COLORS.low - : crimeDataByDistrict[focusedDistrictId || ""]?.level === "medium" - ? CRIME_RATE_COLORS.medium - : crimeDataByDistrict[focusedDistrictId || ""]?.level === "high" - ? CRIME_RATE_COLORS.high - : CRIME_RATE_COLORS.default, - "transparent", - ], - "transparent", - ], - "fill-extrusion-height": [ - "case", - ["has", "kode_kec"], - ["match", ["get", "kode_kec"], focusedDistrictId || "", 500, 0], - 0, - ], - "fill-extrusion-base": 0, - "fill-extrusion-opacity": 0.8, - }, - filter: ["==", ["get", "kode_kec"], focusedDistrictId || ""], - }, - firstSymbolId, - ) - } - - if (crimes.length > 0 && !map.getMap().getSource("crime-incidents")) { - const allIncidents = crimes.flatMap((crime) => { - let filteredIncidents = crime.crime_incidents - - if (filterCategory !== "all") { - filteredIncidents = crime.crime_incidents.filter( - (incident) => incident.crime_categories.name === filterCategory, - ) - } - - return filteredIncidents.map((incident) => ({ - type: "Feature" as const, - properties: { - id: incident.id, - district: crime.districts.name, - category: incident.crime_categories.name, - incidentType: incident.crime_categories.type, - level: crime.level, - description: incident.description, - }, - geometry: { - type: "Point" as const, - coordinates: [incident.locations.longitude, incident.locations.latitude], - }, - })) - }) - - map.getMap().addSource("crime-incidents", { - type: "geojson", - data: { - type: "FeatureCollection", - features: allIncidents, - }, - cluster: true, - clusterMaxZoom: 14, - clusterRadius: 50, - }) - - if (!map.getMap().getLayer("clusters")) { - map.getMap().addLayer( - { - id: "clusters", - type: "circle", - source: "crime-incidents", - filter: ["has", "point_count"], - paint: { - "circle-color": ["step", ["get", "point_count"], "#51bbd6", 5, "#f1f075", 15, "#f28cb1"], - "circle-radius": ["step", ["get", "point_count"], 20, 5, 30, 15, 40], - "circle-opacity": 0.75, - }, - layout: { - visibility: "visible", - }, - }, - firstSymbolId, - ) - } - - if (!map.getMap().getLayer("cluster-count")) { - map.getMap().addLayer({ - id: "cluster-count", - type: "symbol", - source: "crime-incidents", - filter: ["has", "point_count"], - layout: { - "text-field": "{point_count_abbreviated}", - "text-font": ["DIN Offc Pro Medium", "Arial Unicode MS Bold"], - "text-size": 12, - }, - paint: { - "text-color": "#ffffff", - }, - }) - } - - if (!map.getMap().getLayer("unclustered-point")) { - map.getMap().addLayer( - { - id: "unclustered-point", - type: "circle", - source: "crime-incidents", - filter: ["!", ["has", "point_count"]], - paint: { - "circle-color": "#11b4da", - "circle-radius": 8, - "circle-stroke-width": 1, - "circle-stroke-color": "#fff", - }, - layout: { - visibility: "visible", - }, - }, - firstSymbolId, - ) - } - - map.on("mouseenter", "clusters", () => { - map.getCanvas().style.cursor = "pointer" - }) - - map.on("mouseleave", "clusters", () => { - map.getCanvas().style.cursor = "" - }) - - map.on("mouseenter", "unclustered-point", () => { - map.getCanvas().style.cursor = "pointer" - }) - - map.on("mouseleave", "unclustered-point", () => { - map.getCanvas().style.cursor = "" - }) - - map.on("mouseenter", "district-fill", () => { - map.getCanvas().style.cursor = "pointer" - }) - - map.on("mouseleave", "district-fill", () => { - map.getCanvas().style.cursor = "" - }) - - map.off("click", "clusters", handleClusterClick) - map.off("click", "unclustered-point", handleIncidentClick) - - map.on("click", "clusters", handleClusterClick) - map.on("click", "unclustered-point", handleIncidentClick) - - map.off("click", "district-fill", handleDistrictClick) - map.on("click", "district-fill", handleDistrictClick) - - map.on("mouseleave", "district-fill", () => setHoverInfo(null)) - - layersAdded.current = true - } - } else { - if (map.getMap().getLayer("district-fill")) { - map.getMap().setPaintProperty("district-fill", "fill-color", [ - "case", - ["has", "kode_kec"], - [ - "match", - ["get", "kode_kec"], - ...(focusedDistrictId - ? [ - [ - focusedDistrictId, - crimeDataByDistrict[focusedDistrictId]?.level === "low" - ? CRIME_RATE_COLORS.low - : crimeDataByDistrict[focusedDistrictId]?.level === "medium" - ? CRIME_RATE_COLORS.medium - : crimeDataByDistrict[focusedDistrictId]?.level === "high" - ? CRIME_RATE_COLORS.high - : CRIME_RATE_COLORS.default, - ], - "rgba(0,0,0,0.05)", - ] - : Object.entries(crimeDataByDistrict).flatMap(([districtId, data]) => { - return [ - districtId, - data.level === "low" - ? CRIME_RATE_COLORS.low - : data.level === "medium" - ? CRIME_RATE_COLORS.medium - : data.level === "high" - ? CRIME_RATE_COLORS.high - : CRIME_RATE_COLORS.default, - ] - })), - focusedDistrictId ? "rgba(0,0,0,0.05)" : CRIME_RATE_COLORS.default, - ], - CRIME_RATE_COLORS.default, - ] as any) - } - - // Re-add click handler to ensure it works after timelapse - map.off("click", "district-fill", handleDistrictClick) - map.on("click", "district-fill", handleDistrictClick) - } - } catch (error) { - console.error("Error adding district layers:", error) - } - } - - if (map.isStyleLoaded()) { - onStyleLoad() - } else { - map.once("style.load", onStyleLoad) - } - - return () => { - if (map) { - map.off("click", "district-fill", handleDistrictClick) - } - } - }, [map, visible, tilesetId, crimes, filterCategory, year, month, handleIncidentClick, handleClusterClick]) - - // Add an additional effect to ensure click handlers are properly maintained - useEffect(() => { - if (!map || !layersAdded.current) return - - // Re-attach the click handler after any data changes - if (map.getMap().getLayer("district-fill")) { - map.off("click", "district-fill", handleDistrictClick) - map.on("click", "district-fill", handleDistrictClick) - } - - // Ensure district-extrusion settings are maintained after timelapse - if (map.getMap().getLayer("district-extrusion")) { - map.getMap().setFilter("district-extrusion", ["==", ["get", "kode_kec"], focusedDistrictId || ""]); - - // Re-apply the extrusion color based on the focused district - map.getMap().setPaintProperty("district-extrusion", "fill-extrusion-color", [ - "case", - ["has", "kode_kec"], - [ - "match", - ["get", "kode_kec"], - focusedDistrictId || "", - crimeDataByDistrict[focusedDistrictId || ""]?.level === "low" - ? CRIME_RATE_COLORS.low - : crimeDataByDistrict[focusedDistrictId || ""]?.level === "medium" - ? CRIME_RATE_COLORS.medium - : crimeDataByDistrict[focusedDistrictId || ""]?.level === "high" - ? CRIME_RATE_COLORS.high - : CRIME_RATE_COLORS.default, - "transparent", - ], - "transparent", - ]); - - // Ensure extrusion height is restored if needed - if (focusedDistrictId) { - map.getMap().setPaintProperty("district-extrusion", "fill-extrusion-height", [ - "case", - ["has", "kode_kec"], - ["match", ["get", "kode_kec"], focusedDistrictId, 800, 0], - 0, - ]); - } - } - - return () => { - if (map) { - map.off("click", "district-fill", handleDistrictClick) - } - } - }, [map, year, month, crimeDataByDistrict, focusedDistrictId]) - - useEffect(() => { - if (!map || !layersAdded.current) return - - try { - if (map.getMap().getLayer("district-fill")) { - const colorEntries = focusedDistrictId - ? [ - [ - focusedDistrictId, - crimeDataByDistrict[focusedDistrictId]?.level === "low" - ? CRIME_RATE_COLORS.low - : crimeDataByDistrict[focusedDistrictId]?.level === "medium" - ? CRIME_RATE_COLORS.medium - : crimeDataByDistrict[focusedDistrictId]?.level === "high" - ? CRIME_RATE_COLORS.high - : CRIME_RATE_COLORS.default, - ], - "rgba(0,0,0,0.05)", - ] - : Object.entries(crimeDataByDistrict).flatMap(([districtId, data]) => { - if (!data || !data.level) { - return [districtId, CRIME_RATE_COLORS.default] - } - - return [ - districtId, - data.level === "low" - ? CRIME_RATE_COLORS.low - : data.level === "medium" - ? CRIME_RATE_COLORS.medium - : data.level === "high" - ? CRIME_RATE_COLORS.high - : CRIME_RATE_COLORS.default, - ] - }) - - const fillColorExpression = [ - "case", - ["has", "kode_kec"], - [ - "match", - ["get", "kode_kec"], - ...colorEntries, - focusedDistrictId ? "rgba(0,0,0,0.05)" : CRIME_RATE_COLORS.default, - ], - CRIME_RATE_COLORS.default, - ] as any - - map.getMap().setPaintProperty("district-fill", "fill-color", fillColorExpression) - } - - if (map.getMap().getLayer("district-extrusion")) { - map.getMap().setFilter("district-extrusion", ["==", ["get", "kode_kec"], focusedDistrictId || ""]) - - map - .getMap() - .setPaintProperty("district-extrusion", "fill-extrusion-color", [ - "case", - ["has", "kode_kec"], - [ - "match", - ["get", "kode_kec"], - focusedDistrictId || "", - crimeDataByDistrict[focusedDistrictId || ""]?.level === "low" - ? CRIME_RATE_COLORS.low - : crimeDataByDistrict[focusedDistrictId || ""]?.level === "medium" - ? CRIME_RATE_COLORS.medium - : crimeDataByDistrict[focusedDistrictId || ""]?.level === "high" - ? CRIME_RATE_COLORS.high - : CRIME_RATE_COLORS.default, - "transparent", - ], - "transparent", - ]) - - if (focusedDistrictId) { - const startHeight = 0 - const targetHeight = 800 - const duration = 700 - const startTime = performance.now() - - const animateHeight = (currentTime: number) => { - const elapsed = currentTime - startTime - const progress = Math.min(elapsed / duration, 1) - const easedProgress = progress * (2 - progress) - const currentHeight = startHeight + (targetHeight - startHeight) * easedProgress - - map - .getMap() - .setPaintProperty("district-extrusion", "fill-extrusion-height", [ - "case", - ["has", "kode_kec"], - ["match", ["get", "kode_kec"], focusedDistrictId, currentHeight, 0], - 0, - ]) - - if (progress < 1) { - requestAnimationFrame(animateHeight) - } - } - - requestAnimationFrame(animateHeight) - } else { - const startHeight = 800 - const targetHeight = 0 - const duration = 500 - const startTime = performance.now() - - const animateHeightDown = (currentTime: number) => { - const elapsed = currentTime - startTime - const progress = Math.min(elapsed / duration, 1) - const easedProgress = progress * (2 - progress) - const currentHeight = startHeight + (targetHeight - startHeight) * easedProgress - - map - .getMap() - .setPaintProperty("district-extrusion", "fill-extrusion-height", [ - "case", - ["has", "kode_kec"], - ["match", ["get", "kode_kec"], "", currentHeight, 0], - 0, - ]) - - if (progress < 1) { - requestAnimationFrame(animateHeightDown) - } - } - - requestAnimationFrame(animateHeightDown) - } - } - } catch (error) { - console.error("Error updating district layer:", error) - } - }, [map, crimes, crimeDataByDistrict, focusedDistrictId]) - - useEffect(() => { - if (!map || !map.getMap().getSource("crime-incidents")) return - - try { - const allIncidents = crimes.flatMap((crime) => { - if (!crime.crime_incidents) return [] - - let filteredIncidents = crime.crime_incidents - - if (filterCategory !== "all") { - filteredIncidents = crime.crime_incidents.filter( - (incident) => incident.crime_categories && incident.crime_categories.name === filterCategory, - ) - } - - return filteredIncidents - .map((incident) => { - if (!incident.locations) { - console.warn("Missing location for incident:", incident.id) - return null - } - - return { - type: "Feature" as const, - properties: { - id: incident.id, - district: crime.districts?.name || "Unknown", - category: incident.crime_categories?.name || "Unknown", - incidentType: incident.crime_categories?.type || "Unknown", - level: crime.level || "low", - description: incident.description || "", - }, - geometry: { - type: "Point" as const, - coordinates: [incident.locations.longitude || 0, incident.locations.latitude || 0], - }, - } - }) - .filter(Boolean) - }) - ; (map.getMap().getSource("crime-incidents") as mapboxgl.GeoJSONSource).setData({ - type: "FeatureCollection", - features: allIncidents as GeoJSON.Feature[], - }) - } catch (error) { - console.error("Error updating incident data:", error) - } - }, [map, crimes, filterCategory]) - - useEffect(() => { - if (selectedDistrictRef.current) { - const districtId = selectedDistrictRef.current.id - const crimeData = crimeDataByDistrict[districtId] || {} - - 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: DistrictFeature = { - ...selectedDistrictRef.current, - number_of_crime: crimeData.number_of_crime || 0, - level: crimeData.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]) - - useEffect(() => { - return () => { - if (rotationAnimationRef.current) { - cancelAnimationFrame(rotationAnimationRef.current) - rotationAnimationRef.current = null - } - } - }, []) - - if (!visible) return null - - return ( - <> - {selectedDistrict && ( - - )} - - ) -} +// return () => { +// if (map && map.getMap() && map.getMap().getCanvas()) { +// map.getMap().getCanvas().removeEventListener('mapbox_fly_to', handleFlyToEvent as EventListener); +// } +// }; +// }, [map]); + +// useEffect(() => { +// if (!map || !visible) return + +// const onStyleLoad = () => { +// if (!map) return + +// try { +// if (!map.getMap().getSource("districts")) { +// const layers = map.getStyle().layers +// let firstSymbolId: string | undefined +// for (const layer of layers) { +// if (layer.type === "symbol") { +// firstSymbolId = layer.id +// break +// } +// } + +// map.getMap().addSource("districts", { +// type: "vector", +// url: `mapbox://${tilesetId}`, +// }) + +// const fillColorExpression: any = [ +// "case", +// ["has", "kode_kec"], +// [ +// "match", +// ["get", "kode_kec"], +// ...(focusedDistrictId +// ? [ +// [ +// focusedDistrictId, +// crimeDataByDistrict[focusedDistrictId]?.level === "low" +// ? CRIME_RATE_COLORS.low +// : crimeDataByDistrict[focusedDistrictId]?.level === "medium" +// ? CRIME_RATE_COLORS.medium +// : crimeDataByDistrict[focusedDistrictId]?.level === "high" +// ? CRIME_RATE_COLORS.high +// : CRIME_RATE_COLORS.default, +// ], +// "rgba(0,0,0,0.05)", +// ] +// : Object.entries(crimeDataByDistrict).flatMap(([districtId, data]) => { +// return [ +// districtId, +// data.level === "low" +// ? CRIME_RATE_COLORS.low +// : data.level === "medium" +// ? CRIME_RATE_COLORS.medium +// : data.level === "high" +// ? CRIME_RATE_COLORS.high +// : CRIME_RATE_COLORS.default, +// ] +// })), +// focusedDistrictId ? "rgba(0,0,0,0.05)" : CRIME_RATE_COLORS.default, +// ], +// CRIME_RATE_COLORS.default, +// ] + +// if (!map.getMap().getLayer("district-fill")) { +// map.getMap().addLayer( +// { +// id: "district-fill", +// type: "fill", +// source: "districts", +// "source-layer": "Districts", +// paint: { +// "fill-color": fillColorExpression, +// "fill-opacity": 0.6, +// }, +// }, +// firstSymbolId, +// ) +// } + +// if (!map.getMap().getLayer("district-line")) { +// map.getMap().addLayer( +// { +// id: "district-line", +// type: "line", +// source: "districts", +// "source-layer": "Districts", +// paint: { +// "line-color": "#ffffff", +// "line-width": 1, +// "line-opacity": 0.5, +// }, +// }, +// firstSymbolId, +// ) +// } + +// if (!map.getMap().getLayer("district-extrusion")) { +// map.getMap().addLayer( +// { +// id: "district-extrusion", +// type: "fill-extrusion", +// source: "districts", +// "source-layer": "Districts", +// paint: { +// "fill-extrusion-color": [ +// "case", +// ["has", "kode_kec"], +// [ +// "match", +// ["get", "kode_kec"], +// focusedDistrictId || "", +// crimeDataByDistrict[focusedDistrictId || ""]?.level === "low" +// ? CRIME_RATE_COLORS.low +// : crimeDataByDistrict[focusedDistrictId || ""]?.level === "medium" +// ? CRIME_RATE_COLORS.medium +// : crimeDataByDistrict[focusedDistrictId || ""]?.level === "high" +// ? CRIME_RATE_COLORS.high +// : CRIME_RATE_COLORS.default, +// "transparent", +// ], +// "transparent", +// ], +// "fill-extrusion-height": [ +// "case", +// ["has", "kode_kec"], +// ["match", ["get", "kode_kec"], focusedDistrictId || "", 500, 0], +// 0, +// ], +// "fill-extrusion-base": 0, +// "fill-extrusion-opacity": 0.8, +// }, +// filter: ["==", ["get", "kode_kec"], focusedDistrictId || ""], +// }, +// firstSymbolId, +// ) +// } + +// if (crimes.length > 0 && !map.getMap().getSource("crime-incidents")) { +// const allIncidents = crimes.flatMap((crime) => { +// let filteredIncidents = crime.crime_incidents + +// if (filterCategory !== "all") { +// filteredIncidents = crime.crime_incidents.filter( +// (incident) => incident.crime_categories.name === filterCategory, +// ) +// } + +// return filteredIncidents.map((incident) => ({ +// type: "Feature" as const, +// properties: { +// id: incident.id, +// district: crime.districts.name, +// category: incident.crime_categories.name, +// incidentType: incident.crime_categories.type, +// level: crime.level, +// description: incident.description, +// }, +// geometry: { +// type: "Point" as const, +// coordinates: [incident.locations.longitude, incident.locations.latitude], +// }, +// })) +// }) + +// map.getMap().addSource("crime-incidents", { +// type: "geojson", +// data: { +// type: "FeatureCollection", +// features: allIncidents, +// }, +// cluster: true, +// clusterMaxZoom: 14, +// clusterRadius: 50, +// }) + +// if (!map.getMap().getLayer("clusters")) { +// map.getMap().addLayer( +// { +// id: "clusters", +// type: "circle", +// source: "crime-incidents", +// filter: ["has", "point_count"], +// paint: { +// "circle-color": ["step", ["get", "point_count"], "#51bbd6", 5, "#f1f075", 15, "#f28cb1"], +// "circle-radius": ["step", ["get", "point_count"], 20, 5, 30, 15, 40], +// "circle-opacity": 0.75, +// }, +// layout: { +// visibility: "visible", +// }, +// }, +// firstSymbolId, +// ) +// } + +// if (!map.getMap().getLayer("cluster-count")) { +// map.getMap().addLayer({ +// id: "cluster-count", +// type: "symbol", +// source: "crime-incidents", +// filter: ["has", "point_count"], +// layout: { +// "text-field": "{point_count_abbreviated}", +// "text-font": ["DIN Offc Pro Medium", "Arial Unicode MS Bold"], +// "text-size": 12, +// }, +// paint: { +// "text-color": "#ffffff", +// }, +// }) +// } + +// if (!map.getMap().getLayer("unclustered-point")) { +// map.getMap().addLayer( +// { +// id: "unclustered-point", +// type: "circle", +// source: "crime-incidents", +// filter: ["!", ["has", "point_count"]], +// paint: { +// "circle-color": "#11b4da", +// "circle-radius": 8, +// "circle-stroke-width": 1, +// "circle-stroke-color": "#fff", +// }, +// layout: { +// visibility: "visible", +// }, +// }, +// firstSymbolId, +// ) +// } + +// map.on("mouseenter", "clusters", () => { +// map.getCanvas().style.cursor = "pointer" +// }) + +// map.on("mouseleave", "clusters", () => { +// map.getCanvas().style.cursor = "" +// }) + +// map.on("mouseenter", "unclustered-point", () => { +// map.getCanvas().style.cursor = "pointer" +// }) + +// map.on("mouseleave", "unclustered-point", () => { +// map.getCanvas().style.cursor = "" +// }) + +// map.on("mouseenter", "district-fill", () => { +// map.getCanvas().style.cursor = "pointer" +// }) + +// map.on("mouseleave", "district-fill", () => { +// map.getCanvas().style.cursor = "" +// }) + +// map.off("click", "clusters", handleClusterClick) +// map.off("click", "unclustered-point", handleIncidentClick) + +// map.on("click", "clusters", handleClusterClick) +// map.on("click", "unclustered-point", handleIncidentClick) + +// map.off("click", "district-fill", handleDistrictClick) +// map.on("click", "district-fill", handleDistrictClick) + +// map.on("mouseleave", "district-fill", () => setHoverInfo(null)) + +// layersAdded.current = true +// } +// } else { +// if (map.getMap().getLayer("district-fill")) { +// map.getMap().setPaintProperty("district-fill", "fill-color", [ +// "case", +// ["has", "kode_kec"], +// [ +// "match", +// ["get", "kode_kec"], +// ...(focusedDistrictId +// ? [ +// [ +// focusedDistrictId, +// crimeDataByDistrict[focusedDistrictId]?.level === "low" +// ? CRIME_RATE_COLORS.low +// : crimeDataByDistrict[focusedDistrictId]?.level === "medium" +// ? CRIME_RATE_COLORS.medium +// : crimeDataByDistrict[focusedDistrictId]?.level === "high" +// ? CRIME_RATE_COLORS.high +// : CRIME_RATE_COLORS.default, +// ], +// "rgba(0,0,0,0.05)", +// ] +// : Object.entries(crimeDataByDistrict).flatMap(([districtId, data]) => { +// return [ +// districtId, +// data.level === "low" +// ? CRIME_RATE_COLORS.low +// : data.level === "medium" +// ? CRIME_RATE_COLORS.medium +// : data.level === "high" +// ? CRIME_RATE_COLORS.high +// : CRIME_RATE_COLORS.default, +// ] +// })), +// focusedDistrictId ? "rgba(0,0,0,0.05)" : CRIME_RATE_COLORS.default, +// ], +// CRIME_RATE_COLORS.default, +// ] as any) +// } + +// // Re-add click handler to ensure it works after timelapse +// map.off("click", "district-fill", handleDistrictClick) +// map.on("click", "district-fill", handleDistrictClick) +// } +// } catch (error) { +// console.error("Error adding district layers:", error) +// } +// } + +// if (map.isStyleLoaded()) { +// onStyleLoad() +// } else { +// map.once("style.load", onStyleLoad) +// } + +// return () => { +// if (map) { +// map.off("click", "district-fill", handleDistrictClick) +// } +// } +// }, [map, visible, tilesetId, crimes, filterCategory, year, month, handleIncidentClick, handleClusterClick]) + +// // Add an additional effect to ensure click handlers are properly maintained +// useEffect(() => { +// if (!map || !layersAdded.current) return + +// // Re-attach the click handler after any data changes +// if (map.getMap().getLayer("district-fill")) { +// map.off("click", "district-fill", handleDistrictClick) +// map.on("click", "district-fill", handleDistrictClick) +// } + +// // Ensure district-extrusion settings are maintained after timelapse +// if (map.getMap().getLayer("district-extrusion")) { +// map.getMap().setFilter("district-extrusion", ["==", ["get", "kode_kec"], focusedDistrictId || ""]); + +// // Re-apply the extrusion color based on the focused district +// map.getMap().setPaintProperty("district-extrusion", "fill-extrusion-color", [ +// "case", +// ["has", "kode_kec"], +// [ +// "match", +// ["get", "kode_kec"], +// focusedDistrictId || "", +// crimeDataByDistrict[focusedDistrictId || ""]?.level === "low" +// ? CRIME_RATE_COLORS.low +// : crimeDataByDistrict[focusedDistrictId || ""]?.level === "medium" +// ? CRIME_RATE_COLORS.medium +// : crimeDataByDistrict[focusedDistrictId || ""]?.level === "high" +// ? CRIME_RATE_COLORS.high +// : CRIME_RATE_COLORS.default, +// "transparent", +// ], +// "transparent", +// ]); + +// // Ensure extrusion height is restored if needed +// if (focusedDistrictId) { +// map.getMap().setPaintProperty("district-extrusion", "fill-extrusion-height", [ +// "case", +// ["has", "kode_kec"], +// ["match", ["get", "kode_kec"], focusedDistrictId, 800, 0], +// 0, +// ]); +// } +// } + +// return () => { +// if (map) { +// map.off("click", "district-fill", handleDistrictClick) +// } +// } +// }, [map, year, month, crimeDataByDistrict, focusedDistrictId]) + +// useEffect(() => { +// if (!map || !layersAdded.current) return + +// try { +// if (map.getMap().getLayer("district-fill")) { +// const colorEntries = focusedDistrictId +// ? [ +// [ +// focusedDistrictId, +// crimeDataByDistrict[focusedDistrictId]?.level === "low" +// ? CRIME_RATE_COLORS.low +// : crimeDataByDistrict[focusedDistrictId]?.level === "medium" +// ? CRIME_RATE_COLORS.medium +// : crimeDataByDistrict[focusedDistrictId]?.level === "high" +// ? CRIME_RATE_COLORS.high +// : CRIME_RATE_COLORS.default, +// ], +// "rgba(0,0,0,0.05)", +// ] +// : Object.entries(crimeDataByDistrict).flatMap(([districtId, data]) => { +// if (!data || !data.level) { +// return [districtId, CRIME_RATE_COLORS.default] +// } + +// return [ +// districtId, +// data.level === "low" +// ? CRIME_RATE_COLORS.low +// : data.level === "medium" +// ? CRIME_RATE_COLORS.medium +// : data.level === "high" +// ? CRIME_RATE_COLORS.high +// : CRIME_RATE_COLORS.default, +// ] +// }) + +// const fillColorExpression = [ +// "case", +// ["has", "kode_kec"], +// [ +// "match", +// ["get", "kode_kec"], +// ...colorEntries, +// focusedDistrictId ? "rgba(0,0,0,0.05)" : CRIME_RATE_COLORS.default, +// ], +// CRIME_RATE_COLORS.default, +// ] as any + +// map.getMap().setPaintProperty("district-fill", "fill-color", fillColorExpression) +// } + +// if (map.getMap().getLayer("district-extrusion")) { +// map.getMap().setFilter("district-extrusion", ["==", ["get", "kode_kec"], focusedDistrictId || ""]) + +// map +// .getMap() +// .setPaintProperty("district-extrusion", "fill-extrusion-color", [ +// "case", +// ["has", "kode_kec"], +// [ +// "match", +// ["get", "kode_kec"], +// focusedDistrictId || "", +// crimeDataByDistrict[focusedDistrictId || ""]?.level === "low" +// ? CRIME_RATE_COLORS.low +// : crimeDataByDistrict[focusedDistrictId || ""]?.level === "medium" +// ? CRIME_RATE_COLORS.medium +// : crimeDataByDistrict[focusedDistrictId || ""]?.level === "high" +// ? CRIME_RATE_COLORS.high +// : CRIME_RATE_COLORS.default, +// "transparent", +// ], +// "transparent", +// ]) + +// if (focusedDistrictId) { +// const startHeight = 0 +// const targetHeight = 800 +// const duration = 700 +// const startTime = performance.now() + +// const animateHeight = (currentTime: number) => { +// const elapsed = currentTime - startTime +// const progress = Math.min(elapsed / duration, 1) +// const easedProgress = progress * (2 - progress) +// const currentHeight = startHeight + (targetHeight - startHeight) * easedProgress + +// map +// .getMap() +// .setPaintProperty("district-extrusion", "fill-extrusion-height", [ +// "case", +// ["has", "kode_kec"], +// ["match", ["get", "kode_kec"], focusedDistrictId, currentHeight, 0], +// 0, +// ]) + +// if (progress < 1) { +// requestAnimationFrame(animateHeight) +// } +// } + +// requestAnimationFrame(animateHeight) +// } else { +// const startHeight = 800 +// const targetHeight = 0 +// const duration = 500 +// const startTime = performance.now() + +// const animateHeightDown = (currentTime: number) => { +// const elapsed = currentTime - startTime +// const progress = Math.min(elapsed / duration, 1) +// const easedProgress = progress * (2 - progress) +// const currentHeight = startHeight + (targetHeight - startHeight) * easedProgress + +// map +// .getMap() +// .setPaintProperty("district-extrusion", "fill-extrusion-height", [ +// "case", +// ["has", "kode_kec"], +// ["match", ["get", "kode_kec"], "", currentHeight, 0], +// 0, +// ]) + +// if (progress < 1) { +// requestAnimationFrame(animateHeightDown) +// } +// } + +// requestAnimationFrame(animateHeightDown) +// } +// } +// } catch (error) { +// console.error("Error updating district layer:", error) +// } +// }, [map, crimes, crimeDataByDistrict, focusedDistrictId]) + +// useEffect(() => { +// if (!map || !map.getMap().getSource("crime-incidents")) return + +// try { +// const allIncidents = crimes.flatMap((crime) => { +// if (!crime.crime_incidents) return [] + +// let filteredIncidents = crime.crime_incidents + +// if (filterCategory !== "all") { +// filteredIncidents = crime.crime_incidents.filter( +// (incident) => incident.crime_categories && incident.crime_categories.name === filterCategory, +// ) +// } + +// return filteredIncidents +// .map((incident) => { +// if (!incident.locations) { +// console.warn("Missing location for incident:", incident.id) +// return null +// } + +// return { +// type: "Feature" as const, +// properties: { +// id: incident.id, +// district: crime.districts?.name || "Unknown", +// category: incident.crime_categories?.name || "Unknown", +// incidentType: incident.crime_categories?.type || "Unknown", +// level: crime.level || "low", +// description: incident.description || "", +// }, +// geometry: { +// type: "Point" as const, +// coordinates: [incident.locations.longitude || 0, incident.locations.latitude || 0], +// }, +// } +// }) +// .filter(Boolean) +// }) +// ; (map.getMap().getSource("crime-incidents") as mapboxgl.GeoJSONSource).setData({ +// type: "FeatureCollection", +// features: allIncidents as GeoJSON.Feature[], +// }) +// } catch (error) { +// console.error("Error updating incident data:", error) +// } +// }, [map, crimes, filterCategory]) + +// useEffect(() => { +// if (selectedDistrictRef.current) { +// const districtId = selectedDistrictRef.current.id +// const crimeData = crimeDataByDistrict[districtId] || {} + +// 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: DistrictFeature = { +// ...selectedDistrictRef.current, +// number_of_crime: crimeData.number_of_crime || 0, +// level: crimeData.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]) + +// useEffect(() => { +// return () => { +// if (rotationAnimationRef.current) { +// cancelAnimationFrame(rotationAnimationRef.current) +// rotationAnimationRef.current = null +// } +// } +// }, []) + +// if (!visible) return null + +// return ( +// <> +// {selectedDistrict && ( +// +// )} +// +// ) +// } diff --git a/sigap-website/app/_components/map/layers/district-layer.tsx b/sigap-website/app/_components/map/layers/district-layer.tsx index b5418ff..dde7966 100644 --- a/sigap-website/app/_components/map/layers/district-layer.tsx +++ b/sigap-website/app/_components/map/layers/district-layer.tsx @@ -1,6 +1,6 @@ "use client" -import { BASE_BEARING, BASE_PITCH, BASE_ZOOM } from "@/app/_utils/const/map" +import { BASE_BEARING, BASE_DURATION, BASE_PITCH, BASE_ZOOM } from "@/app/_utils/const/map" import { createFillColorExpression, getFillOpacity, processDistrictFeature } from "@/app/_utils/map" import type { IDistrictLayerProps } from "@/app/_utils/types/map" import { useEffect } from "react" @@ -63,7 +63,7 @@ export default function DistrictFillLineLayer({ zoom: BASE_ZOOM, pitch: BASE_PITCH, bearing: BASE_BEARING, - duration: 1500, + duration: BASE_DURATION, easing: (t) => t * (2 - t), // easeOutQuad }) @@ -137,14 +137,14 @@ export default function DistrictFillLineLayer({ } // Animate to a pitched view focused on the district - map.flyTo({ - center: [district.longitude, district.latitude], - zoom: 12.5, - pitch: 75, - bearing: 0, - duration: 1500, - easing: (t) => t * (2 - t), // easeOutQuad - }) + // map.flyTo({ + // center: [district.longitude, district.latitude], + // zoom: 12.5, + // pitch: 75, + // bearing: 0, + // duration: 1500, + // easing: (t) => t * (2 - t), // easeOutQuad + // }) // Use onDistrictClick if available, otherwise fall back to onClick if (onDistrictClick) { diff --git a/sigap-website/app/_components/map/layers/layers.tsx b/sigap-website/app/_components/map/layers/layers.tsx index 35c68a0..578cf08 100644 --- a/sigap-website/app/_components/map/layers/layers.tsx +++ b/sigap-website/app/_components/map/layers/layers.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useEffect, useCallback } from "react" import { useMap } from "react-map-gl/mapbox" -import { BASE_BEARING, BASE_DURATION, BASE_PITCH, BASE_ZOOM, MAPBOX_TILESET_ID } from "@/app/_utils/const/map" +import { BASE_BEARING, BASE_DURATION, BASE_PITCH, BASE_ZOOM, MAPBOX_TILESET_ID, PITCH_3D, ZOOM_3D } from "@/app/_utils/const/map" import DistrictPopup from "../pop-up/district-popup" import DistrictExtrusionLayer from "./district-extrusion-layer" import ClusterLayer from "./cluster-layer" @@ -263,9 +263,9 @@ export default function Layers({ if (map && feature.longitude && feature.latitude) { map.flyTo({ center: [feature.longitude, feature.latitude], - zoom: 12.5, - pitch: 60, - bearing: 0, + zoom: ZOOM_3D, + pitch: PITCH_3D, + bearing: BASE_BEARING, duration: BASE_DURATION, easing: (t) => t * (2 - t), })