import L, { Bounds, LatLng, Point } from 'leaflet' export const TileSize = 100 export const HnHMaxZoom = 6 export const HnHMinZoom = 1 export const HnHDefaultZoom = 6 /** When scaleFactor exceeds this, render one label per tile instead of a full grid (avoids 100k+ DOM nodes at zoom 1). */ const GRID_COORD_SCALE_FACTOR_THRESHOLD = 8 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 } const bottomRight = { x: topLeft.x + scaleFactor - 1, y: topLeft.y + scaleFactor - 1 } if (scaleFactor > GRID_COORD_SCALE_FACTOR_THRESHOLD) { // Low zoom: one label per tile to avoid hundreds of thousands of DOM nodes (Reset view freeze fix) 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 } for (let gx = topLeft.x; gx <= bottomRight.x; gx++) { for (let gy = topLeft.y; gy <= bottomRight.y; gy++) { const leftPx = ((gx - topLeft.x) / scaleFactor) * TileSize const topPx = ((gy - topLeft.y) / scaleFactor) * TileSize const textElement = document.createElement('div') textElement.classList.add('map-tile-text') textElement.textContent = `(${gx}, ${gy})` textElement.style.position = 'absolute' textElement.style.left = leftPx + 2 + 'px' textElement.style.top = topPx + 2 + 'px' if (scaleFactor > 1) { textElement.style.fontSize = Math.max(8, 12 - Math.log2(scaleFactor) * 2) + 'px' } element.appendChild(textElement) } } return element }, }) as unknown as new (options?: L.GridLayerOptions) => L.GridLayer export const ImageIcon = L.Icon.extend({ options: { iconSize: [32, 32], iconAnchor: [16, 16], }, }) as unknown as new (options?: L.IconOptions) => 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