Refactor client Leaflet integration to avoid conflicts
This commit is contained in:
+81
-66
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user