import type L from 'leaflet' import { getColorForCharacterId, type CharacterColors } from '~/lib/characterColors' import { HnHMaxZoom } from '~/lib/LeafletCustomTypes' export type LeafletApi = typeof import('leaflet') function buildCharacterIconUrl(colors: CharacterColors): string { const svg = '' + `` + '' + '' return 'data:image/svg+xml,' + encodeURIComponent(svg) } export function createCharacterIcon(L: LeafletApi, colors: CharacterColors): L.Icon { return new L.Icon({ iconUrl: buildCharacterIconUrl(colors), iconSize: [24, 32], iconAnchor: [12, 32], popupAnchor: [0, -32], }) } export interface CharacterData { name: string position: { x: number; y: number } type: string id: number map: number /** True when this character was last updated by one of the current user's tokens. */ ownedByMe?: boolean } export interface CharacterMapViewRef { map: L.Map mapid: number markerLayer?: L.LayerGroup } export interface MapCharacter { id: number name: string position: { x: number; y: number } type: string map: number text: string value: number ownedByMe?: boolean leafletMarker: L.Marker | null remove: (mapview: CharacterMapViewRef) => void add: (mapview: CharacterMapViewRef) => void update: (mapview: CharacterMapViewRef, updated: CharacterData | MapCharacter) => void setClickCallback: (callback: (e: L.LeafletMouseEvent) => void) => void } export function createCharacter(data: CharacterData, L: LeafletApi): MapCharacter { let leafletMarker: L.Marker | null = null let onClick: ((e: L.LeafletMouseEvent) => void) | null = null let ownedByMe = data.ownedByMe ?? false const colors = getColorForCharacterId(data.id, { ownedByMe }) let characterIcon = createCharacterIcon(L, colors) const character: MapCharacter = { id: data.id, name: data.name, position: { ...data.position }, type: data.type, map: data.map, text: data.name, value: data.id, get ownedByMe() { return ownedByMe }, set ownedByMe(v: boolean | undefined) { ownedByMe = v ?? false }, get leafletMarker() { return leafletMarker }, remove(mapview: CharacterMapViewRef): void { if (leafletMarker) { const layer = mapview.markerLayer ?? mapview.map layer.removeLayer(leafletMarker) leafletMarker = null } }, add(mapview: CharacterMapViewRef): void { if (character.map === mapview.mapid) { const position = mapview.map.unproject([character.position.x, character.position.y], HnHMaxZoom) leafletMarker = L.marker(position, { icon: characterIcon, title: character.name }) leafletMarker.on('click', (e: L.LeafletMouseEvent) => { if (onClick) onClick(e) }) const targetLayer = mapview.markerLayer ?? mapview.map leafletMarker.addTo(targetLayer) } }, update(mapview: CharacterMapViewRef, updated: CharacterData | MapCharacter): void { const updatedOwnedByMe = (updated as { ownedByMe?: boolean }).ownedByMe ?? false if (ownedByMe !== updatedOwnedByMe) { ownedByMe = updatedOwnedByMe characterIcon = createCharacterIcon(L, getColorForCharacterId(character.id, { ownedByMe })) if (leafletMarker) leafletMarker.setIcon(characterIcon) } if (character.map !== updated.map) { character.remove(mapview) } character.map = updated.map character.position = { ...updated.position } if (!leafletMarker && character.map === mapview.mapid) { character.add(mapview) } if (leafletMarker) { const position = mapview.map.unproject([updated.position.x, updated.position.y], HnHMaxZoom) leafletMarker.setLatLng(position) } }, setClickCallback(callback: (e: L.LeafletMouseEvent) => void): void { onClick = callback }, } return character }