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 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 characters: UniqueList 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() const characters = createUniqueList() 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) => { 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, } }