Refactor Dockerignore and enhance Leaflet styles for improved map functionality
- Updated .dockerignore to streamline build context by ensuring unnecessary files are excluded. - Refined CSS styles in leaflet-overrides.css to enhance visual consistency and user experience for map tooltips and popups. - Improved map initialization and update handling in useMapApi and useMapUpdates composables for better performance and reliability.
This commit is contained in:
@@ -1,171 +1,171 @@
|
||||
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 }
|
||||
}
|
||||
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 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user