- 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.
93 lines
3.3 KiB
TypeScript
93 lines
3.3 KiB
TypeScript
import L, { Bounds, LatLng, Point } from 'leaflet'
|
|
|
|
export const TileSize = 100
|
|
export const HnHMaxZoom = 6
|
|
export const HnHMinZoom = 1
|
|
export const HnHDefaultZoom = 6
|
|
|
|
export interface GridCoordLayerOptions extends L.GridLayerOptions {
|
|
visible?: boolean
|
|
}
|
|
|
|
/**
|
|
* Grid layer that draws one coordinate label per Leaflet tile in the top-left corner.
|
|
* coords.(x,y,z) are Leaflet tile indices and zoom; they map to game tiles as:
|
|
* scaleFactor = 2^(HnHMaxZoom - coords.z),
|
|
* topLeft = (coords.x * scaleFactor, coords.y * scaleFactor).
|
|
* This matches backend tile URL {mapid}/{z}/{x}_{y}.png (storageZ: z=6→0, Coord = tile index).
|
|
*/
|
|
export const GridCoordLayer = L.GridLayer.extend({
|
|
options: {
|
|
visible: true,
|
|
},
|
|
createTile(coords: { x: number; y: number; z: number }) {
|
|
if (!this.options.visible) {
|
|
const element = document.createElement('div')
|
|
element.style.width = TileSize + 'px'
|
|
element.style.height = TileSize + 'px'
|
|
element.classList.add('map-tile')
|
|
return element
|
|
}
|
|
const element = document.createElement('div')
|
|
element.style.width = TileSize + 'px'
|
|
element.style.height = TileSize + 'px'
|
|
element.style.position = 'relative'
|
|
element.classList.add('map-tile')
|
|
|
|
const scaleFactor = Math.pow(2, HnHMaxZoom - coords.z)
|
|
const topLeft = { x: coords.x * scaleFactor, y: coords.y * scaleFactor }
|
|
|
|
// One label per Leaflet tile at top-left (2px, 2px); same (x,y) as backend tile for this coords.
|
|
const textElement = document.createElement('div')
|
|
textElement.classList.add('map-tile-text')
|
|
textElement.textContent = `(${topLeft.x}, ${topLeft.y})`
|
|
textElement.style.position = 'absolute'
|
|
textElement.style.left = '2px'
|
|
textElement.style.top = '2px'
|
|
textElement.style.fontSize = Math.max(8, 12 - Math.log2(scaleFactor) * 2) + 'px'
|
|
element.appendChild(textElement)
|
|
return element
|
|
},
|
|
}) as unknown as new (options?: GridCoordLayerOptions) => L.GridLayer
|
|
|
|
export interface ImageIconOptions extends L.IconOptions {
|
|
/** When the main icon image fails to load, use this URL (e.g. data URL or default marker). */
|
|
fallbackIconUrl?: string
|
|
}
|
|
|
|
export const ImageIcon = L.Icon.extend({
|
|
options: {
|
|
iconSize: [32, 32],
|
|
iconAnchor: [16, 16],
|
|
} as ImageIconOptions,
|
|
|
|
createIcon(oldIcon?: HTMLElement): HTMLElement {
|
|
const img = L.Icon.prototype.createIcon.call(this, oldIcon) as HTMLImageElement
|
|
const fallback = (this.options as ImageIconOptions).fallbackIconUrl
|
|
if (fallback && img && img.tagName === 'IMG') {
|
|
img.onerror = () => {
|
|
img.onerror = null
|
|
img.src = fallback
|
|
}
|
|
}
|
|
return img
|
|
},
|
|
}) as unknown as new (options?: ImageIconOptions) => L.Icon
|
|
|
|
const latNormalization = (90.0 * TileSize) / 2500000.0
|
|
const lngNormalization = (180.0 * TileSize) / 2500000.0
|
|
|
|
const HnHProjection = {
|
|
project(latlng: LatLng) {
|
|
return new Point(latlng.lat / latNormalization, latlng.lng / lngNormalization)
|
|
},
|
|
unproject(point: Point) {
|
|
return new LatLng(point.x * latNormalization, point.y * lngNormalization)
|
|
},
|
|
bounds: (() => new Bounds([-latNormalization, -lngNormalization], [latNormalization, lngNormalization]))(),
|
|
}
|
|
|
|
export const HnHCRS = L.extend({}, L.CRS.Simple, {
|
|
projection: HnHProjection,
|
|
}) as L.CRS
|