feat: enhance district layer interactions and animations for improved user experience
This commit is contained in:
parent
078bf969bc
commit
c6115adf88
|
@ -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 {
|
||||
// Update the filter for the extrusion layer
|
||||
map.setFilter("district-extrusion", ["==", ["get", "kode_kec"], focusedDistrictId || ""])
|
||||
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", [
|
||||
|
@ -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
|
||||
// Store current focused district
|
||||
lastFocusedDistrictRef.current = focusedDistrictId;
|
||||
|
||||
// Stop any existing animations and restart
|
||||
if (rotationAnimationRef.current) {
|
||||
cancelAnimationFrame(rotationAnimationRef.current)
|
||||
rotationAnimationRef.current = null
|
||||
cancelAnimationFrame(rotationAnimationRef.current);
|
||||
rotationAnimationRef.current = null;
|
||||
}
|
||||
bearingRef.current = 0
|
||||
|
||||
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,6 +271,7 @@ export default function DistrictExtrusionLayer({
|
|||
const easedProgress = progress * (2 - progress) // easeOutQuad
|
||||
const currentHeight = startHeight + (targetHeight - startHeight) * easedProgress
|
||||
|
||||
try {
|
||||
map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
|
||||
"case",
|
||||
["has", "kode_kec"],
|
||||
|
@ -203,6 +285,13 @@ export default function DistrictExtrusionLayer({
|
|||
// 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)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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(() => {
|
||||
// Start the animation loop
|
||||
if (rotationAnimationRef.current) {
|
||||
cancelAnimationFrame(rotationAnimationRef.current)
|
||||
}
|
||||
rotationAnimationRef.current = requestAnimationFrame(animate)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// Start rotation after the initial flyTo completes
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue