feat: enhance district layer interactions and animations for improved user experience

This commit is contained in:
vergiLgood1 2025-05-05 05:49:50 +07:00
parent 078bf969bc
commit c6115adf88
4 changed files with 199 additions and 44 deletions

View File

@ -15,6 +15,7 @@ export default function DistrictExtrusionLayer({
const bearingRef = useRef(0)
const rotationAnimationRef = useRef<number | null>(null)
const extrusionCreatedRef = useRef(false)
const lastFocusedDistrictRef = useRef<string | null>(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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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)
}
}
}