Refactor client Leaflet integration to avoid conflicts

This commit is contained in:
Bernard Ngandu
2025-10-10 08:59:14 +02:00
parent 49d93ffc63
commit 5e094e9258
12 changed files with 144 additions and 91 deletions
+81 -66
View File
@@ -1,9 +1,12 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Badge } from '@ui/badge'
import { Button } from '@ui/button'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@ui/card'
import { Separator } from '@ui/separator'
import { cn, distanceInKm, formatCoordinate, formatRelativeTime } from '@lib/utils'
import L, { type LayerGroup, type LeafletMouseEvent, type Map as LeafletMap } from 'leaflet'
import 'leaflet.heat'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
import { cn, distanceInKm, formatCoordinate, formatRelativeTime } from '@/lib/utils'
const API_BASE = import.meta.env.VITE_API_BASE ?? 'http://localhost:8000/api/signals'
const VISIBLE_RADIUS_KM = 5
@@ -36,6 +39,25 @@ type ApiResponse = {
updatedAt?: string
}
type HeatPoint = [number, number, number?]
type LeafletHeatLayer = L.Layer & {
setLatLngs(points: HeatPoint[]): LeafletHeatLayer
}
interface HeatLayerOptions {
radius?: number
blur?: number
maxZoom?: number
max?: number
minOpacity?: number
gradient?: Record<number, string>
}
type LeafletWithHeat = typeof L & {
heatLayer?: (points: HeatPoint[], options?: HeatLayerOptions) => LeafletHeatLayer
}
interface LatLng {
lat: number
lng: number
@@ -70,12 +92,12 @@ function geolocationErrorMessage(error: GeolocationPositionError): string {
}
export default function App() {
const mapRef = useRef<any>(null)
const mapRef = useRef<LeafletMap | null>(null)
const mapContainerRef = useRef<HTMLDivElement | null>(null)
const heatLayerRef = useRef<any>(null)
const markersLayerRef = useRef<any>(null)
const zonesLayerRef = useRef<any>(null)
const userLayerRef = useRef<any>(null)
const heatLayerRef = useRef<LeafletHeatLayer | null>(null)
const markersLayerRef = useRef<LayerGroup | null>(null)
const zonesLayerRef = useRef<LayerGroup | null>(null)
const userLayerRef = useRef<LayerGroup | null>(null)
const locationWatchIdRef = useRef<number | null>(null)
const statusRef = useRef<Status>('loading')
const initialLoadRef = useRef(true)
@@ -229,15 +251,11 @@ export default function App() {
return
}
const leaflet = (window as any).L
if (!leaflet) {
setErrorMessage('Leaflet failed to load. Refresh the page to try again.')
setStatusSafe('error')
return
}
const leaflet = L as LeafletWithHeat
const container = mapContainerRef.current
const map = leaflet
.map(mapContainerRef.current, {
.map(container, {
worldCopyJump: true,
minZoom: 2,
zoomControl: true,
@@ -252,20 +270,21 @@ export default function App() {
})
.addTo(map)
const heatLayer = typeof leaflet.heatLayer === 'function'
? leaflet.heatLayer([], {
radius: 32,
blur: 24,
maxZoom: 12,
gradient: {
0.2: '#38bdf8',
0.4: '#0ea5e9',
0.6: '#fbbf24',
0.8: '#f97316',
1.0: '#ef4444',
},
})
: null
const heatLayer =
typeof leaflet.heatLayer === 'function'
? leaflet.heatLayer([], {
radius: 32,
blur: 24,
maxZoom: 12,
gradient: {
0.2: '#38bdf8',
0.4: '#0ea5e9',
0.6: '#fbbf24',
0.8: '#f97316',
1.0: '#ef4444',
},
})
: null
const markersLayer = leaflet.layerGroup().addTo(map)
const zonesLayer = leaflet.layerGroup().addTo(map)
@@ -275,8 +294,8 @@ export default function App() {
heatLayer.addTo(map)
}
const onClick = (event: any) => {
const { lat, lng } = event.latlng ?? {}
const onClick = (event: LeafletMouseEvent) => {
const { lat, lng } = event.latlng
if (typeof lat === 'number' && typeof lng === 'number') {
handleMapClick({ lat, lng })
}
@@ -314,7 +333,7 @@ export default function App() {
zonesLayerRef.current = null
userLayerRef.current = null
}
}, [handleMapClick, setStatusSafe])
}, [handleMapClick])
useEffect(() => {
const cleanup = initialiseMap()
@@ -382,8 +401,7 @@ export default function App() {
useEffect(() => {
const heatLayer = heatLayerRef.current
const leaflet = (window as any).L
if (!heatLayer || !leaflet) {
if (!heatLayer) {
return
}
@@ -393,14 +411,17 @@ export default function App() {
}
const maxIntensity = Math.max(...visibleDensity.map((entry) => entry.intensity)) || 1
const heatPoints = visibleDensity.map((entry) => [entry.lat, entry.lng, Math.max(0.25, entry.intensity / maxIntensity)])
const heatPoints: HeatPoint[] = visibleDensity.map((entry) => [
entry.lat,
entry.lng,
Math.max(0.25, entry.intensity / maxIntensity),
])
heatLayer.setLatLngs(heatPoints)
}, [visibleDensity])
useEffect(() => {
const layer = zonesLayerRef.current
const leaflet = (window as any).L
if (!layer || !leaflet) {
if (!layer) {
return
}
@@ -415,7 +436,7 @@ export default function App() {
visibleDangerZones.forEach((zone, index) => {
const intensityRatio = maxIntensity ? zone.intensity / maxIntensity : 0.5
const radius = 400 + intensityRatio * 1600
const circle = leaflet.circle([zone.lat, zone.lng], {
const circle = L.circle([zone.lat, zone.lng], {
radius,
color: index === 0 ? '#ef4444' : '#f97316',
weight: 2,
@@ -432,8 +453,7 @@ export default function App() {
useEffect(() => {
const layer = markersLayerRef.current
const leaflet = (window as any).L
if (!layer || !leaflet) {
if (!layer) {
return
}
@@ -449,7 +469,7 @@ export default function App() {
latestMap.forEach((point) => {
const isSelf = clientKey && point.userKey === clientKey
const marker = leaflet.circleMarker([point.lat, point.lng], {
const marker = L.circleMarker([point.lat, point.lng], {
radius: isSelf ? 10 : 6,
color: isSelf ? '#38bdf8' : '#94a3b8',
weight: isSelf ? 3 : 1.5,
@@ -470,8 +490,7 @@ export default function App() {
useEffect(() => {
const layer = userLayerRef.current
const leaflet = (window as any).L
if (!layer || !leaflet) {
if (!layer) {
return
}
@@ -481,28 +500,24 @@ export default function App() {
return
}
leaflet
.circle([userLocation.lat, userLocation.lng], {
radius: VISIBLE_RADIUS_KM * 1000,
color: '#38bdf8',
weight: 1.5,
opacity: 0.6,
dashArray: '6 6',
fillColor: '#38bdf8',
fillOpacity: 0.05,
})
.addTo(layer)
L.circle([userLocation.lat, userLocation.lng], {
radius: VISIBLE_RADIUS_KM * 1000,
color: '#38bdf8',
weight: 1.5,
opacity: 0.6,
dashArray: '6 6',
fillColor: '#38bdf8',
fillOpacity: 0.05,
}).addTo(layer)
leaflet
.circleMarker([userLocation.lat, userLocation.lng], {
radius: 7,
color: '#38bdf8',
weight: 2,
opacity: 0.9,
fillColor: '#38bdf8',
fillOpacity: 0.5,
})
.addTo(layer)
L.circleMarker([userLocation.lat, userLocation.lng], {
radius: 7,
color: '#38bdf8',
weight: 2,
opacity: 0.9,
fillColor: '#38bdf8',
fillOpacity: 0.5,
}).addTo(layer)
}, [userLocation])
useEffect(() => {