"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(null) const bearingRef = useRef(0) const rotationAnimationRef = useRef(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 }