refactor: refactor district layer
This commit is contained in:
parent
03a5e527d4
commit
078bf969bc
|
@ -362,20 +362,20 @@ export default function CrimeMap() {
|
||||||
<MapView mapStyle="mapbox://styles/mapbox/dark-v11" className="h-[600px] w-full rounded-md">
|
<MapView mapStyle="mapbox://styles/mapbox/dark-v11" className="h-[600px] w-full rounded-md">
|
||||||
{/* District Layer with crime data - don't pass onClick if we want internal popup */}
|
{/* District Layer with crime data - don't pass onClick if we want internal popup */}
|
||||||
|
|
||||||
<DistrictLayer
|
{/* <DistrictLayer
|
||||||
crimes={filteredCrimes || []}
|
|
||||||
year={selectedYear.toString()}
|
|
||||||
month={selectedMonth.toString()}
|
|
||||||
filterCategory={selectedCategory}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* <Layers
|
|
||||||
crimes={filteredCrimes || []}
|
crimes={filteredCrimes || []}
|
||||||
year={selectedYear.toString()}
|
year={selectedYear.toString()}
|
||||||
month={selectedMonth.toString()}
|
month={selectedMonth.toString()}
|
||||||
filterCategory={selectedCategory}
|
filterCategory={selectedCategory}
|
||||||
/> */}
|
/> */}
|
||||||
|
|
||||||
|
<Layers
|
||||||
|
crimes={filteredCrimes || []}
|
||||||
|
year={selectedYear.toString()}
|
||||||
|
month={selectedMonth.toString()}
|
||||||
|
filterCategory={selectedCategory}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Popup for selected incident */}
|
{/* Popup for selected incident */}
|
||||||
{selectedIncident && selectedIncident.latitude && selectedIncident.longitude && (
|
{selectedIncident && selectedIncident.latitude && selectedIncident.longitude && (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { IBaseLayerProps } from "@/app/_utils/types/map"
|
import { IBaseLayerProps } from "@/app/_utils/types/map"
|
||||||
import { useEffect } from "react"
|
import { useEffect, useRef } from "react"
|
||||||
|
|
||||||
|
|
||||||
export default function FlyToHandler({ map }: Pick<IBaseLayerProps, "map">) {
|
export default function FlyToHandler({ map }: Pick<IBaseLayerProps, "map">) {
|
||||||
|
// Track active animations
|
||||||
|
const animationRef = useRef<number | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map) return
|
if (!map) return
|
||||||
|
|
||||||
|
@ -21,71 +24,96 @@ export default function FlyToHandler({ map }: Pick<IBaseLayerProps, "map">) {
|
||||||
duration: duration || 2000,
|
duration: duration || 2000,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Cancel any existing animation
|
||||||
|
if (animationRef.current) {
|
||||||
|
cancelAnimationFrame(animationRef.current)
|
||||||
|
}
|
||||||
|
|
||||||
// Add a highlight or pulse effect to the target incident
|
// Add a highlight or pulse effect to the target incident
|
||||||
if (map.getLayer("target-incident-highlight")) {
|
try {
|
||||||
map.removeLayer("target-incident-highlight")
|
if (map.getLayer("target-incident-highlight")) {
|
||||||
}
|
map.removeLayer("target-incident-highlight")
|
||||||
|
}
|
||||||
|
|
||||||
if (map.getSource("target-incident-highlight")) {
|
if (map.getSource("target-incident-highlight")) {
|
||||||
map.removeSource("target-incident-highlight")
|
map.removeSource("target-incident-highlight")
|
||||||
}
|
}
|
||||||
|
|
||||||
map.addSource("target-incident-highlight", {
|
map.addSource("target-incident-highlight", {
|
||||||
type: "geojson",
|
type: "geojson",
|
||||||
data: {
|
data: {
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
geometry: {
|
geometry: {
|
||||||
type: "Point",
|
type: "Point",
|
||||||
coordinates: [longitude, latitude],
|
coordinates: [longitude, latitude],
|
||||||
|
},
|
||||||
|
properties: {},
|
||||||
},
|
},
|
||||||
properties: {},
|
})
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
map.addLayer({
|
map.addLayer({
|
||||||
id: "target-incident-highlight",
|
id: "target-incident-highlight",
|
||||||
source: "target-incident-highlight",
|
source: "target-incident-highlight",
|
||||||
type: "circle",
|
type: "circle",
|
||||||
paint: {
|
paint: {
|
||||||
"circle-radius": ["interpolate", ["linear"], ["zoom"], 10, 10, 15, 15, 20, 20],
|
"circle-radius": ["interpolate", ["linear"], ["zoom"], 10, 10, 15, 15, 20, 20],
|
||||||
"circle-color": "#ff0000",
|
"circle-color": "#ff0000",
|
||||||
"circle-opacity": 0.7,
|
"circle-opacity": 0.7,
|
||||||
"circle-stroke-width": 2,
|
"circle-stroke-width": 2,
|
||||||
"circle-stroke-color": "#ffffff",
|
"circle-stroke-color": "#ffffff",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add a pulsing effect using animations
|
// Add a pulsing effect using animations
|
||||||
let size = 10
|
let size = 10
|
||||||
const animatePulse = () => {
|
const animatePulse = () => {
|
||||||
if (!map || !map.getLayer("target-incident-highlight")) return
|
if (!map || !map.getLayer("target-incident-highlight")) {
|
||||||
|
if (animationRef.current) {
|
||||||
|
cancelAnimationFrame(animationRef.current)
|
||||||
|
animationRef.current = null
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
size = (size % 20) + 1
|
size = (size % 20) + 1
|
||||||
|
|
||||||
map.setPaintProperty("target-incident-highlight", "circle-radius", [
|
map.setPaintProperty("target-incident-highlight", "circle-radius", [
|
||||||
"interpolate",
|
"interpolate",
|
||||||
["linear"],
|
["linear"],
|
||||||
["zoom"],
|
["zoom"],
|
||||||
10,
|
10,
|
||||||
size,
|
size,
|
||||||
15,
|
15,
|
||||||
size * 1.5,
|
size * 1.5,
|
||||||
20,
|
20,
|
||||||
size * 2,
|
size * 2,
|
||||||
])
|
])
|
||||||
|
|
||||||
requestAnimationFrame(animatePulse)
|
animationRef.current = requestAnimationFrame(animatePulse)
|
||||||
|
}
|
||||||
|
|
||||||
|
animationRef.current = requestAnimationFrame(animatePulse)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error adding highlight effect:", error)
|
||||||
}
|
}
|
||||||
|
|
||||||
requestAnimationFrame(animatePulse)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen for the custom fly-to event
|
||||||
map.getCanvas().addEventListener("mapbox_fly_to", handleFlyToEvent as EventListener)
|
map.getCanvas().addEventListener("mapbox_fly_to", handleFlyToEvent as EventListener)
|
||||||
|
// Also listen on the document to ensure we catch the event
|
||||||
|
document.addEventListener("mapbox_fly_to", handleFlyToEvent as EventListener)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (map && map.getCanvas()) {
|
if (map && map.getCanvas()) {
|
||||||
map.getCanvas().removeEventListener("mapbox_fly_to", handleFlyToEvent as EventListener)
|
map.getCanvas().removeEventListener("mapbox_fly_to", handleFlyToEvent as EventListener)
|
||||||
}
|
}
|
||||||
|
document.removeEventListener("mapbox_fly_to", handleFlyToEvent as EventListener)
|
||||||
|
|
||||||
|
// Clean up animation on unmount
|
||||||
|
if (animationRef.current) {
|
||||||
|
cancelAnimationFrame(animationRef.current)
|
||||||
|
animationRef.current = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [map])
|
}, [map])
|
||||||
|
|
||||||
|
|
|
@ -26,14 +26,43 @@ export default function ClusterLayer({
|
||||||
if (!features || features.length === 0) return
|
if (!features || features.length === 0) return
|
||||||
|
|
||||||
const clusterId: number = features[0].properties?.cluster_id as number
|
const clusterId: number = features[0].properties?.cluster_id as number
|
||||||
; (map.getSource("crime-incidents") as mapboxgl.GeoJSONSource).getClusterExpansionZoom(clusterId, (err, zoom) => {
|
|
||||||
if (err) return
|
|
||||||
|
|
||||||
map.easeTo({
|
try {
|
||||||
center: (features[0].geometry as any).coordinates,
|
// Get the expanded zoom level for this cluster
|
||||||
zoom: zoom ?? undefined,
|
(map.getSource("crime-incidents") as mapboxgl.GeoJSONSource).getClusterExpansionZoom(clusterId, (err, zoom) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Error getting cluster expansion zoom:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const coordinates = (features[0].geometry as any).coordinates
|
||||||
|
|
||||||
|
// Dispatch a custom event for the fly-to behavior
|
||||||
|
const clusterClickEvent = new CustomEvent('cluster_click', {
|
||||||
|
detail: {
|
||||||
|
center: coordinates,
|
||||||
|
zoom: zoom ?? undefined,
|
||||||
|
},
|
||||||
|
bubbles: true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (map.getCanvas()) {
|
||||||
|
map.getCanvas().dispatchEvent(clusterClickEvent)
|
||||||
|
} else {
|
||||||
|
document.dispatchEvent(clusterClickEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also perform the direct flyTo operation for immediate feedback
|
||||||
|
map.flyTo({
|
||||||
|
center: coordinates,
|
||||||
|
zoom: zoom ?? 12,
|
||||||
|
duration: 1000,
|
||||||
|
easing: (t) => t * (2 - t) // easeOutQuad
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error handling cluster click:", error)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[map],
|
[map],
|
||||||
)
|
)
|
||||||
|
@ -114,6 +143,7 @@ export default function ClusterLayer({
|
||||||
map.getCanvas().style.cursor = ""
|
map.getCanvas().style.cursor = ""
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Remove and re-add click handler to avoid duplicates
|
||||||
map.off("click", "clusters", handleClusterClick)
|
map.off("click", "clusters", handleClusterClick)
|
||||||
map.on("click", "clusters", handleClusterClick)
|
map.on("click", "clusters", handleClusterClick)
|
||||||
} else {
|
} else {
|
||||||
|
@ -124,6 +154,10 @@ export default function ClusterLayer({
|
||||||
if (map.getLayer("cluster-count")) {
|
if (map.getLayer("cluster-count")) {
|
||||||
map.setLayoutProperty("cluster-count", "visibility", focusedDistrictId ? "none" : "visible")
|
map.setLayoutProperty("cluster-count", "visibility", focusedDistrictId ? "none" : "visible")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the cluster click handler
|
||||||
|
map.off("click", "clusters", handleClusterClick)
|
||||||
|
map.on("click", "clusters", handleClusterClick)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error adding cluster layer:", error)
|
console.error("Error adding cluster layer:", error)
|
||||||
|
|
|
@ -14,6 +14,7 @@ export default function DistrictExtrusionLayer({
|
||||||
const animationRef = useRef<number | null>(null)
|
const animationRef = useRef<number | null>(null)
|
||||||
const bearingRef = useRef(0)
|
const bearingRef = useRef(0)
|
||||||
const rotationAnimationRef = useRef<number | null>(null)
|
const rotationAnimationRef = useRef<number | null>(null)
|
||||||
|
const extrusionCreatedRef = useRef(false)
|
||||||
|
|
||||||
// Handle extrusion layer creation and updates
|
// Handle extrusion layer creation and updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -32,55 +33,64 @@ export default function DistrictExtrusionLayer({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!map.getLayer("district-extrusion")) {
|
// Remove existing layer if it exists to avoid conflicts
|
||||||
map.addLayer(
|
if (map.getLayer("district-extrusion")) {
|
||||||
{
|
map.removeLayer("district-extrusion")
|
||||||
id: "district-extrusion",
|
extrusionCreatedRef.current = false
|
||||||
type: "fill-extrusion",
|
}
|
||||||
source: "districts",
|
|
||||||
"source-layer": "Districts",
|
// Make sure the districts source exists
|
||||||
paint: {
|
if (!map.getSource("districts")) {
|
||||||
"fill-extrusion-color": [
|
if (!tilesetId) {
|
||||||
"case",
|
console.error("No tileset ID provided for districts source");
|
||||||
["has", "kode_kec"],
|
return;
|
||||||
[
|
}
|
||||||
"match",
|
|
||||||
["get", "kode_kec"],
|
map.addSource("districts", {
|
||||||
focusedDistrictId || "",
|
type: "vector",
|
||||||
getCrimeRateColor(crimeDataByDistrict[focusedDistrictId || ""]?.level),
|
url: `mapbox://${tilesetId}`,
|
||||||
"transparent",
|
})
|
||||||
],
|
}
|
||||||
|
|
||||||
|
// 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": [
|
"transparent",
|
||||||
"case",
|
],
|
||||||
["has", "kode_kec"],
|
"fill-extrusion-height": [
|
||||||
["match", ["get", "kode_kec"], focusedDistrictId || "", 500, 0],
|
"case",
|
||||||
0,
|
["has", "kode_kec"],
|
||||||
],
|
["match", ["get", "kode_kec"], focusedDistrictId || "", 0, 0], // Start at 0 for animation
|
||||||
"fill-extrusion-base": 0,
|
0,
|
||||||
"fill-extrusion-opacity": 0.8,
|
],
|
||||||
},
|
"fill-extrusion-base": 0,
|
||||||
filter: ["==", ["get", "kode_kec"], focusedDistrictId || ""],
|
"fill-extrusion-opacity": 0.8,
|
||||||
},
|
},
|
||||||
firstSymbolId,
|
filter: ["==", ["get", "kode_kec"], focusedDistrictId || ""],
|
||||||
)
|
},
|
||||||
} else {
|
firstSymbolId
|
||||||
// Update existing layer
|
)
|
||||||
map.setFilter("district-extrusion", ["==", ["get", "kode_kec"], focusedDistrictId || ""])
|
|
||||||
|
|
||||||
map.setPaintProperty("district-extrusion", "fill-extrusion-color", [
|
extrusionCreatedRef.current = true
|
||||||
"case",
|
|
||||||
["has", "kode_kec"],
|
// If a district is focused, start the animation
|
||||||
[
|
if (focusedDistrictId) {
|
||||||
"match",
|
animateExtrusion()
|
||||||
["get", "kode_kec"],
|
|
||||||
focusedDistrictId || "",
|
|
||||||
getCrimeRateColor(crimeDataByDistrict[focusedDistrictId || ""]?.level),
|
|
||||||
"transparent",
|
|
||||||
],
|
|
||||||
"transparent",
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error adding district extrusion layer:", error)
|
console.error("Error adding district extrusion layer:", error)
|
||||||
|
@ -99,73 +109,42 @@ export default function DistrictExtrusionLayer({
|
||||||
animationRef.current = null
|
animationRef.current = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [map, visible, tilesetId, focusedDistrictId, crimeDataByDistrict])
|
}, [map, visible, tilesetId])
|
||||||
|
|
||||||
// Handle extrusion height animation
|
// Update filter and color when focused district changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map || !map.getLayer("district-extrusion")) return
|
if (!map || !map.getLayer("district-extrusion")) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Update the filter for the extrusion layer
|
||||||
|
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,
|
||||||
|
])
|
||||||
|
|
||||||
|
// Start animation if district is focused, otherwise reset
|
||||||
if (focusedDistrictId) {
|
if (focusedDistrictId) {
|
||||||
const startHeight = 0
|
animateExtrusion()
|
||||||
const targetHeight = 800
|
|
||||||
const duration = 700
|
|
||||||
const startTime = performance.now()
|
|
||||||
|
|
||||||
const animateHeight = (currentTime: number) => {
|
|
||||||
const elapsed = currentTime - startTime
|
|
||||||
const progress = Math.min(elapsed / duration, 1)
|
|
||||||
const easedProgress = progress * (2 - progress)
|
|
||||||
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,
|
|
||||||
])
|
|
||||||
|
|
||||||
if (progress < 1) {
|
|
||||||
animationRef.current = requestAnimationFrame(animateHeight)
|
|
||||||
} else {
|
|
||||||
// Start rotation after extrusion completes
|
|
||||||
startRotation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (animationRef.current) {
|
|
||||||
cancelAnimationFrame(animationRef.current)
|
|
||||||
}
|
|
||||||
animationRef.current = requestAnimationFrame(animateHeight)
|
|
||||||
} else {
|
} else {
|
||||||
const startHeight = 800
|
|
||||||
const targetHeight = 0
|
|
||||||
const duration = 500
|
|
||||||
const startTime = performance.now()
|
|
||||||
|
|
||||||
const animateHeightDown = (currentTime: number) => {
|
|
||||||
const elapsed = currentTime - startTime
|
|
||||||
const progress = Math.min(elapsed / duration, 1)
|
|
||||||
const easedProgress = progress * (2 - progress)
|
|
||||||
const currentHeight = startHeight + (targetHeight - startHeight) * easedProgress
|
|
||||||
|
|
||||||
map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
|
|
||||||
"case",
|
|
||||||
["has", "kode_kec"],
|
|
||||||
["match", ["get", "kode_kec"], "", currentHeight, 0],
|
|
||||||
0,
|
|
||||||
])
|
|
||||||
|
|
||||||
if (progress < 1) {
|
|
||||||
animationRef.current = requestAnimationFrame(animateHeightDown)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (animationRef.current) {
|
|
||||||
cancelAnimationFrame(animationRef.current)
|
|
||||||
}
|
|
||||||
animationRef.current = requestAnimationFrame(animateHeightDown)
|
|
||||||
|
|
||||||
// Stop rotation when unfocusing
|
// Stop rotation when unfocusing
|
||||||
if (rotationAnimationRef.current) {
|
if (rotationAnimationRef.current) {
|
||||||
cancelAnimationFrame(rotationAnimationRef.current)
|
cancelAnimationFrame(rotationAnimationRef.current)
|
||||||
|
@ -174,9 +153,9 @@ export default function DistrictExtrusionLayer({
|
||||||
bearingRef.current = 0
|
bearingRef.current = 0
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error animating district extrusion:", error)
|
console.error("Error updating district extrusion:", error)
|
||||||
}
|
}
|
||||||
}, [map, focusedDistrictId])
|
}, [map, focusedDistrictId, crimeDataByDistrict])
|
||||||
|
|
||||||
// Cleanup on unmount
|
// Cleanup on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -192,6 +171,43 @@ export default function DistrictExtrusionLayer({
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Animate extrusion height
|
||||||
|
const animateExtrusion = () => {
|
||||||
|
if (!map || !map.getLayer("district-extrusion") || !focusedDistrictId) return
|
||||||
|
|
||||||
|
if (animationRef.current) {
|
||||||
|
cancelAnimationFrame(animationRef.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animationRef.current = requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
|
||||||
// Start rotation animation
|
// Start rotation animation
|
||||||
const startRotation = () => {
|
const startRotation = () => {
|
||||||
if (!map || !focusedDistrictId) return
|
if (!map || !focusedDistrictId) return
|
||||||
|
|
|
@ -51,6 +51,31 @@ export default function Layers({
|
||||||
|
|
||||||
const crimeDataByDistrict = processCrimeDataByDistrict(crimes)
|
const crimeDataByDistrict = processCrimeDataByDistrict(crimes)
|
||||||
|
|
||||||
|
// Set up custom event handler for cluster clicks to ensure it works across components
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mapboxMap) return;
|
||||||
|
|
||||||
|
const handleClusterClickEvent = (e: CustomEvent) => {
|
||||||
|
if (!e.detail) return;
|
||||||
|
|
||||||
|
const { center, zoom } = e.detail;
|
||||||
|
if (center && zoom) {
|
||||||
|
mapboxMap.flyTo({
|
||||||
|
center: center,
|
||||||
|
zoom: zoom,
|
||||||
|
duration: 1000,
|
||||||
|
easing: (t) => t * (2 - t)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mapboxMap.getCanvas().addEventListener('cluster_click', handleClusterClickEvent as EventListener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mapboxMap.getCanvas().removeEventListener('cluster_click', handleClusterClickEvent as EventListener);
|
||||||
|
};
|
||||||
|
}, [mapboxMap]);
|
||||||
|
|
||||||
// Handle district selection
|
// Handle district selection
|
||||||
const handleDistrictClick = (district: IDistrictFeature) => {
|
const handleDistrictClick = (district: IDistrictFeature) => {
|
||||||
selectedDistrictRef.current = district
|
selectedDistrictRef.current = district
|
||||||
|
|
|
@ -33,20 +33,40 @@ export default function UnclusteredPointLayer({
|
||||||
status: incident.properties?.status || "Unknown",
|
status: incident.properties?.status || "Unknown",
|
||||||
longitude: (incident.geometry as any).coordinates[0],
|
longitude: (incident.geometry as any).coordinates[0],
|
||||||
latitude: (incident.geometry as any).coordinates[1],
|
latitude: (incident.geometry as any).coordinates[1],
|
||||||
timestamp: new Date(),
|
timestamp: new Date(incident.properties.timestamp || Date.now()),
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Incident clicked:", incidentDetails)
|
console.log("Incident clicked:", incidentDetails)
|
||||||
|
|
||||||
|
// Create a custom event with incident details
|
||||||
const customEvent = new CustomEvent("incident_click", {
|
const customEvent = new CustomEvent("incident_click", {
|
||||||
detail: incidentDetails,
|
detail: incidentDetails,
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Dispatch the event on both the map canvas and document to ensure it's captured
|
||||||
if (map.getCanvas()) {
|
if (map.getCanvas()) {
|
||||||
map.getCanvas().dispatchEvent(customEvent)
|
map.getCanvas().dispatchEvent(customEvent)
|
||||||
|
}
|
||||||
|
document.dispatchEvent(customEvent)
|
||||||
|
|
||||||
|
// Also trigger a fly-to event to zoom to the incident
|
||||||
|
const flyToEvent = new CustomEvent("mapbox_fly_to", {
|
||||||
|
detail: {
|
||||||
|
longitude: incidentDetails.longitude,
|
||||||
|
latitude: incidentDetails.latitude,
|
||||||
|
zoom: 15,
|
||||||
|
bearing: 0,
|
||||||
|
pitch: 45,
|
||||||
|
duration: 1000,
|
||||||
|
},
|
||||||
|
bubbles: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (map.getCanvas()) {
|
||||||
|
map.getCanvas().dispatchEvent(flyToEvent)
|
||||||
} else {
|
} else {
|
||||||
document.dispatchEvent(customEvent)
|
document.dispatchEvent(flyToEvent)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[map],
|
[map],
|
||||||
|
@ -101,6 +121,10 @@ export default function UnclusteredPointLayer({
|
||||||
} else if (map.getLayer("unclustered-point")) {
|
} else if (map.getLayer("unclustered-point")) {
|
||||||
// Update visibility based on focused district
|
// Update visibility based on focused district
|
||||||
map.setLayoutProperty("unclustered-point", "visibility", focusedDistrictId ? "none" : "visible")
|
map.setLayoutProperty("unclustered-point", "visibility", focusedDistrictId ? "none" : "visible")
|
||||||
|
|
||||||
|
// Ensure click handler is registered
|
||||||
|
map.off("click", "unclustered-point", handleIncidentClick)
|
||||||
|
map.on("click", "unclustered-point", handleIncidentClick)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error adding unclustered point layer:", error)
|
console.error("Error adding unclustered point layer:", error)
|
||||||
|
|
Loading…
Reference in New Issue