"use client" import { getCrimeRateColor } from "@/app/_utils/map" import type { 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) const lastFocusedDistrictRef = useRef(null) // Handle extrusion layer creation and updates useEffect(() => { if (!map || !visible) return console.log("DistrictExtrusionLayer effect running, focusedDistrictId:", focusedDistrictId) 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 console.log("District extrusion layer created") // If a district is focused, start the animation if (focusedDistrictId) { console.log("Starting animation for district:", focusedDistrictId) lastFocusedDistrictRef.current = 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, focusedDistrictId, crimeDataByDistrict]) // Update filter and color when focused district changes useEffect(() => { if (!map || !map.getLayer("district-extrusion")) return console.log("Updating district extrusion for district:", focusedDistrictId) // Skip unnecessary updates if nothing has changed if (lastFocusedDistrictRef.current === focusedDistrictId) return // If we're unfocusing a district if (!focusedDistrictId) { // Stop rotation when unfocusing if (rotationAnimationRef.current) { cancelAnimationFrame(rotationAnimationRef.current) rotationAnimationRef.current = null } bearingRef.current = 0 // Animate height down const animateHeightDown = () => { if (!map || !map.getLayer("district-extrusion")) return const currentHeight = 800 const duration = 500 const startTime = performance.now() const animate = (time: number) => { const elapsed = time - startTime const progress = Math.min(elapsed / duration, 1) const easedProgress = progress * (2 - progress) // easeOutQuad const height = 800 - 800 * easedProgress try { map.setPaintProperty("district-extrusion", "fill-extrusion-height", [ "case", ["has", "kode_kec"], ["match", ["get", "kode_kec"], lastFocusedDistrictRef.current || "", height, 0], 0, ]) if (progress < 1) { animationRef.current = requestAnimationFrame(animate) } else { // Reset when animation completes map.setPaintProperty("district-extrusion", "fill-extrusion-height", [ "case", ["has", "kode_kec"], ["match", ["get", "kode_kec"], "", 0, 0], 0, ]) map.setFilter("district-extrusion", ["==", ["get", "kode_kec"], ""]) lastFocusedDistrictRef.current = null // Ensure bearing is reset map.setBearing(0) } } catch (error) { console.error("Error animating extrusion down:", error) if (animationRef.current) { cancelAnimationFrame(animationRef.current) animationRef.current = null } } } if (animationRef.current) { cancelAnimationFrame(animationRef.current) } animationRef.current = requestAnimationFrame(animate) } animateHeightDown() return } try { // Update filter for the new district 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, ]) // Store current focused district lastFocusedDistrictRef.current = focusedDistrictId // Stop any existing animations and restart if (rotationAnimationRef.current) { cancelAnimationFrame(rotationAnimationRef.current) rotationAnimationRef.current = null } if (animationRef.current) { cancelAnimationFrame(animationRef.current) animationRef.current = null } // Start animation with small delay to ensure smooth transition setTimeout(() => { console.log("Starting animation after district update") animateExtrusion() }, 100) } 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) { console.log("Cannot animate extrusion: missing map, layer, or focusedDistrictId") return } console.log("Animating extrusion for district:", focusedDistrictId) if (animationRef.current) { cancelAnimationFrame(animationRef.current) animationRef.current = null } 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 try { 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 { console.log("Extrusion animation complete, starting rotation") // Start rotation after extrusion completes startRotation() } } catch (error) { console.error("Error animating extrusion:", error) if (animationRef.current) { cancelAnimationFrame(animationRef.current) animationRef.current = null } } } animationRef.current = requestAnimationFrame(animate) } // Start rotation animation const startRotation = () => { if (!map || !focusedDistrictId) return const rotationSpeed = 0.05 // degrees per frame bearingRef.current = 0 // Reset bearing at start const animate = () => { if (!map || !focusedDistrictId || focusedDistrictId !== lastFocusedDistrictRef.current) { if (rotationAnimationRef.current) { cancelAnimationFrame(rotationAnimationRef.current) rotationAnimationRef.current = null } return } try { // Update bearing with smooth increment bearingRef.current = (bearingRef.current + rotationSpeed) % 360 map.setBearing(bearingRef.current) // Continue the animation rotationAnimationRef.current = requestAnimationFrame(animate) } catch (error) { console.error("Error during rotation animation:", error) if (rotationAnimationRef.current) { cancelAnimationFrame(rotationAnimationRef.current) rotationAnimationRef.current = null } } } // Start the animation loop if (rotationAnimationRef.current) { cancelAnimationFrame(rotationAnimationRef.current) rotationAnimationRef.current = null } rotationAnimationRef.current = requestAnimationFrame(animate) } return null }