// Maidenhead grid square overlay shared between the main propagation map // and the rover planner map. Draws red field/square rectangles with labels. interface GridBounds { west: number east: number south: number north: number center: { lat: number; lon: number } } export class GridSquare { lat: number lon: number constructor(lat: number, lon: number) { this.lat = lat this.lon = ((lon % 360) + 540) % 360 - 180 } encode(precision = 4): string { const adjLon = this.lon + 180 const adjLat = this.lat + 90 let g = "" g += String.fromCharCode(65 + Math.floor(adjLon / 20)) g += String.fromCharCode(65 + Math.floor(adjLat / 10)) g += Math.floor((adjLon % 20) / 2) g += Math.floor(adjLat % 10) if (precision > 4) { g += String.fromCharCode(97 + Math.floor(((adjLon % 2) * 60) / 5)) g += String.fromCharCode(97 + Math.floor(((adjLat % 1) * 60) / 2.5)) } return g } static decode(grid: string): GridBounds { const f1 = grid.charCodeAt(0) - 65 const f2 = grid.charCodeAt(1) - 65 let west = f1 * 20 - 180 let south = f2 * 10 - 90 let w = 20, h = 10 if (grid.length >= 4) { west += parseInt(grid[2]) * 2 south += parseInt(grid[3]) w = 2; h = 1 } if (grid.length >= 6) { west += (grid.charCodeAt(4) - 97) * 5 / 60 south += (grid.charCodeAt(5) - 97) * 2.5 / 60 w = 5 / 60; h = 2.5 / 60 } return { west, east: west + w, south, north: south + h, center: { lat: south + h / 2, lon: west + w / 2 } } } } function drawGridSquare( grid: string, precision: number, bounds: L.LatLngBounds, layerGroup: L.LayerGroup, drawnSet: Set, map: L.Map ): void { if (grid.length < 2 || drawnSet.has(grid)) return drawnSet.add(grid) const c = GridSquare.decode(grid) if (c.west > bounds.getEast() || c.east < bounds.getWest() || c.south > bounds.getNorth() || c.north < bounds.getSouth()) return const field = precision <= 2 const sub = precision >= 6 const weight = field ? 2.5 : sub ? 1 : 2 const opacity = sub ? 0.5 : 0.8 layerGroup.addLayer(L.rectangle( [[c.south, c.west], [c.north, c.east]], { color: "#E63946", weight, opacity, fillOpacity: 0, interactive: false } )) const sw = map.latLngToContainerPoint([c.south, c.west]) const ne = map.latLngToContainerPoint([c.north, c.east]) const px = Math.abs(ne.x - sw.x) const minW = sub ? 25 : field ? 30 : 40 if (px > minW) { // Show the full grid label at every level (e.g. "EM13sf" for a subsquare) const text = field ? grid.substring(0, 2) : sub ? grid.substring(0, 6) : grid.substring(0, 4) const len = text.length const fs = Math.max(9, Math.min(sub ? 12 : 14, Math.round(px / (len * 1.2)))) layerGroup.addLayer(L.marker([c.center.lat, c.center.lon], { interactive: false, icon: L.divIcon({ className: "grid-label", html: `
${text}
`, iconSize: [Math.round(len * fs * 0.8), Math.round(fs * 1.5)], iconAnchor: [Math.round(len * fs * 0.4), Math.round(fs * 0.75)] }) })) } } export function updateGridOverlay(map: L.Map, gridLayer: L.LayerGroup): void { gridLayer.clearLayers() const drawn = new Set() const bounds = map.getBounds() const zoom = map.getZoom() const showSquares = zoom >= 7 const showSubsquares = zoom >= 11 const south = Math.max(bounds.getSouth(), -90) const north = Math.min(bounds.getNorth(), 90) if (!showSquares) { const fW = Math.floor((bounds.getWest() + 180) / 20) * 20 - 180 const fE = Math.ceil((bounds.getEast() + 180) / 20) * 20 - 180 const fS = Math.floor((south + 90) / 10) * 10 - 90 const fN = Math.ceil((north + 90) / 10) * 10 - 90 for (let lon = fW; lon < fE; lon += 20) { for (let lat = fS; lat < fN; lat += 10) { const nLon = ((lon % 360) + 540) % 360 - 180 const g = new GridSquare(lat + 5, nLon + 10).encode(4).substring(0, 2) drawGridSquare(g, 2, bounds, gridLayer, drawn, map) } } return } const wE = Math.floor((bounds.getWest() + 180) / 2) * 2 - 180 const eE = Math.ceil((bounds.getEast() + 180) / 2) * 2 - 180 const sE = Math.floor(south + 90) - 90 const nE = Math.ceil(north + 90) - 90 if (Math.ceil((eE - wE) / 2) * Math.ceil(nE - sE) > 500) return for (let lon = wE; lon < eE; lon += 2) { for (let lat = sE; lat < nE; lat += 1) { const nLon = ((lon % 360) + 540) % 360 - 180 if (showSubsquares) { // Draw the 4-char square boundary first const sq = new GridSquare(lat + 0.5, nLon + 1).encode(4) drawGridSquare(sq.substring(0, 4), 4, bounds, gridLayer, drawn, map) // Then draw 6-char subsquares (24 x 24 = 576 per square, 5' x 2.5') const subW = 5 / 60 const subH = 2.5 / 60 const startI = Math.max(0, Math.floor((bounds.getWest() - lon) / subW)) const endI = Math.min(24, Math.ceil((bounds.getEast() - lon) / subW)) const startJ = Math.max(0, Math.floor((bounds.getSouth() - lat) / subH)) const endJ = Math.min(24, Math.ceil((bounds.getNorth() - lat) / subH)) for (let i = startI; i < endI; i++) { for (let j = startJ; j < endJ; j++) { const cLon = nLon + i * subW + subW / 2 const cLat = lat + j * subH + subH / 2 const sub = new GridSquare(cLat, cLon).encode(6) drawGridSquare(sub, 6, bounds, gridLayer, drawn, map) } } } else { const g = new GridSquare(lat + 0.5, nLon + 1).encode(4) drawGridSquare(g.substring(0, 4), 4, bounds, gridLayer, drawn, map) } } } }