Files
hnh-map/frontend-nuxt/composables/useMapLayers.ts
Nikolay Tatarinov 49af08c13f 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.
2026-03-01 17:30:48 +03:00

206 lines
7.0 KiB
TypeScript

import type L from 'leaflet'
import { HnHMaxZoom } from '~/lib/LeafletCustomTypes'
import { createMarker, type MapMarker, type MarkerData, type MapViewRef } from '~/lib/Marker'
import { createCharacter, type MapCharacter, type CharacterData, type CharacterMapViewRef } from '~/lib/Character'
import {
createUniqueList,
uniqueListUpdate,
uniqueListGetElements,
uniqueListById,
type UniqueList,
} from '~/lib/UniqueList'
import type { SmartTileLayer } from '~/lib/SmartTileLayer'
import type { Marker as ApiMarker, Character as ApiCharacter } from '~/types/api'
type SmartTileLayerInstance = InstanceType<typeof SmartTileLayer>
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
markerLayer: L.LayerGroup
layer: SmartTileLayerInstance
overlayLayer: SmartTileLayerInstance
getCurrentMapId: () => number
setCurrentMapId: (id: number) => void
setSelectedMapId: (id: number) => void
getAuths: () => string[]
getTrackingCharacterId: () => number
setTrackingCharacterId: (id: number) => void
onMarkerContextMenu: (clientX: number, clientY: number, id: number, name: string) => void
/** Resolves relative marker icon path to absolute URL. If omitted, relative paths are used. */
resolveIconUrl?: (path: string) => string
/** Fallback icon URL when a marker image fails to load. */
fallbackIconUrl?: string
}
export interface MapLayersManager {
markers: UniqueList<MapMarker>
characters: UniqueList<MapCharacter>
changeMap: (id: number) => void
updateMarkers: (markersData: ApiMarker[]) => void
updateCharacters: (charactersData: ApiCharacter[]) => void
getQuestGivers: () => Array<{ id: number; name: string }>
getPlayers: () => Array<{ id: number; name: string }>
refreshMarkersVisibility: (hidden: boolean) => void
refreshOverlayMarkers: (overlayMapIdValue: number) => void
findMarkerById: (id: number) => MapMarker | undefined
findCharacterById: (id: number) => MapCharacter | undefined
}
export function createMapLayers(options: MapLayersOptions): MapLayersManager {
const {
L,
map,
markerLayer,
layer,
overlayLayer,
getCurrentMapId,
setCurrentMapId,
setSelectedMapId,
getAuths,
getTrackingCharacterId,
setTrackingCharacterId,
onMarkerContextMenu,
resolveIconUrl,
fallbackIconUrl,
} = options
const markers = createUniqueList<MapMarker>()
const characters = createUniqueList<MapCharacter>()
function markerCtx(): MapViewRef {
return { map, markerLayer, mapid: getCurrentMapId() }
}
function characterCtx(): CharacterMapViewRef {
return { map, mapid: getCurrentMapId() }
}
function changeMap(id: number) {
if (id === getCurrentMapId()) return
setCurrentMapId(id)
setSelectedMapId(id)
layer.map = id
layer.redraw()
overlayLayer.map = -1
overlayLayer.redraw()
const ctx = markerCtx()
uniqueListGetElements(markers).forEach((it) => it.remove(ctx))
uniqueListGetElements(markers)
.filter((it) => it.map === id)
.forEach((it) => it.add(ctx))
const cCtx = characterCtx()
uniqueListGetElements(characters).forEach((it) => {
it.remove(cCtx)
it.add(cCtx)
})
}
function updateMarkers(markersData: ApiMarker[]) {
const list = Array.isArray(markersData) ? markersData : []
const ctx = markerCtx()
const iconOptions =
resolveIconUrl != null
? { resolveIconUrl, fallbackIconUrl }
: undefined
uniqueListUpdate(
markers,
list.map((it) => createMarker(it as MarkerData, iconOptions, L)),
(marker: MapMarker) => {
if (marker.map === getCurrentMapId() || marker.map === overlayLayer.map) marker.add(ctx)
marker.setClickCallback(() => {
if (marker.leafletMarker) map.setView(marker.leafletMarker.getLatLng(), HnHMaxZoom)
})
marker.setContextMenu((mev: L.LeafletMouseEvent) => {
if (getAuths().includes('admin')) {
mev.originalEvent.preventDefault()
mev.originalEvent.stopPropagation()
onMarkerContextMenu(mev.originalEvent.clientX, mev.originalEvent.clientY, marker.id, marker.name)
}
})
},
(marker: MapMarker) => marker.remove(ctx),
(marker: MapMarker, updated: MapMarker) => marker.update(ctx, updated)
)
}
function updateCharacters(charactersData: ApiCharacter[]) {
const list = Array.isArray(charactersData) ? charactersData : []
const ctx = characterCtx()
uniqueListUpdate(
characters,
list.map((it) => createCharacter(it as CharacterData, L)),
(character: MapCharacter) => {
character.add(ctx)
character.setClickCallback(() => setTrackingCharacterId(character.id))
},
(character: MapCharacter) => character.remove(ctx),
(character: MapCharacter, updated: MapCharacter) => {
if (getTrackingCharacterId() === updated.id) {
if (getCurrentMapId() !== updated.map) changeMap(updated.map)
const latlng = map.unproject([updated.position.x, updated.position.y], HnHMaxZoom)
map.setView(latlng, HnHMaxZoom)
}
character.update(ctx, updated)
}
)
}
function getQuestGivers(): Array<{ id: number; name: string }> {
return uniqueListGetElements(markers)
.filter((it) => it.type === 'quest')
.map((it) => ({ id: it.id, name: it.name }))
}
function getPlayers(): Array<{ id: number; name: string }> {
return uniqueListGetElements(characters).map((it) => ({ id: it.id, name: it.name }))
}
function refreshMarkersVisibility(hidden: boolean) {
const ctx = markerCtx()
if (hidden) {
uniqueListGetElements(markers).forEach((it) => it.remove(ctx))
} else {
uniqueListGetElements(markers).forEach((it) => it.remove(ctx))
uniqueListGetElements(markers)
.filter((it) => it.map === getCurrentMapId() || it.map === overlayLayer.map)
.forEach((it) => it.add(ctx))
}
}
function refreshOverlayMarkers(overlayMapIdValue: number) {
overlayLayer.map = overlayMapIdValue
overlayLayer.redraw()
const ctx = markerCtx()
uniqueListGetElements(markers).forEach((it) => it.remove(ctx))
uniqueListGetElements(markers)
.filter((it) => it.map === getCurrentMapId() || it.map === overlayMapIdValue)
.forEach((it) => it.add(ctx))
}
function findMarkerById(id: number): MapMarker | undefined {
return uniqueListById(markers, id)
}
function findCharacterById(id: number): MapCharacter | undefined {
return uniqueListById(characters, id)
}
return {
markers,
characters,
changeMap,
updateMarkers,
updateCharacters,
getQuestGivers,
getPlayers,
refreshMarkersVisibility,
refreshOverlayMarkers,
findMarkerById,
findCharacterById,
}
}