169 lines
5.8 KiB
TypeScript
169 lines
5.8 KiB
TypeScript
// 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<string>,
|
|
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: `<div style="font-size:${fs}px;font-weight:${sub ? "normal" : "bold"};color:#fff;background:rgba(0,0,0,${sub ? "0.35" : "0.5"});padding:1px 4px;border-radius:3px;white-space:nowrap;width:fit-content;">${text}</div>`,
|
|
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<string>()
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|