Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 | 30x 10x 4x 3x 1016x 1016x 102524x 102524x 1016x 1660x 49800x 49800x 25854x 1660x 1016x 1016x 1016x 1016x 51064x 51064x 51064x 51064x 830x 830x 830x 830x 51064x 830x 830x 830x 830x 51064x 51064x 51064x 1016x 1016x 15x 15x 104x 104x 104x 197x 197x 197x 197x 197x 102x 102x 102x 95x 95x 2x 51x 51x 30x 30x 30x 30x 10x 10x 10x 10x 150x 10x 830x 755x 755x 75x 75x 150x 150x 104x 104x 104x 150x 51x 10x 10x 12010x 11970x 11970x 32x 32x 12010x 12010x 12010x 12010x 12010x 1160x 1160x 1160x 1160x 1160x 1160x 32x 32x 32x 32x 32x 32x 32x 32x 24x 1016x 24x 408x 408x 38706x 312x 408x 608x 608x 63818x 518x 608x 2x 2x 38x 1014x 616x 1232x 1232x 1232x 24x 12010x 18x 18x 166x 2388x 2388x 1580x 1580x 1580x 1568x 1568x 1568x 1568x 1580x 10x 7x 7x 7x 6x 6x 6x 6x 6x 6x 6x 6x 6x 20x 20x 20x 160x 160x 5x 5x 1x | /** MGRS grid lines and 100 km square labels, clipped to each grid zone. */
import type { LatLon } from '../../geo/ellipsoid'
import {
type Box,
type GridGeometry,
type GridLabel,
type GridPath,
type Position,
boxPath,
inBox,
intersectBoxes,
} from '../../geo/geometry'
import { GZD_CELLS, cellBox } from '../utm/gzd'
import { type Hemisphere, fromUtm, toUtm } from '../utm/utm'
import { fromMgrs, utmSquareId } from './mgrs'
/** MGRS grid spacing in metres for a zoom level, or 0 for none. */
export function mgrsSpacing(zoom: number): number {
if (zoom < 5) return 0
if (zoom < 8.5) return 100_000
if (zoom < 11.5) return 10_000
Eif (zoom < 14.5) return 1_000
return 100
}
/**
* Trace a straight grid line in UTM space and clip it to `clip` in geographic space.
* Boundary crossings are found by bisection so lines end on the zone edge.
*/
function traceClipped(
zone: number,
hemi: Hemisphere,
at: (t: number) => [number, number],
steps: number,
clip: Box,
): Position[][] {
const runs: Position[][] = []
const geo = (t: number) => {
const [e, n] = at(t)
return fromUtm(zone, hemi, e, n)
}
const edge = (tIn: number, tOut: number): LatLon => {
for (let i = 0; i < 30; i++) {
const mid = (tIn + tOut) / 2
if (inBox(geo(mid), clip)) tIn = mid
else tOut = mid
}
return geo(tIn)
}
let run: Position[] | null = null
let prevT = 0
let prevIn = false
for (let i = 0; i <= steps; i++) {
const t = i / steps
const p = geo(t)
const inside = inBox(p, clip)
if (inside && !prevIn) {
run = []
Eif (i > 0) {
const q = edge(t, prevT)
run.push([q.lon, q.lat])
}
}
if (!inside && prevIn && run) {
const q = edge(prevT, t)
run.push([q.lon, q.lat])
runs.push(run)
run = null
}
if (inside) run!.push([p.lon, p.lat])
prevIn = inside
prevT = t
}
Iif (run && run.length > 1) runs.push(run)
return runs
}
/** Full grid lines, or only ticks at the edges of the view and crosses at the intersections. */
export type MgrsGridStyle = 'Lines' | 'Ticks'
/** Tick length as a fraction of the view height. */
const TICK_FRACTION = 0.02
/** Cross arm length as a fraction of the grid spacing. */
const CROSS_FRACTION = 0.06
/** The first `length` of a path from its start, measured in degrees of latitude on the ground. */
function headOf(path: ReadonlyArray<Position>, length: number): Position[] {
const head: Position[] = [path[0]!]
let remaining = length
for (let i = 1; i < path.length; i++) {
const [lonA, latA] = path[i - 1]!
const [lonB, latB] = path[i]!
const cosLat = Math.cos((((latA + latB) / 2) * Math.PI) / 180)
const step = Math.hypot((lonB - lonA) * cosLat, latB - latA)
if (step >= remaining) {
const k = step === 0 ? 0 : remaining / step
head.push([lonA + (lonB - lonA) * k, latA + (latB - latA) * k])
return head
}
head.push([lonB, latB])
remaining -= step
}
return head
}
/** The digits a grid value shows at the given spacing, as in a square label: 620000 at 1 km is "20". */
function gridDigits(value: number, spacing: number): string {
const digits = Math.round(Math.log10(100_000 / spacing))
return String(Math.floor((value % 100_000) / spacing)).padStart(digits, '0')
}
export function mgrsGrid(
view: Box,
zoom: number,
maxLines = 600,
style: MgrsGridStyle = 'Lines',
): GridGeometry {
const spacing = mgrsSpacing(zoom)
const paths: GridPath[] = []
const labels: GridLabel[] = []
if (!spacing) return { paths, labels }
const [viewW, viewS, viewE, viewN] = view
const tickLength = (viewN - viewS) * TICK_FRACTION
const edgeTolerance = Math.max(viewE - viewW, viewN - viewS) * 1e-6
const isOnEdge = ([lon, lat]: Position): boolean =>
Math.abs(lon - viewW) < edgeTolerance ||
Math.abs(lon - viewE) < edgeTolerance ||
Math.abs(lat - viewS) < edgeTolerance ||
Math.abs(lat - viewN) < edgeTolerance
/**
* Add one traced grid line. In tick style only its ends on the view edge remain, and the
* south and west ends carry the line's digits, like the margin of a chart.
*/
const emit = (
path: Position[],
width: number,
axis: 'easting' | 'northing',
value: number,
) => {
if (style === 'Lines') {
paths.push({ path, width })
return
}
const ends = [path, [...path].reverse()]
for (const end of ends) {
const start = end[0]!
if (!isOnEdge(start)) continue
const tick = headOf(end, tickLength)
paths.push({ path: tick, width })
const isLabelledEdge =
axis === 'easting'
? Math.abs(start[1] - viewS) < edgeTolerance
: Math.abs(start[0] - viewW) < edgeTolerance
if (isLabelledEdge && spacing < 100_000) {
labels.push({
position: tick[tick.length - 1]!,
text: gridDigits(value, spacing),
size: 11,
})
}
}
}
let lineCount = 0
for (const c of GZD_CELLS) {
if (c.zone === null) continue
const clip = intersectBoxes(view, cellBox(c))
if (!clip) continue
const zone = c.zone
const hemi: Hemisphere = c.band >= 'N' ? 'N' : 'S'
// UTM extent of the clipped area, from its densified outline.
let minE = Infinity
let maxE = -Infinity
let minN = Infinity
let maxN = -Infinity
for (const [lon, lat] of boxPath(clip, (clip[2] - clip[0]) / 8)) {
// Keep samples on the cell's side of the equator so northings share one false origin.
const cellLat =
hemi === 'S' ? Math.min(lat, -1e-9) : Math.min(lat, 83.9999999)
const u = toUtm({ lat: cellLat, lon: Math.min(lon, 179.9999999) }, zone)
minE = Math.min(minE, u.easting)
maxE = Math.max(maxE, u.easting)
minN = Math.min(minN, u.northing)
maxN = Math.max(maxN, u.northing)
}
const pad = spacing
const e0 = Math.floor((minE - pad) / spacing) * spacing
const n0 = Math.floor((minN - pad) / spacing) * spacing
const e1 = Math.ceil((maxE + pad) / spacing) * spacing
const n1 = Math.ceil((maxN + pad) / spacing) * spacing
const ne = (e1 - e0) / spacing
const nn = (n1 - n0) / spacing
if (lineCount + ne + nn > maxLines) continue
lineCount += ne + nn
const steps = (count: number) => Math.min(64, Math.max(8, count * 2))
for (let e = e0; e <= e1; e += spacing) {
const major = e % 100_000 === 0
for (const path of traceClipped(
zone,
hemi,
t => [e, n0 + (n1 - n0) * t],
steps(nn),
clip,
)) {
emit(path, major ? 1.5 : 0.75, 'easting', e)
}
}
for (let n = n0; n <= n1; n += spacing) {
const major = n % 100_000 === 0
for (const path of traceClipped(
zone,
hemi,
t => [e0 + (e1 - e0) * t, n],
steps(ne),
clip,
)) {
emit(path, major ? 1.5 : 0.75, 'northing', n)
}
}
// Tick style marks each grid intersection in view with a small cross.
if (style === 'Ticks' && ne * nn < 1000) {
const arm = spacing * CROSS_FRACTION
for (let e = e0; e <= e1; e += spacing) {
for (let n = n0; n <= n1; n += spacing) {
if (!inBox(fromUtm(zone, hemi, e, n), clip)) continue
for (const [[ea, na], [eb, nb]] of [
[
[e - arm, n],
[e + arm, n],
],
[
[e, n - arm],
[e, n + arm],
],
] as const) {
const a = fromUtm(zone, hemi, ea, na)
const b = fromUtm(zone, hemi, eb, nb)
paths.push({
path: [
[a.lon, a.lat],
[b.lon, b.lat],
],
width: 1,
})
}
}
}
}
// Label 100 km squares at their centres, and finer squares with their digits. Tick style
// labels finer squares on the edges instead.
const isSquareLabelled =
spacing === 100_000 || (style === 'Lines' && ne * nn < 1000)
if (isSquareLabelled) {
const size = spacing
for (let e = e0; e < e1; e += size) {
for (let n = n0; n < n1; n += size) {
const p = fromUtm(zone, hemi, e + size / 2, n + size / 2)
if (!inBox(p, clip)) continue
const sq = utmSquareId(zone, e, n)
let text = sq
if (size < 100_000) {
const digits = Math.round(Math.log10(100_000 / size))
const de = String(Math.floor((e % 100_000) / size)).padStart(
digits,
'0',
)
const dn = String(Math.floor((n % 100_000) / size)).padStart(
digits,
'0',
)
text = `${sq} ${de} ${dn}`
}
labels.push({ position: [p.lon, p.lat], text, size: 12 })
}
}
}
}
return { paths, labels }
}
/** Outline of the MGRS square that contains `mgrs` (any precision). */
export function mgrsSquareOutline(mgrs: string): Position[] | null {
try {
const compact = mgrs.replace(/\s+/g, '').toUpperCase()
if (/^[ABYZ]/.test(compact)) return null
const { sw, size } = fromMgrs(compact)
const zone = parseInt(compact, 10)
const u = toUtm(sw, zone)
const hemi = u.hemisphere
// Snap the south-west corner to the square grid, then walk the edges in UTM space.
const e0 = Math.round(u.easting / size) * size
const n0 = Math.round(u.northing / size) * size
const corners: [number, number][] = [
[e0, n0],
[e0 + size, n0],
[e0 + size, n0 + size],
[e0, n0 + size],
[e0, n0],
]
const out: Position[] = []
for (let i = 0; i < 4; i++) {
const [ea, na] = corners[i]!
const [eb, nb] = corners[i + 1]!
for (let k = 0; k < 8; k++) {
const p = fromUtm(
zone,
hemi,
ea + ((eb - ea) * k) / 8,
na + ((nb - na) * k) / 8,
)
out.push([p.lon, p.lat])
}
}
out.push(out[0]!)
return out
} catch {
return null
}
}
|