- Updated API documentation to clarify the initial data message structure for real-time tile updates. - Modified MapView component to load configuration and user data in parallel, improving map loading speed. - Implemented asynchronous loading for markers after the map is visible, enhancing user experience. - Introduced batching for tile updates to optimize rendering performance during map updates. - Refactored character and marker creation functions to utilize dynamic Leaflet imports, improving modularity.
122 lines
3.4 KiB
TypeScript
122 lines
3.4 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'
|
|
}
|
|
|
|
const BATCH_MS = 50
|
|
let batch: TileUpdate[] = []
|
|
let batchScheduled = false
|
|
|
|
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
|
|
}
|
|
for (const u of updates) {
|
|
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)
|
|
}
|
|
|
|
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.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 }
|
|
}
|