import type L from 'leaflet' import { HnHMaxZoom, ImageIcon, TileSize } from '~/lib/LeafletCustomTypes' export interface MarkerData { id: number position: { x: number; y: number } name: string image: string hidden: boolean map: number } export interface MapViewRef { map: L.Map mapid: number markerLayer: L.LayerGroup } export interface MapMarker { id: number position: { x: number; y: number } name: string image: string type: string text: string value: number hidden: boolean map: number leafletMarker: L.Marker | null remove: (mapview: MapViewRef) => void add: (mapview: MapViewRef) => void update: (mapview: MapViewRef, updated: MarkerData | MapMarker) => void jumpTo: (map: L.Map) => void setClickCallback: (callback: (e: L.LeafletMouseEvent) => void) => void setContextMenu: (callback: (e: L.LeafletMouseEvent) => void) => void } function detectType(name: string): string { if (name === 'gfx/invobjs/small/bush' || name === 'gfx/invobjs/small/bumling') return 'quest' if (name === 'custom') return 'custom' return name.substring('gfx/terobjs/mm/'.length) } export interface MarkerIconOptions { /** Resolves relative icon path to absolute URL (e.g. with app base path). */ resolveIconUrl: (path: string) => string /** Optional fallback URL when the icon image fails to load. */ fallbackIconUrl?: string } export type LeafletApi = typeof import('leaflet') export function createMarker( data: MarkerData, iconOptions: MarkerIconOptions | undefined, L: LeafletApi ): MapMarker { let leafletMarker: L.Marker | null = null let onClick: ((e: L.LeafletMouseEvent) => void) | null = null let onContext: ((e: L.LeafletMouseEvent) => void) | null = null const marker: MapMarker = { id: data.id, position: { ...data.position }, name: data.name, image: data.image, type: detectType(data.image), text: data.name, value: data.id, hidden: data.hidden, map: data.map, get leafletMarker() { return leafletMarker }, remove(_mapview: MapViewRef): void { if (leafletMarker) { leafletMarker.remove() leafletMarker = null } }, add(mapview: MapViewRef): void { if (!marker.hidden) { const resolve = iconOptions?.resolveIconUrl ?? ((path: string) => path) const fallback = iconOptions?.fallbackIconUrl let icon: L.Icon if (marker.image === 'gfx/terobjs/mm/custom') { icon = new ImageIcon({ iconUrl: resolve('gfx/terobjs/mm/custom.png'), iconSize: [21, 23], iconAnchor: [11, 21], popupAnchor: [1, 3], tooltipAnchor: [1, 3], fallbackIconUrl: fallback, }) } else { icon = new ImageIcon({ iconUrl: resolve(`${marker.image}.png`), iconSize: [32, 32], fallbackIconUrl: fallback, }) } const position = mapview.map.unproject([marker.position.x, marker.position.y], HnHMaxZoom) leafletMarker = L.marker(position, { icon }) const gridX = Math.floor(marker.position.x / TileSize) const gridY = Math.floor(marker.position.y / TileSize) const tooltipContent = `${marker.name} · ${gridX}, ${gridY}` leafletMarker.bindTooltip(tooltipContent, { direction: 'top', permanent: false, offset: L.point(0, -14), }) leafletMarker.addTo(mapview.markerLayer) const markerEl = (leafletMarker as unknown as { getElement?: () => HTMLElement }).getElement?.() if (markerEl) markerEl.setAttribute('aria-label', marker.name) leafletMarker.on('click', (e: L.LeafletMouseEvent) => { if (onClick) onClick(e) }) leafletMarker.on('contextmenu', (e: L.LeafletMouseEvent) => { if (onContext) onContext(e) }) } }, update(mapview: MapViewRef, updated: MarkerData | MapMarker): void { marker.position = { ...updated.position } marker.name = updated.name marker.hidden = updated.hidden marker.map = updated.map if (leafletMarker) { const position = mapview.map.unproject([updated.position.x, updated.position.y], HnHMaxZoom) leafletMarker.setLatLng(position) const gridX = Math.floor(updated.position.x / TileSize) const gridY = Math.floor(updated.position.y / TileSize) leafletMarker.setTooltipContent(`${marker.name} · ${gridX}, ${gridY}`) } }, jumpTo(map: L.Map): void { if (leafletMarker) { const position = map.unproject([marker.position.x, marker.position.y], HnHMaxZoom) leafletMarker.setLatLng(position) } }, setClickCallback(callback: (e: L.LeafletMouseEvent) => void): void { onClick = callback }, setContextMenu(callback: (e: L.LeafletMouseEvent) => void): void { onContext = callback }, } return marker }