- Introduced MapErrorBoundary component to handle map loading errors gracefully. - Enhanced MapView with a reconnection status indicator for live updates. - Added tile freshness animation to indicate updated tiles visually. - Preloaded marker icon images to improve rendering performance. - Updated various pages to utilize the new MapErrorBoundary for better user experience.
99 lines
2.9 KiB
TypeScript
99 lines
2.9 KiB
TypeScript
import type { Ref } from 'vue'
|
|
import type { SmartTileLayer } from '~/lib/SmartTileLayer'
|
|
import { TileSize } from '~/lib/LeafletCustomTypes'
|
|
import type L from 'leaflet'
|
|
|
|
type SmartTileLayerInstance = InstanceType<typeof SmartTileLayer>
|
|
|
|
export type SseConnectionState = 'connecting' | 'open' | 'error'
|
|
|
|
interface TileUpdate {
|
|
M: number
|
|
X: number
|
|
Y: number
|
|
Z: number
|
|
T: number
|
|
}
|
|
|
|
interface MergeEvent {
|
|
From: number
|
|
To: number
|
|
Shift: { x: number; y: number }
|
|
}
|
|
|
|
export interface UseMapUpdatesOptions {
|
|
backendBase: string
|
|
layer: SmartTileLayerInstance
|
|
overlayLayer: SmartTileLayerInstance
|
|
map: L.Map
|
|
getCurrentMapId: () => number
|
|
onMerge: (mapTo: number, shift: { x: number; y: number }) => void
|
|
/** Optional ref updated with SSE connection state for reconnection indicator. */
|
|
connectionStateRef?: Ref<SseConnectionState>
|
|
}
|
|
|
|
export interface UseMapUpdatesReturn {
|
|
cleanup: () => void
|
|
}
|
|
|
|
export function startMapUpdates(options: UseMapUpdatesOptions): UseMapUpdatesReturn {
|
|
const { backendBase, layer, overlayLayer, map, getCurrentMapId, onMerge, connectionStateRef } = options
|
|
|
|
const updatesPath = `${backendBase}/updates`
|
|
const updatesUrl = import.meta.client ? `${window.location.origin}${updatesPath}` : updatesPath
|
|
const source = new EventSource(updatesUrl)
|
|
|
|
if (connectionStateRef) {
|
|
connectionStateRef.value = 'connecting'
|
|
}
|
|
source.onopen = () => {
|
|
if (connectionStateRef) connectionStateRef.value = 'open'
|
|
}
|
|
source.onerror = () => {
|
|
if (connectionStateRef) connectionStateRef.value = 'error'
|
|
}
|
|
|
|
source.onmessage = (event: MessageEvent) => {
|
|
if (connectionStateRef) connectionStateRef.value = 'open'
|
|
try {
|
|
const raw: unknown = event?.data
|
|
if (raw == null || typeof raw !== 'string' || raw.trim() === '') return
|
|
const updates: unknown = JSON.parse(raw)
|
|
if (!Array.isArray(updates)) return
|
|
for (const u of updates as TileUpdate[]) {
|
|
const key = `${u.M}:${u.X}:${u.Y}:${u.Z}`
|
|
layer.cache[key] = u.T
|
|
overlayLayer.cache[key] = u.T
|
|
if (layer.map === u.M) layer.refresh(u.X, u.Y, u.Z)
|
|
if (overlayLayer.map === u.M) overlayLayer.refresh(u.X, u.Y, u.Z)
|
|
}
|
|
} catch {
|
|
// Ignore parse errors from SSE
|
|
}
|
|
}
|
|
|
|
source.onerror = () => {}
|
|
|
|
source.addEventListener('merge', (e: MessageEvent) => {
|
|
try {
|
|
const merge: MergeEvent = JSON.parse((e?.data as string) ?? '{}')
|
|
if (getCurrentMapId() === merge.From) {
|
|
const point = map.project(map.getCenter(), 6)
|
|
const shift = {
|
|
x: Math.floor(point.x / TileSize) + merge.Shift.x,
|
|
y: Math.floor(point.y / TileSize) + merge.Shift.y,
|
|
}
|
|
onMerge(merge.To, shift)
|
|
}
|
|
} catch {
|
|
// Ignore merge parse errors
|
|
}
|
|
})
|
|
|
|
function cleanup() {
|
|
source.close()
|
|
}
|
|
|
|
return { cleanup }
|
|
}
|