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 bearingRef = useRef(0)
|
||||||
const rotationAnimationRef = useRef<number | null>(null)
|
const rotationAnimationRef = useRef<number | null>(null)
|
||||||
const extrusionCreatedRef = useRef(false)
|
const extrusionCreatedRef = useRef(false)
|
||||||
|
const lastFocusedDistrictRef = useRef<string | null>(null)
|
||||||
|
|
||||||
// Handle extrusion layer creation and updates
|
// Handle extrusion layer creation and updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -90,6 +91,7 @@ export default function DistrictExtrusionLayer({
|
||||||
|
|
||||||
// If a district is focused, start the animation
|
// If a district is focused, start the animation
|
||||||
if (focusedDistrictId) {
|
if (focusedDistrictId) {
|
||||||
|
lastFocusedDistrictRef.current = focusedDistrictId
|
||||||
animateExtrusion()
|
animateExtrusion()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -115,9 +117,79 @@ export default function DistrictExtrusionLayer({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map || !map.getLayer("district-extrusion")) return
|
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 {
|
try {
|
||||||
// Update the filter for the extrusion layer
|
// Update filter for the new district
|
||||||
map.setFilter("district-extrusion", ["==", ["get", "kode_kec"], focusedDistrictId || ""])
|
map.setFilter("district-extrusion", ["==", ["get", "kode_kec"], focusedDistrictId]);
|
||||||
|
|
||||||
// Update the extrusion color
|
// Update the extrusion color
|
||||||
map.setPaintProperty("district-extrusion", "fill-extrusion-color", [
|
map.setPaintProperty("district-extrusion", "fill-extrusion-color", [
|
||||||
|
@ -126,32 +198,40 @@ export default function DistrictExtrusionLayer({
|
||||||
[
|
[
|
||||||
"match",
|
"match",
|
||||||
["get", "kode_kec"],
|
["get", "kode_kec"],
|
||||||
focusedDistrictId || "",
|
focusedDistrictId,
|
||||||
getCrimeRateColor(crimeDataByDistrict[focusedDistrictId || ""]?.level),
|
getCrimeRateColor(crimeDataByDistrict[focusedDistrictId]?.level),
|
||||||
"transparent",
|
"transparent",
|
||||||
],
|
],
|
||||||
"transparent",
|
"transparent",
|
||||||
])
|
]);
|
||||||
|
|
||||||
// Reset height for animation
|
// Reset height for animation
|
||||||
map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
|
map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
|
||||||
"case",
|
"case",
|
||||||
["has", "kode_kec"],
|
["has", "kode_kec"],
|
||||||
["match", ["get", "kode_kec"], focusedDistrictId || "", 0, 0],
|
["match", ["get", "kode_kec"], focusedDistrictId, 0, 0],
|
||||||
0,
|
0,
|
||||||
])
|
]);
|
||||||
|
|
||||||
// Start animation if district is focused, otherwise reset
|
// Store current focused district
|
||||||
if (focusedDistrictId) {
|
lastFocusedDistrictRef.current = focusedDistrictId;
|
||||||
animateExtrusion()
|
|
||||||
} else {
|
// Stop any existing animations and restart
|
||||||
// Stop rotation when unfocusing
|
if (rotationAnimationRef.current) {
|
||||||
if (rotationAnimationRef.current) {
|
cancelAnimationFrame(rotationAnimationRef.current);
|
||||||
cancelAnimationFrame(rotationAnimationRef.current)
|
rotationAnimationRef.current = null;
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error("Error updating district extrusion:", error)
|
console.error("Error updating district extrusion:", error)
|
||||||
}
|
}
|
||||||
|
@ -177,6 +257,7 @@ export default function DistrictExtrusionLayer({
|
||||||
|
|
||||||
if (animationRef.current) {
|
if (animationRef.current) {
|
||||||
cancelAnimationFrame(animationRef.current)
|
cancelAnimationFrame(animationRef.current)
|
||||||
|
animationRef.current = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const startHeight = 0
|
const startHeight = 0
|
||||||
|
@ -190,18 +271,26 @@ export default function DistrictExtrusionLayer({
|
||||||
const easedProgress = progress * (2 - progress) // easeOutQuad
|
const easedProgress = progress * (2 - progress) // easeOutQuad
|
||||||
const currentHeight = startHeight + (targetHeight - startHeight) * easedProgress
|
const currentHeight = startHeight + (targetHeight - startHeight) * easedProgress
|
||||||
|
|
||||||
map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
|
try {
|
||||||
"case",
|
map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
|
||||||
["has", "kode_kec"],
|
"case",
|
||||||
["match", ["get", "kode_kec"], focusedDistrictId, currentHeight, 0],
|
["has", "kode_kec"],
|
||||||
0,
|
["match", ["get", "kode_kec"], focusedDistrictId, currentHeight, 0],
|
||||||
])
|
0,
|
||||||
|
])
|
||||||
|
|
||||||
if (progress < 1) {
|
if (progress < 1) {
|
||||||
animationRef.current = requestAnimationFrame(animate)
|
animationRef.current = requestAnimationFrame(animate)
|
||||||
} else {
|
} else {
|
||||||
// Start rotation after extrusion completes
|
// Start rotation after extrusion completes
|
||||||
startRotation()
|
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
|
if (!map || !focusedDistrictId) return
|
||||||
|
|
||||||
const rotationSpeed = 0.05 // degrees per frame
|
const rotationSpeed = 0.05 // degrees per frame
|
||||||
|
bearingRef.current = 0 // Reset bearing at start
|
||||||
|
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
if (!map || !focusedDistrictId) {
|
if (!map || !focusedDistrictId || focusedDistrictId !== lastFocusedDistrictRef.current) {
|
||||||
if (rotationAnimationRef.current) {
|
if (rotationAnimationRef.current) {
|
||||||
cancelAnimationFrame(rotationAnimationRef.current)
|
cancelAnimationFrame(rotationAnimationRef.current)
|
||||||
rotationAnimationRef.current = null
|
rotationAnimationRef.current = null
|
||||||
|
@ -223,17 +313,26 @@ export default function DistrictExtrusionLayer({
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update bearing with smooth increment
|
try {
|
||||||
bearingRef.current = (bearingRef.current + rotationSpeed) % 360
|
// Update bearing with smooth increment
|
||||||
map.setBearing(bearingRef.current)
|
bearingRef.current = (bearingRef.current + rotationSpeed) % 360
|
||||||
|
map.setBearing(bearingRef.current)
|
||||||
|
|
||||||
// Continue the animation
|
// Continue the animation
|
||||||
rotationAnimationRef.current = requestAnimationFrame(animate)
|
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
|
// Start the animation loop
|
||||||
if (rotationAnimationRef.current) {
|
if (rotationAnimationRef.current) {
|
||||||
cancelAnimationFrame(rotationAnimationRef.current)
|
cancelAnimationFrame(rotationAnimationRef.current)
|
||||||
|
rotationAnimationRef.current = null
|
||||||
}
|
}
|
||||||
rotationAnimationRef.current = requestAnimationFrame(animate)
|
rotationAnimationRef.current = requestAnimationFrame(animate)
|
||||||
}
|
}
|
||||||
|
|
|
@ -276,11 +276,12 @@ export default function DistrictLayer({
|
||||||
|
|
||||||
// Improved continuous bearing rotation function
|
// Improved continuous bearing rotation function
|
||||||
const startRotation = () => {
|
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 = () => {
|
const animate = () => {
|
||||||
// Check if map and focus are still valid
|
if (!map || !focusedDistrictId) {
|
||||||
if (!map || !map.getMap() || focusedDistrictId !== district.id) {
|
|
||||||
if (rotationAnimationRef.current) {
|
if (rotationAnimationRef.current) {
|
||||||
cancelAnimationFrame(rotationAnimationRef.current)
|
cancelAnimationFrame(rotationAnimationRef.current)
|
||||||
rotationAnimationRef.current = null
|
rotationAnimationRef.current = null
|
||||||
|
@ -290,16 +291,17 @@ export default function DistrictLayer({
|
||||||
|
|
||||||
// Update bearing with smooth increment
|
// Update bearing with smooth increment
|
||||||
bearingRef.current = (bearingRef.current + rotationSpeed) % 360
|
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
|
// Continue the animation
|
||||||
rotationAnimationRef.current = requestAnimationFrame(animate)
|
rotationAnimationRef.current = requestAnimationFrame(animate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the animation loop after a short delay to ensure the flyTo has started
|
// Start the animation loop
|
||||||
setTimeout(() => {
|
if (rotationAnimationRef.current) {
|
||||||
rotationAnimationRef.current = requestAnimationFrame(animate)
|
cancelAnimationFrame(rotationAnimationRef.current)
|
||||||
}, 100)
|
}
|
||||||
|
rotationAnimationRef.current = requestAnimationFrame(animate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start rotation after the initial flyTo completes
|
// Start rotation after the initial flyTo completes
|
||||||
|
|
|
@ -47,6 +47,10 @@ export default function DistrictFillLineLayer({
|
||||||
easing: (t) => t * (2 - t), // easeOutQuad
|
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
|
// Show all clusters again when unfocusing
|
||||||
if (map.getLayer("clusters")) {
|
if (map.getLayer("clusters")) {
|
||||||
map.setLayoutProperty("clusters", "visibility", "visible")
|
map.setLayoutProperty("clusters", "visibility", "visible")
|
||||||
|
@ -55,6 +59,34 @@ export default function DistrictFillLineLayer({
|
||||||
map.setLayoutProperty("unclustered-point", "visibility", "visible")
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +94,10 @@ export default function DistrictFillLineLayer({
|
||||||
|
|
||||||
if (!district) return
|
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)
|
setFocusedDistrictId(district.id)
|
||||||
|
|
||||||
// Hide clusters when focusing on a district
|
// Hide clusters when focusing on a district
|
||||||
|
@ -75,7 +111,7 @@ export default function DistrictFillLineLayer({
|
||||||
// Animate to a pitched view focused on the district
|
// Animate to a pitched view focused on the district
|
||||||
map.flyTo({
|
map.flyTo({
|
||||||
center: [district.longitude, district.latitude],
|
center: [district.longitude, district.latitude],
|
||||||
zoom: 12.5,
|
zoom: 14.5,
|
||||||
pitch: 75,
|
pitch: 75,
|
||||||
bearing: 0,
|
bearing: 0,
|
||||||
duration: 1500,
|
duration: 1500,
|
||||||
|
@ -187,5 +223,17 @@ export default function DistrictFillLineLayer({
|
||||||
setFocusedDistrictId,
|
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
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import ClusterLayer from "./cluster-layer"
|
||||||
|
|
||||||
import type { ICrimes } from "@/app/_utils/types/crimes"
|
import type { ICrimes } from "@/app/_utils/types/crimes"
|
||||||
import { IDistrictFeature } from "@/app/_utils/types/map"
|
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 DistrictFillLineLayer from "./district-layer"
|
||||||
import UnclusteredPointLayer from "./uncluster-layer"
|
import UnclusteredPointLayer from "./uncluster-layer"
|
||||||
import FlyToHandler from "../fly-to"
|
import FlyToHandler from "../fly-to"
|
||||||
|
@ -111,6 +111,12 @@ export default function Layers({
|
||||||
if (map.getLayer("unclustered-point")) {
|
if (map.getLayer("unclustered-point")) {
|
||||||
map.getMap().setLayoutProperty("unclustered-point", "visibility", "visible")
|
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