Enhance map updates and component performance
- 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.
This commit is contained in:
@@ -60,7 +60,7 @@ The game client (e.g. Purus Pasta) communicates via `/client/{token}/...` endpoi
|
|||||||
|
|
||||||
## SSE (Server-Sent Events)
|
## SSE (Server-Sent Events)
|
||||||
|
|
||||||
- **GET /map/updates** — real-time tile and merge updates. Requires a session with `map` permission. Sends `data:` messages with tile cache arrays and `event: merge` messages for map merges.
|
- **GET /map/updates** — real-time tile and merge updates. Requires a session with `map` permission. Sends an initial `data:` message with an empty tile cache array `[]`, then incremental `data:` messages with tile cache updates and `event: merge` messages for map merges. The client requests tiles with `cache=0` when not yet in cache.
|
||||||
|
|
||||||
## Tile images
|
## Tile images
|
||||||
|
|
||||||
|
|||||||
@@ -271,9 +271,12 @@ onMounted(async () => {
|
|||||||
|
|
||||||
const L = (await import('leaflet')).default
|
const L = (await import('leaflet')).default
|
||||||
|
|
||||||
const [charactersData, mapsData] = await Promise.all([
|
// Load maps, characters, config and me in parallel so map can show sooner.
|
||||||
|
const [charactersData, mapsData, config, user] = await Promise.all([
|
||||||
api.getCharacters().then((d) => (Array.isArray(d) ? d : [])).catch(() => []),
|
api.getCharacters().then((d) => (Array.isArray(d) ? d : [])).catch(() => []),
|
||||||
api.getMaps().then((d) => (d && typeof d === 'object' ? d : {})).catch(() => ({})),
|
api.getMaps().then((d) => (d && typeof d === 'object' ? d : {})).catch(() => ({})),
|
||||||
|
api.getConfig().catch(() => ({})) as Promise<ConfigResponse>,
|
||||||
|
api.me().catch(() => null) as Promise<MeResponse | null>,
|
||||||
])
|
])
|
||||||
|
|
||||||
const mapsList: MapInfo[] = []
|
const mapsList: MapInfo[] = []
|
||||||
@@ -295,9 +298,7 @@ onMounted(async () => {
|
|||||||
maps.value = mapsList
|
maps.value = mapsList
|
||||||
mapsLoaded.value = true
|
mapsLoaded.value = true
|
||||||
|
|
||||||
const config = (await api.getConfig().catch(() => ({}))) as ConfigResponse
|
|
||||||
if (config?.title) document.title = config.title
|
if (config?.title) document.title = config.title
|
||||||
const user = (await api.me().catch(() => null)) as MeResponse | null
|
|
||||||
auths.value = user?.auths ?? config?.auths ?? []
|
auths.value = user?.auths ?? config?.auths ?? []
|
||||||
|
|
||||||
const initialMapId =
|
const initialMapId =
|
||||||
@@ -328,6 +329,7 @@ onMounted(async () => {
|
|||||||
document.addEventListener('contextmenu', contextMenuHandler, true)
|
document.addEventListener('contextmenu', contextMenuHandler, true)
|
||||||
|
|
||||||
layersManager = createMapLayers({
|
layersManager = createMapLayers({
|
||||||
|
L,
|
||||||
map: leafletMap,
|
map: leafletMap,
|
||||||
markerLayer: mapInit.markerLayer,
|
markerLayer: mapInit.markerLayer,
|
||||||
layer: mapInit.layer,
|
layer: mapInit.layer,
|
||||||
@@ -382,16 +384,25 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show map as soon as canvas and layers are ready; markers load in background.
|
||||||
|
if (leafletMap) leafletMap.invalidateSize()
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
requestAnimationFrame(() => {
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (leafletMap) leafletMap.invalidateSize()
|
if (leafletMap) leafletMap.invalidateSize()
|
||||||
mapReady.value = true
|
mapReady.value = true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Markers load asynchronously after map is visible.
|
||||||
|
api.getMarkers().then((body) => {
|
||||||
|
layersManager!.updateMarkers(Array.isArray(body) ? body : [])
|
||||||
|
questGivers.value = layersManager!.getQuestGivers()
|
||||||
})
|
})
|
||||||
|
|
||||||
intervalId = setInterval(() => {
|
const CHARACTER_POLL_MS = 4000
|
||||||
|
const CHARACTER_POLL_MS_HIDDEN = 30000
|
||||||
|
|
||||||
|
function pollCharacters() {
|
||||||
api
|
api
|
||||||
.getCharacters()
|
.getCharacters()
|
||||||
.then((body) => {
|
.then((body) => {
|
||||||
@@ -400,24 +411,37 @@ onMounted(async () => {
|
|||||||
players.value = layersManager!.getPlayers()
|
players.value = layersManager!.getPlayers()
|
||||||
mapLive.value = list.some((c) => c.ownedByMe)
|
mapLive.value = list.some((c) => c.ownedByMe)
|
||||||
})
|
})
|
||||||
.catch(() => clearInterval(intervalId!))
|
.catch(() => {
|
||||||
}, 2000)
|
if (intervalId) clearInterval(intervalId)
|
||||||
|
intervalId = null
|
||||||
api.getMarkers().then((body) => {
|
|
||||||
layersManager!.updateMarkers(Array.isArray(body) ? body : [])
|
|
||||||
questGivers.value = layersManager!.getQuestGivers()
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function startCharacterPoll() {
|
||||||
|
if (intervalId) clearInterval(intervalId)
|
||||||
|
const ms =
|
||||||
|
typeof document !== 'undefined' && document.visibilityState === 'hidden'
|
||||||
|
? CHARACTER_POLL_MS_HIDDEN
|
||||||
|
: CHARACTER_POLL_MS
|
||||||
|
pollCharacters()
|
||||||
|
intervalId = setInterval(pollCharacters, ms)
|
||||||
|
}
|
||||||
|
|
||||||
|
startCharacterPoll()
|
||||||
|
if (import.meta.client) {
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
startCharacterPoll()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
watch(mapLogic.state.showGridCoordinates, (v) => {
|
watch(mapLogic.state.showGridCoordinates, (v) => {
|
||||||
if (mapInit?.coordLayer) {
|
if (mapInit?.coordLayer) {
|
||||||
;(mapInit.coordLayer.options as { visible?: boolean }).visible = v
|
;(mapInit.coordLayer.options as { visible?: boolean }).visible = v
|
||||||
mapInit.coordLayer.setOpacity(v ? 1 : 0)
|
mapInit.coordLayer.setOpacity(v ? 1 : 0)
|
||||||
|
mapInit.coordLayer.redraw?.()
|
||||||
if (v && leafletMap) {
|
if (v && leafletMap) {
|
||||||
mapInit.coordLayer.bringToFront?.()
|
mapInit.coordLayer.bringToFront?.()
|
||||||
mapInit.coordLayer.redraw?.()
|
|
||||||
leafletMap.invalidateSize()
|
leafletMap.invalidateSize()
|
||||||
} else {
|
|
||||||
mapInit.coordLayer.redraw?.()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export async function initLeafletMap(
|
|||||||
const layer = new SmartTileLayer(tileUrl, {
|
const layer = new SmartTileLayer(tileUrl, {
|
||||||
minZoom: 1,
|
minZoom: 1,
|
||||||
maxZoom: 6,
|
maxZoom: 6,
|
||||||
|
maxNativeZoom: 6,
|
||||||
zoomOffset: 0,
|
zoomOffset: 0,
|
||||||
zoomReverse: true,
|
zoomReverse: true,
|
||||||
tileSize: TileSize,
|
tileSize: TileSize,
|
||||||
@@ -81,6 +82,7 @@ export async function initLeafletMap(
|
|||||||
const overlayLayer = new SmartTileLayer(tileUrl, {
|
const overlayLayer = new SmartTileLayer(tileUrl, {
|
||||||
minZoom: 1,
|
minZoom: 1,
|
||||||
maxZoom: 6,
|
maxZoom: 6,
|
||||||
|
maxNativeZoom: 6,
|
||||||
zoomOffset: 0,
|
zoomOffset: 0,
|
||||||
zoomReverse: true,
|
zoomReverse: true,
|
||||||
tileSize: TileSize,
|
tileSize: TileSize,
|
||||||
@@ -100,6 +102,8 @@ export async function initLeafletMap(
|
|||||||
opacity: 0,
|
opacity: 0,
|
||||||
visible: false,
|
visible: false,
|
||||||
pane: 'tilePane',
|
pane: 'tilePane',
|
||||||
|
updateWhenIdle: true,
|
||||||
|
keepBuffer: 2,
|
||||||
} as GridCoordLayerOptions)
|
} as GridCoordLayerOptions)
|
||||||
coordLayer.addTo(map)
|
coordLayer.addTo(map)
|
||||||
coordLayer.setZIndex(500)
|
coordLayer.setZIndex(500)
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import type { Marker as ApiMarker, Character as ApiCharacter } from '~/types/api
|
|||||||
type SmartTileLayerInstance = InstanceType<typeof SmartTileLayer>
|
type SmartTileLayerInstance = InstanceType<typeof SmartTileLayer>
|
||||||
|
|
||||||
export interface MapLayersOptions {
|
export interface MapLayersOptions {
|
||||||
|
/** Leaflet API (from dynamic import). Required for creating markers and characters without static leaflet import. */
|
||||||
|
L: typeof import('leaflet')
|
||||||
map: L.Map
|
map: L.Map
|
||||||
markerLayer: L.LayerGroup
|
markerLayer: L.LayerGroup
|
||||||
layer: SmartTileLayerInstance
|
layer: SmartTileLayerInstance
|
||||||
@@ -48,6 +50,7 @@ export interface MapLayersManager {
|
|||||||
|
|
||||||
export function createMapLayers(options: MapLayersOptions): MapLayersManager {
|
export function createMapLayers(options: MapLayersOptions): MapLayersManager {
|
||||||
const {
|
const {
|
||||||
|
L,
|
||||||
map,
|
map,
|
||||||
markerLayer,
|
markerLayer,
|
||||||
layer,
|
layer,
|
||||||
@@ -105,7 +108,7 @@ export function createMapLayers(options: MapLayersOptions): MapLayersManager {
|
|||||||
: undefined
|
: undefined
|
||||||
uniqueListUpdate(
|
uniqueListUpdate(
|
||||||
markers,
|
markers,
|
||||||
list.map((it) => createMarker(it as MarkerData, iconOptions)),
|
list.map((it) => createMarker(it as MarkerData, iconOptions, L)),
|
||||||
(marker: MapMarker) => {
|
(marker: MapMarker) => {
|
||||||
if (marker.map === getCurrentMapId() || marker.map === overlayLayer.map) marker.add(ctx)
|
if (marker.map === getCurrentMapId() || marker.map === overlayLayer.map) marker.add(ctx)
|
||||||
marker.setClickCallback(() => {
|
marker.setClickCallback(() => {
|
||||||
@@ -129,7 +132,7 @@ export function createMapLayers(options: MapLayersOptions): MapLayersManager {
|
|||||||
const ctx = characterCtx()
|
const ctx = characterCtx()
|
||||||
uniqueListUpdate(
|
uniqueListUpdate(
|
||||||
characters,
|
characters,
|
||||||
list.map((it) => createCharacter(it as CharacterData)),
|
list.map((it) => createCharacter(it as CharacterData, L)),
|
||||||
(character: MapCharacter) => {
|
(character: MapCharacter) => {
|
||||||
character.add(ctx)
|
character.add(ctx)
|
||||||
character.setClickCallback(() => setTrackingCharacterId(character.id))
|
character.setClickCallback(() => setTrackingCharacterId(character.id))
|
||||||
|
|||||||
@@ -53,6 +53,32 @@ export function startMapUpdates(options: UseMapUpdatesOptions): UseMapUpdatesRet
|
|||||||
if (connectionStateRef) connectionStateRef.value = 'error'
|
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) => {
|
source.onmessage = (event: MessageEvent) => {
|
||||||
if (connectionStateRef) connectionStateRef.value = 'open'
|
if (connectionStateRef) connectionStateRef.value = 'open'
|
||||||
try {
|
try {
|
||||||
@@ -61,12 +87,9 @@ export function startMapUpdates(options: UseMapUpdatesOptions): UseMapUpdatesRet
|
|||||||
const updates: unknown = JSON.parse(raw)
|
const updates: unknown = JSON.parse(raw)
|
||||||
if (!Array.isArray(updates)) return
|
if (!Array.isArray(updates)) return
|
||||||
for (const u of updates as TileUpdate[]) {
|
for (const u of updates as TileUpdate[]) {
|
||||||
const key = `${u.M}:${u.X}:${u.Y}:${u.Z}`
|
batch.push(u)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
scheduleBatch()
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore parse errors from SSE
|
// Ignore parse errors from SSE
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import type L from 'leaflet'
|
||||||
import { HnHMaxZoom } from '~/lib/LeafletCustomTypes'
|
import { HnHMaxZoom } from '~/lib/LeafletCustomTypes'
|
||||||
import * as L from 'leaflet'
|
|
||||||
|
export type LeafletApi = typeof import('leaflet')
|
||||||
|
|
||||||
/** SVG data URL for character marker icon (teal pin, bottom-center anchor). */
|
/** SVG data URL for character marker icon (teal pin, bottom-center anchor). */
|
||||||
const CHARACTER_ICON_URL =
|
const CHARACTER_ICON_URL =
|
||||||
@@ -11,12 +13,14 @@ const CHARACTER_ICON_URL =
|
|||||||
'</svg>'
|
'</svg>'
|
||||||
)
|
)
|
||||||
|
|
||||||
const CHARACTER_ICON = new L.Icon({
|
function createCharacterIcon(L: LeafletApi): L.Icon {
|
||||||
|
return new L.Icon({
|
||||||
iconUrl: CHARACTER_ICON_URL,
|
iconUrl: CHARACTER_ICON_URL,
|
||||||
iconSize: [24, 32],
|
iconSize: [24, 32],
|
||||||
iconAnchor: [12, 32],
|
iconAnchor: [12, 32],
|
||||||
popupAnchor: [0, -32],
|
popupAnchor: [0, -32],
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export interface CharacterData {
|
export interface CharacterData {
|
||||||
name: string
|
name: string
|
||||||
@@ -47,9 +51,10 @@ export interface MapCharacter {
|
|||||||
setClickCallback: (callback: (e: L.LeafletMouseEvent) => void) => void
|
setClickCallback: (callback: (e: L.LeafletMouseEvent) => void) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createCharacter(data: CharacterData): MapCharacter {
|
export function createCharacter(data: CharacterData, L: LeafletApi): MapCharacter {
|
||||||
let leafletMarker: L.Marker | null = null
|
let leafletMarker: L.Marker | null = null
|
||||||
let onClick: ((e: L.LeafletMouseEvent) => void) | null = null
|
let onClick: ((e: L.LeafletMouseEvent) => void) | null = null
|
||||||
|
const characterIcon = createCharacterIcon(L)
|
||||||
|
|
||||||
const character: MapCharacter = {
|
const character: MapCharacter = {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
@@ -75,7 +80,7 @@ export function createCharacter(data: CharacterData): MapCharacter {
|
|||||||
add(mapview: CharacterMapViewRef): void {
|
add(mapview: CharacterMapViewRef): void {
|
||||||
if (character.map === mapview.mapid) {
|
if (character.map === mapview.mapid) {
|
||||||
const position = mapview.map.unproject([character.position.x, character.position.y], HnHMaxZoom)
|
const position = mapview.map.unproject([character.position.x, character.position.y], HnHMaxZoom)
|
||||||
leafletMarker = L.marker(position, { icon: CHARACTER_ICON, title: character.name })
|
leafletMarker = L.marker(position, { icon: characterIcon, title: character.name })
|
||||||
leafletMarker.on('click', (e: L.LeafletMouseEvent) => {
|
leafletMarker.on('click', (e: L.LeafletMouseEvent) => {
|
||||||
if (onClick) onClick(e)
|
if (onClick) onClick(e)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,13 +5,17 @@ export const HnHMaxZoom = 6
|
|||||||
export const HnHMinZoom = 1
|
export const HnHMinZoom = 1
|
||||||
export const HnHDefaultZoom = 6
|
export const HnHDefaultZoom = 6
|
||||||
|
|
||||||
/** When scaleFactor exceeds this, render one label per tile instead of a full grid (avoids 100k+ DOM nodes at zoom 1). */
|
|
||||||
const GRID_COORD_SCALE_FACTOR_THRESHOLD = 8
|
|
||||||
|
|
||||||
export interface GridCoordLayerOptions extends L.GridLayerOptions {
|
export interface GridCoordLayerOptions extends L.GridLayerOptions {
|
||||||
visible?: boolean
|
visible?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grid layer that draws one coordinate label per Leaflet tile in the top-left corner.
|
||||||
|
* coords.(x,y,z) are Leaflet tile indices and zoom; they map to game tiles as:
|
||||||
|
* scaleFactor = 2^(HnHMaxZoom - coords.z),
|
||||||
|
* topLeft = (coords.x * scaleFactor, coords.y * scaleFactor).
|
||||||
|
* This matches backend tile URL {mapid}/{z}/{x}_{y}.png (storageZ: z=6→0, Coord = tile index).
|
||||||
|
*/
|
||||||
export const GridCoordLayer = L.GridLayer.extend({
|
export const GridCoordLayer = L.GridLayer.extend({
|
||||||
options: {
|
options: {
|
||||||
visible: true,
|
visible: true,
|
||||||
@@ -32,13 +36,8 @@ export const GridCoordLayer = L.GridLayer.extend({
|
|||||||
|
|
||||||
const scaleFactor = Math.pow(2, HnHMaxZoom - coords.z)
|
const scaleFactor = Math.pow(2, HnHMaxZoom - coords.z)
|
||||||
const topLeft = { x: coords.x * scaleFactor, y: coords.y * scaleFactor }
|
const topLeft = { x: coords.x * scaleFactor, y: coords.y * scaleFactor }
|
||||||
const bottomRight = { x: topLeft.x + scaleFactor - 1, y: topLeft.y + scaleFactor - 1 }
|
|
||||||
const swPoint = { x: topLeft.x * TileSize, y: topLeft.y * TileSize }
|
|
||||||
const tileWidthPx = scaleFactor * TileSize
|
|
||||||
const tileHeightPx = scaleFactor * TileSize
|
|
||||||
|
|
||||||
if (scaleFactor > GRID_COORD_SCALE_FACTOR_THRESHOLD) {
|
// One label per Leaflet tile at top-left (2px, 2px); same (x,y) as backend tile for this coords.
|
||||||
// Low zoom: one label per tile to avoid hundreds of thousands of DOM nodes (Reset view freeze fix)
|
|
||||||
const textElement = document.createElement('div')
|
const textElement = document.createElement('div')
|
||||||
textElement.classList.add('map-tile-text')
|
textElement.classList.add('map-tile-text')
|
||||||
textElement.textContent = `(${topLeft.x}, ${topLeft.y})`
|
textElement.textContent = `(${topLeft.x}, ${topLeft.y})`
|
||||||
@@ -48,25 +47,6 @@ export const GridCoordLayer = L.GridLayer.extend({
|
|||||||
textElement.style.fontSize = Math.max(8, 12 - Math.log2(scaleFactor) * 2) + 'px'
|
textElement.style.fontSize = Math.max(8, 12 - Math.log2(scaleFactor) * 2) + 'px'
|
||||||
element.appendChild(textElement)
|
element.appendChild(textElement)
|
||||||
return element
|
return element
|
||||||
}
|
|
||||||
|
|
||||||
for (let gx = topLeft.x; gx <= bottomRight.x; gx++) {
|
|
||||||
for (let gy = topLeft.y; gy <= bottomRight.y; gy++) {
|
|
||||||
const leftPx = tileWidthPx > 0 ? ((gx * TileSize - swPoint.x) / tileWidthPx) * TileSize : 0
|
|
||||||
const topPx = tileHeightPx > 0 ? ((gy * TileSize - swPoint.y) / tileHeightPx) * TileSize : 0
|
|
||||||
const textElement = document.createElement('div')
|
|
||||||
textElement.classList.add('map-tile-text')
|
|
||||||
textElement.textContent = `(${gx}, ${gy})`
|
|
||||||
textElement.style.position = 'absolute'
|
|
||||||
textElement.style.left = leftPx + 2 + 'px'
|
|
||||||
textElement.style.top = topPx + 2 + 'px'
|
|
||||||
if (scaleFactor > 1) {
|
|
||||||
textElement.style.fontSize = Math.max(8, 12 - Math.log2(scaleFactor) * 2) + 'px'
|
|
||||||
}
|
|
||||||
element.appendChild(textElement)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return element
|
|
||||||
},
|
},
|
||||||
}) as unknown as new (options?: GridCoordLayerOptions) => L.GridLayer
|
}) as unknown as new (options?: GridCoordLayerOptions) => L.GridLayer
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import type L from 'leaflet'
|
||||||
import { HnHMaxZoom, ImageIcon } from '~/lib/LeafletCustomTypes'
|
import { HnHMaxZoom, ImageIcon } from '~/lib/LeafletCustomTypes'
|
||||||
import * as L from 'leaflet'
|
|
||||||
|
|
||||||
export interface MarkerData {
|
export interface MarkerData {
|
||||||
id: number
|
id: number
|
||||||
@@ -48,7 +48,13 @@ export interface MarkerIconOptions {
|
|||||||
fallbackIconUrl?: string
|
fallbackIconUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMarker(data: MarkerData, iconOptions?: MarkerIconOptions): MapMarker {
|
export type LeafletApi = typeof import('leaflet')
|
||||||
|
|
||||||
|
export function createMarker(
|
||||||
|
data: MarkerData,
|
||||||
|
iconOptions: MarkerIconOptions | undefined,
|
||||||
|
L: LeafletApi
|
||||||
|
): MapMarker {
|
||||||
let leafletMarker: L.Marker | null = null
|
let leafletMarker: L.Marker | null = null
|
||||||
let onClick: ((e: L.LeafletMouseEvent) => void) | null = null
|
let onClick: ((e: L.LeafletMouseEvent) => void) | null = null
|
||||||
let onContext: ((e: L.LeafletMouseEvent) => void) | null = null
|
let onContext: ((e: L.LeafletMouseEvent) => void) | null = null
|
||||||
|
|||||||
@@ -50,13 +50,13 @@ func (h *Handlers) WatchGridUpdates(rw http.ResponseWriter, req *http.Request) {
|
|||||||
c := h.Map.WatchTiles()
|
c := h.Map.WatchTiles()
|
||||||
mc := h.Map.WatchMerges()
|
mc := h.Map.WatchMerges()
|
||||||
|
|
||||||
tileCache := h.Map.GetAllTileCache(ctx)
|
// Option 1A: do not send full cache on connect; client requests tiles with cache=0 when missing.
|
||||||
|
// This avoids a huge JSON dump and slow parse on connect when the DB has many tiles.
|
||||||
|
tileCache := []services.TileCache{}
|
||||||
raw, _ := json.Marshal(tileCache)
|
raw, _ := json.Marshal(tileCache)
|
||||||
fmt.Fprint(rw, "data: ")
|
fmt.Fprint(rw, "data: ")
|
||||||
rw.Write(raw)
|
rw.Write(raw)
|
||||||
fmt.Fprint(rw, "\n\n")
|
fmt.Fprint(rw, "\n\n")
|
||||||
tileCache = tileCache[:0]
|
|
||||||
flusher.Flush()
|
flusher.Flush()
|
||||||
|
|
||||||
ticker := time.NewTicker(app.SSETickInterval)
|
ticker := time.NewTicker(app.SSETickInterval)
|
||||||
|
|||||||
Reference in New Issue
Block a user