Enhance API documentation and improve marker functionality
- Updated API documentation for clarity and consistency, including detailed descriptions of authentication and user account endpoints. - Added a new cave marker image to enhance visual representation in the frontend. - Implemented normalization for cave marker images during upload to ensure consistent storage format. - Expanded test coverage for client services, including new tests for marker uploads and image normalization.
This commit is contained in:
@@ -1,159 +1,165 @@
|
||||
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 = L
|
||||
|
||||
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
|
||||
}
|
||||
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 = L
|
||||
|
||||
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
|
||||
const iconUrl =
|
||||
marker.name === 'Cave' && marker.image === 'gfx/terobjs/mm/custom'
|
||||
? resolve('gfx/terobjs/mm/cave.png')
|
||||
: marker.image === 'gfx/terobjs/mm/custom'
|
||||
? resolve('gfx/terobjs/mm/custom.png')
|
||||
: resolve(`${marker.image}.png`)
|
||||
let icon: L.Icon
|
||||
if (marker.image === 'gfx/terobjs/mm/custom' && marker.name !== 'Cave') {
|
||||
icon = new ImageIcon({
|
||||
iconUrl,
|
||||
iconSize: [21, 23],
|
||||
iconAnchor: [11, 21],
|
||||
popupAnchor: [1, 3],
|
||||
tooltipAnchor: [1, 3],
|
||||
fallbackIconUrl: fallback,
|
||||
})
|
||||
} else {
|
||||
icon = new ImageIcon({
|
||||
iconUrl,
|
||||
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
|
||||
}
|
||||
|
||||
BIN
frontend-nuxt/public/gfx/terobjs/mm/cave.png
Normal file
BIN
frontend-nuxt/public/gfx/terobjs/mm/cave.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
Reference in New Issue
Block a user