- Introduced a new MapBookmarkNameModal for adding and editing bookmarks. - Updated MapView to manage selected markers for bookmarking and handle bookmark name submissions. - Enhanced MapContextMenu with an option to add markers to bookmarks. - Improved MapBookmarks component to support editing bookmark names and adding selected markers. - Refactored MapControls and MapControlsContent to integrate selected marker functionality for bookmarks. - Updated useMapBookmarks composable to include bookmark updating logic. - Removed unused grid coordinates toggle from the UI for a cleaner interface.
172 lines
5.0 KiB
TypeScript
172 lines
5.0 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
|
|
}
|
|
|
|
const RECONNECT_INITIAL_MS = 1000
|
|
const RECONNECT_MAX_MS = 30000
|
|
|
|
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 BATCH_MS = 50
|
|
let batch: TileUpdate[] = []
|
|
let batchScheduled = false
|
|
let source: EventSource | null = null
|
|
let reconnectTimeoutId: ReturnType<typeof setTimeout> | null = null
|
|
let reconnectDelayMs = RECONNECT_INITIAL_MS
|
|
let destroyed = false
|
|
|
|
const VISIBLE_TILE_BUFFER = 1
|
|
|
|
function getVisibleTileBounds() {
|
|
const zoom = map.getZoom()
|
|
const px = map.getPixelBounds()
|
|
if (!px) return null
|
|
return {
|
|
zoom,
|
|
minX: Math.floor(px.min.x / TileSize) - VISIBLE_TILE_BUFFER,
|
|
maxX: Math.ceil(px.max.x / TileSize) + VISIBLE_TILE_BUFFER,
|
|
minY: Math.floor(px.min.y / TileSize) - VISIBLE_TILE_BUFFER,
|
|
maxY: Math.ceil(px.max.y / TileSize) + VISIBLE_TILE_BUFFER,
|
|
}
|
|
}
|
|
|
|
function applyBatch() {
|
|
batchScheduled = false
|
|
if (batch.length === 0) return
|
|
const updates = batch
|
|
batch = []
|
|
for (const u of updates) {
|
|
const key = `${u.M}:${u.X}:${u.Y}:${u.Z}`
|
|
layer.cache[key] = u.T
|
|
overlayLayer.cache[key] = u.T
|
|
}
|
|
const visible = getVisibleTileBounds()
|
|
for (const u of updates) {
|
|
if (visible && u.Z !== visible.zoom) continue
|
|
if (
|
|
visible &&
|
|
(u.X < visible.minX || u.X > visible.maxX || u.Y < visible.minY || u.Y > visible.maxY)
|
|
)
|
|
continue
|
|
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)
|
|
}
|
|
}
|
|
|
|
function scheduleBatch() {
|
|
if (batchScheduled) return
|
|
batchScheduled = true
|
|
setTimeout(applyBatch, BATCH_MS)
|
|
}
|
|
|
|
function connect() {
|
|
if (destroyed || !import.meta.client) return
|
|
source = new EventSource(updatesUrl)
|
|
if (connectionStateRef) connectionStateRef.value = 'connecting'
|
|
|
|
source.onopen = () => {
|
|
if (connectionStateRef) connectionStateRef.value = 'open'
|
|
reconnectDelayMs = RECONNECT_INITIAL_MS
|
|
}
|
|
|
|
source.onerror = () => {
|
|
if (destroyed || !source) return
|
|
if (connectionStateRef) connectionStateRef.value = 'error'
|
|
source.close()
|
|
source = null
|
|
if (destroyed) return
|
|
reconnectTimeoutId = setTimeout(() => {
|
|
reconnectTimeoutId = null
|
|
connect()
|
|
reconnectDelayMs = Math.min(reconnectDelayMs * 2, RECONNECT_MAX_MS)
|
|
}, reconnectDelayMs)
|
|
}
|
|
|
|
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[]) {
|
|
batch.push(u)
|
|
}
|
|
scheduleBatch()
|
|
} catch {
|
|
// Ignore parse errors from SSE
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
})
|
|
}
|
|
|
|
connect()
|
|
|
|
function cleanup() {
|
|
destroyed = true
|
|
if (reconnectTimeoutId != null) {
|
|
clearTimeout(reconnectTimeoutId)
|
|
reconnectTimeoutId = null
|
|
}
|
|
if (source) {
|
|
source.close()
|
|
source = null
|
|
}
|
|
}
|
|
|
|
return { cleanup }
|
|
}
|