243 lines
8.4 KiB
TypeScript
243 lines
8.4 KiB
TypeScript
"use client"
|
|
|
|
import { getCrimeRateColor } from "@/app/_utils/map"
|
|
import { IExtrusionLayerProps } from "@/app/_utils/types/map"
|
|
import { useEffect, useRef } from "react"
|
|
|
|
export default function DistrictExtrusionLayer({
|
|
visible = true,
|
|
map,
|
|
tilesetId,
|
|
focusedDistrictId,
|
|
crimeDataByDistrict,
|
|
}: IExtrusionLayerProps) {
|
|
const animationRef = useRef<number | null>(null)
|
|
const bearingRef = useRef(0)
|
|
const rotationAnimationRef = useRef<number | null>(null)
|
|
const extrusionCreatedRef = useRef(false)
|
|
|
|
// Handle extrusion layer creation and updates
|
|
useEffect(() => {
|
|
if (!map || !visible) return
|
|
|
|
const onStyleLoad = () => {
|
|
if (!map) return
|
|
|
|
try {
|
|
const layers = map.getStyle().layers
|
|
let firstSymbolId: string | undefined
|
|
for (const layer of layers) {
|
|
if (layer.type === "symbol") {
|
|
firstSymbolId = layer.id
|
|
break
|
|
}
|
|
}
|
|
|
|
// Remove existing layer if it exists to avoid conflicts
|
|
if (map.getLayer("district-extrusion")) {
|
|
map.removeLayer("district-extrusion")
|
|
extrusionCreatedRef.current = false
|
|
}
|
|
|
|
// Make sure the districts source exists
|
|
if (!map.getSource("districts")) {
|
|
if (!tilesetId) {
|
|
console.error("No tileset ID provided for districts source");
|
|
return;
|
|
}
|
|
|
|
map.addSource("districts", {
|
|
type: "vector",
|
|
url: `mapbox://${tilesetId}`,
|
|
})
|
|
}
|
|
|
|
// Create the extrusion layer
|
|
map.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 || "",
|
|
getCrimeRateColor(crimeDataByDistrict[focusedDistrictId || ""]?.level),
|
|
"transparent",
|
|
],
|
|
"transparent",
|
|
],
|
|
"fill-extrusion-height": [
|
|
"case",
|
|
["has", "kode_kec"],
|
|
["match", ["get", "kode_kec"], focusedDistrictId || "", 0, 0], // Start at 0 for animation
|
|
0,
|
|
],
|
|
"fill-extrusion-base": 0,
|
|
"fill-extrusion-opacity": 0.8,
|
|
},
|
|
filter: ["==", ["get", "kode_kec"], focusedDistrictId || ""],
|
|
},
|
|
firstSymbolId
|
|
)
|
|
|
|
extrusionCreatedRef.current = true
|
|
|
|
// If a district is focused, start the animation
|
|
if (focusedDistrictId) {
|
|
animateExtrusion()
|
|
}
|
|
} catch (error) {
|
|
console.error("Error adding district extrusion layer:", error)
|
|
}
|
|
}
|
|
|
|
if (map.isStyleLoaded()) {
|
|
onStyleLoad()
|
|
} else {
|
|
map.once("style.load", onStyleLoad)
|
|
}
|
|
|
|
return () => {
|
|
if (animationRef.current) {
|
|
cancelAnimationFrame(animationRef.current)
|
|
animationRef.current = null
|
|
}
|
|
}
|
|
}, [map, visible, tilesetId])
|
|
|
|
// Update filter and color when focused district changes
|
|
useEffect(() => {
|
|
if (!map || !map.getLayer("district-extrusion")) return
|
|
|
|
try {
|
|
// Update the filter for the extrusion layer
|
|
map.setFilter("district-extrusion", ["==", ["get", "kode_kec"], focusedDistrictId || ""])
|
|
|
|
// Update the extrusion color
|
|
map.setPaintProperty("district-extrusion", "fill-extrusion-color", [
|
|
"case",
|
|
["has", "kode_kec"],
|
|
[
|
|
"match",
|
|
["get", "kode_kec"],
|
|
focusedDistrictId || "",
|
|
getCrimeRateColor(crimeDataByDistrict[focusedDistrictId || ""]?.level),
|
|
"transparent",
|
|
],
|
|
"transparent",
|
|
])
|
|
|
|
// Reset height for animation
|
|
map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
|
|
"case",
|
|
["has", "kode_kec"],
|
|
["match", ["get", "kode_kec"], focusedDistrictId || "", 0, 0],
|
|
0,
|
|
])
|
|
|
|
// Start animation if district is focused, otherwise reset
|
|
if (focusedDistrictId) {
|
|
animateExtrusion()
|
|
} else {
|
|
// Stop rotation when unfocusing
|
|
if (rotationAnimationRef.current) {
|
|
cancelAnimationFrame(rotationAnimationRef.current)
|
|
rotationAnimationRef.current = null
|
|
}
|
|
bearingRef.current = 0
|
|
}
|
|
} catch (error) {
|
|
console.error("Error updating district extrusion:", error)
|
|
}
|
|
}, [map, focusedDistrictId, crimeDataByDistrict])
|
|
|
|
// Cleanup on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
if (animationRef.current) {
|
|
cancelAnimationFrame(animationRef.current)
|
|
animationRef.current = null
|
|
}
|
|
if (rotationAnimationRef.current) {
|
|
cancelAnimationFrame(rotationAnimationRef.current)
|
|
rotationAnimationRef.current = null
|
|
}
|
|
}
|
|
}, [])
|
|
|
|
// Animate extrusion height
|
|
const animateExtrusion = () => {
|
|
if (!map || !map.getLayer("district-extrusion") || !focusedDistrictId) return
|
|
|
|
if (animationRef.current) {
|
|
cancelAnimationFrame(animationRef.current)
|
|
}
|
|
|
|
const startHeight = 0
|
|
const targetHeight = 800
|
|
const duration = 700
|
|
const startTime = performance.now()
|
|
|
|
const animate = (currentTime: number) => {
|
|
const elapsed = currentTime - startTime
|
|
const progress = Math.min(elapsed / duration, 1)
|
|
const easedProgress = progress * (2 - progress) // easeOutQuad
|
|
const currentHeight = startHeight + (targetHeight - startHeight) * easedProgress
|
|
|
|
map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
|
|
"case",
|
|
["has", "kode_kec"],
|
|
["match", ["get", "kode_kec"], focusedDistrictId, currentHeight, 0],
|
|
0,
|
|
])
|
|
|
|
if (progress < 1) {
|
|
animationRef.current = requestAnimationFrame(animate)
|
|
} else {
|
|
// Start rotation after extrusion completes
|
|
startRotation()
|
|
}
|
|
}
|
|
|
|
animationRef.current = requestAnimationFrame(animate)
|
|
}
|
|
|
|
// Start rotation animation
|
|
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)
|
|
}
|
|
|
|
return null
|
|
}
|