diff --git a/sigap-website/app/_components/map/layers/district-extrusion-layer.tsx b/sigap-website/app/_components/map/layers/district-extrusion-layer.tsx index 7f74271..741261c 100644 --- a/sigap-website/app/_components/map/layers/district-extrusion-layer.tsx +++ b/sigap-website/app/_components/map/layers/district-extrusion-layer.tsx @@ -15,6 +15,7 @@ export default function DistrictExtrusionLayer({ const bearingRef = useRef(0) const rotationAnimationRef = useRef(null) const extrusionCreatedRef = useRef(false) + const lastFocusedDistrictRef = useRef(null) // Handle extrusion layer creation and updates useEffect(() => { @@ -90,6 +91,7 @@ export default function DistrictExtrusionLayer({ // If a district is focused, start the animation if (focusedDistrictId) { + lastFocusedDistrictRef.current = focusedDistrictId animateExtrusion() } } catch (error) { @@ -115,9 +117,79 @@ export default function DistrictExtrusionLayer({ useEffect(() => { if (!map || !map.getLayer("district-extrusion")) return + // 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; + + let 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 the filter for the extrusion layer - map.setFilter("district-extrusion", ["==", ["get", "kode_kec"], focusedDistrictId || ""]) + // 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", [ @@ -126,32 +198,40 @@ export default function DistrictExtrusionLayer({ [ "match", ["get", "kode_kec"], - focusedDistrictId || "", - getCrimeRateColor(crimeDataByDistrict[focusedDistrictId || ""]?.level), + 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], + ["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 + // 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(() => { + animateExtrusion(); + }, 100); + } catch (error) { console.error("Error updating district extrusion:", error) } @@ -177,6 +257,7 @@ export default function DistrictExtrusionLayer({ if (animationRef.current) { cancelAnimationFrame(animationRef.current) + animationRef.current = null } const startHeight = 0 @@ -190,18 +271,26 @@ export default function DistrictExtrusionLayer({ 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, - ]) + 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 { - // Start rotation after extrusion completes - startRotation() + if (progress < 1) { + animationRef.current = requestAnimationFrame(animate) + } else { + // Start rotation after extrusion completes + startRotation() + } + } catch (error) { + console.error("Error animating extrusion:", error) + if (animationRef.current) { + cancelAnimationFrame(animationRef.current) + animationRef.current = null + } } } @@ -213,9 +302,10 @@ export default function DistrictExtrusionLayer({ if (!map || !focusedDistrictId) return const rotationSpeed = 0.05 // degrees per frame + bearingRef.current = 0 // Reset bearing at start const animate = () => { - if (!map || !focusedDistrictId) { + if (!map || !focusedDistrictId || focusedDistrictId !== lastFocusedDistrictRef.current) { if (rotationAnimationRef.current) { cancelAnimationFrame(rotationAnimationRef.current) rotationAnimationRef.current = null @@ -223,17 +313,26 @@ export default function DistrictExtrusionLayer({ return } - // Update bearing with smooth increment - bearingRef.current = (bearingRef.current + rotationSpeed) % 360 - map.setBearing(bearingRef.current) + try { + // Update bearing with smooth increment + bearingRef.current = (bearingRef.current + rotationSpeed) % 360 + map.setBearing(bearingRef.current) - // Continue the animation - rotationAnimationRef.current = requestAnimationFrame(animate) + // 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) } diff --git a/sigap-website/app/_components/map/layers/district-layer-old.tsx b/sigap-website/app/_components/map/layers/district-layer-old.tsx index d1b40b4..d332257 100644 --- a/sigap-website/app/_components/map/layers/district-layer-old.tsx +++ b/sigap-website/app/_components/map/layers/district-layer-old.tsx @@ -276,11 +276,12 @@ export default function DistrictLayer({ // Improved continuous bearing rotation function const startRotation = () => { - const rotationSpeed = 0.05 // degrees per frame - adjust for slower/faster rotation + if (!map || !focusedDistrictId) return + + const rotationSpeed = 0.05 // degrees per frame const animate = () => { - // Check if map and focus are still valid - if (!map || !map.getMap() || focusedDistrictId !== district.id) { + if (!map || !focusedDistrictId) { if (rotationAnimationRef.current) { cancelAnimationFrame(rotationAnimationRef.current) rotationAnimationRef.current = null @@ -290,16 +291,17 @@ export default function DistrictLayer({ // Update bearing with smooth increment bearingRef.current = (bearingRef.current + rotationSpeed) % 360 - map.getMap().setBearing(bearingRef.current) // Use map.getMap().setBearing instead of map.setBearing + map.setBearing(bearingRef.current) // Continue the animation rotationAnimationRef.current = requestAnimationFrame(animate) } - // Start the animation loop after a short delay to ensure the flyTo has started - setTimeout(() => { - rotationAnimationRef.current = requestAnimationFrame(animate) - }, 100) + // Start the animation loop + if (rotationAnimationRef.current) { + cancelAnimationFrame(rotationAnimationRef.current) + } + rotationAnimationRef.current = requestAnimationFrame(animate) } // Start rotation after the initial flyTo completes diff --git a/sigap-website/app/_components/map/layers/district-layer.tsx b/sigap-website/app/_components/map/layers/district-layer.tsx index adec11d..50a5299 100644 --- a/sigap-website/app/_components/map/layers/district-layer.tsx +++ b/sigap-website/app/_components/map/layers/district-layer.tsx @@ -47,6 +47,10 @@ export default function DistrictFillLineLayer({ easing: (t) => t * (2 - t), // easeOutQuad }) + // Restore fill color for all districts when unfocusing + const fillColorExpression = createFillColorExpression(null, crimeDataByDistrict) + map.setPaintProperty("district-fill", "fill-color", fillColorExpression as any) + // Show all clusters again when unfocusing if (map.getLayer("clusters")) { map.setLayoutProperty("clusters", "visibility", "visible") @@ -55,6 +59,34 @@ export default function DistrictFillLineLayer({ map.setLayoutProperty("unclustered-point", "visibility", "visible") } + return + } else if (focusedDistrictId) { + // If we're already focusing on a district and clicking a different one, + // we need to reset the current one and move to the new one + setFocusedDistrictId(null) + + // Wait a moment before selecting the new district to ensure clean transitions + setTimeout(() => { + const district = processDistrictFeature(feature, e, districtId, crimeDataByDistrict, crimes, year, month) + if (!district) return + + setFocusedDistrictId(district.id) + + // Fly to the new district + map.flyTo({ + center: [district.longitude, district.latitude], + zoom: 14.5, + pitch: 75, + bearing: 0, + duration: 1500, + easing: (t) => t * (2 - t), // easeOutQuad + }) + + if (onClick) { + onClick(district) + } + }, 100) + return } @@ -62,6 +94,10 @@ export default function DistrictFillLineLayer({ if (!district) return + // Set the fill color expression immediately to show the focus + const focusedFillColorExpression = createFillColorExpression(district.id, crimeDataByDistrict) + map.setPaintProperty("district-fill", "fill-color", focusedFillColorExpression as any) + setFocusedDistrictId(district.id) // Hide clusters when focusing on a district @@ -75,7 +111,7 @@ export default function DistrictFillLineLayer({ // Animate to a pitched view focused on the district map.flyTo({ center: [district.longitude, district.latitude], - zoom: 12.5, + zoom: 14.5, pitch: 75, bearing: 0, duration: 1500, @@ -187,5 +223,17 @@ export default function DistrictFillLineLayer({ setFocusedDistrictId, ]) + // Add an effect to update the fill color whenever focusedDistrictId changes + useEffect(() => { + if (!map || !map.getLayer("district-fill")) return; + + try { + const fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict) + map.setPaintProperty("district-fill", "fill-color", fillColorExpression as any) + } catch (error) { + console.error("Error updating district fill colors:", error) + } + }, [map, focusedDistrictId, crimeDataByDistrict]) + return null } diff --git a/sigap-website/app/_components/map/layers/layers.tsx b/sigap-website/app/_components/map/layers/layers.tsx index 2511aa2..92cb21b 100644 --- a/sigap-website/app/_components/map/layers/layers.tsx +++ b/sigap-website/app/_components/map/layers/layers.tsx @@ -10,7 +10,7 @@ import ClusterLayer from "./cluster-layer" import type { ICrimes } from "@/app/_utils/types/crimes" import { IDistrictFeature } from "@/app/_utils/types/map" -import { processCrimeDataByDistrict } from "@/app/_utils/map" +import { createFillColorExpression, processCrimeDataByDistrict } from "@/app/_utils/map" import DistrictFillLineLayer from "./district-layer" import UnclusteredPointLayer from "./uncluster-layer" import FlyToHandler from "../fly-to" @@ -111,6 +111,12 @@ export default function Layers({ if (map.getLayer("unclustered-point")) { map.getMap().setLayoutProperty("unclustered-point", "visibility", "visible") } + + // Explicitly update fill color for all districts + if (map.getLayer("district-fill")) { + const fillColorExpression = createFillColorExpression(null, crimeDataByDistrict) + map.getMap().setPaintProperty("district-fill", "fill-color", fillColorExpression as any) + } } }