1199 lines
52 KiB
TypeScript
1199 lines
52 KiB
TypeScript
// "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<DistrictFeature | null>(null)
|
|
// const [selectedDistrict, setSelectedDistrict] = useState<DistrictFeature | null>(null)
|
|
// const [focusedDistrictId, setFocusedDistrictId] = useState<string | null>(null)
|
|
// const rotationAnimationRef = useRef<number | null>(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<string, { number_of_crime?: number; level?: $Enums.crime_rates }>,
|
|
// )
|
|
|
|
// 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 { 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
|
|
// });
|
|
|
|
// // 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');
|
|
// }
|
|
|
|
// 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'
|
|
// }
|
|
// });
|
|
|
|
// // Add a pulsing effect using animations
|
|
// let size = 10;
|
|
// const animatePulse = () => {
|
|
// if (!map || !map.getMap().getLayer('target-incident-highlight')) return;
|
|
|
|
// size = (size % 20) + 1;
|
|
|
|
// map.getMap().setPaintProperty('target-incident-highlight', 'circle-radius', [
|
|
// 'interpolate', ['linear'], ['zoom'],
|
|
// 10, size,
|
|
// 15, size * 1.5,
|
|
// 20, size * 2
|
|
// ]);
|
|
|
|
// requestAnimationFrame(animatePulse);
|
|
// };
|
|
|
|
// requestAnimationFrame(animatePulse);
|
|
// };
|
|
|
|
// 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 && (
|
|
// <DistrictPopup
|
|
// longitude={selectedDistrict.longitude || 0}
|
|
// latitude={selectedDistrict.latitude || 0}
|
|
// onClose={handleCloseDistrictPopup}
|
|
// district={selectedDistrict}
|
|
// year={year}
|
|
// month={month}
|
|
// filterCategory={filterCategory}
|
|
// />
|
|
// )}
|
|
// </>
|
|
// )
|
|
// }
|