MIF_E31221222/sigap-website/app/_components/map/layers/district-extrusion-layer.tsx

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
}