All files / core/systems/georef georef.ts

95% Statements 38/40
92% Branches 23/25
100% Functions 5/5
100% Lines 37/37

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                    15x 15x 15x 15x 15x                       2847x       2847x       2847x 2847x   2847x       2847x 2838x 5676x 2838x         288x             15x     2014x         1782x 1782x 1782x 1782x   1782x       1782x     1782x 1782x 1782x 1782x 1781x 1781x 1781x 1781x 1x 1780x 1780x 1780x   1781x 1781x    
/**
 * World Geographic Reference System (GEOREF), the aviation grid.
 *
 * 15° quadrangle: longitude letter (A-Z without I and O, from 180°W) then latitude letter
 *                 (A-M without I, from 90°S)
 * 1° square:      longitude letter then latitude letter (A-Q without I and O)
 * minutes:        two longitude digits then two latitude digits (00-59)
 */
import type { LatLon } from '../../geo/ellipsoid'
 
const QUADRANGLE_LON = 'ABCDEFGHJKLMNPQRSTUVWXYZ'
const QUADRANGLE_LAT = 'ABCDEFGHJKLM'
const DEGREE_LETTERS = 'ABCDEFGHJKLMNPQ'
const QUADRANGLE_DEGREES = 15
const MINUTES_PER_DEGREE = 60
 
export interface GeorefCell {
  west: number
  south: number
  east: number
  north: number
}
 
/** The GEOREF for a point, to whole degrees (4 letters) or to whole minutes (8 characters). */
export function toGeoref({ lat, lon }: LatLon, withMinutes = true): string {
  // Work in whole minutes so cell edges do not drift.
  const lonMinutes = Math.min(
    360 * MINUTES_PER_DEGREE - 1,
    Math.max(0, Math.floor((lon + 180) * MINUTES_PER_DEGREE + 1e-9)),
  )
  const latMinutes = Math.min(
    180 * MINUTES_PER_DEGREE - 1,
    Math.max(0, Math.floor((lat + 90) * MINUTES_PER_DEGREE + 1e-9)),
  )
  const lonDegrees = Math.floor(lonMinutes / MINUTES_PER_DEGREE)
  const latDegrees = Math.floor(latMinutes / MINUTES_PER_DEGREE)
  const letters =
    QUADRANGLE_LON.charAt(Math.floor(lonDegrees / QUADRANGLE_DEGREES)) +
    QUADRANGLE_LAT.charAt(Math.floor(latDegrees / QUADRANGLE_DEGREES)) +
    DEGREE_LETTERS.charAt(lonDegrees % QUADRANGLE_DEGREES) +
    DEGREE_LETTERS.charAt(latDegrees % QUADRANGLE_DEGREES)
  if (!withMinutes) return letters
  const pad = (minutes: number) =>
    String(minutes % MINUTES_PER_DEGREE).padStart(2, '0')
  return letters + pad(lonMinutes) + pad(latMinutes)
}
 
/** The 15° quadrangle letters for the quadrangle whose south-west corner is given. */
export function quadrangleLabel(west: number, south: number): string {
  return (
    QUADRANGLE_LON.charAt(Math.floor((west + 180) / QUADRANGLE_DEGREES)) +
    QUADRANGLE_LAT.charAt(Math.floor((south + 90) / QUADRANGLE_DEGREES))
  )
}
 
const GEOREF_RE =
  /^([A-HJ-NP-Z])([A-HJ-M])([A-HJ-NP-Q])([A-HJ-NP-Q])(?:(\d{2})(\d{2})|(\d{3})(\d{3}))?$/
 
export function isGeoref(text: string): boolean {
  return GEOREF_RE.test(text.replace(/\s+/g, '').toUpperCase())
}
 
/** The cell a GEOREF names: 1° for letters only, 1′ or 0.1′ with digits. */
export function fromGeoref(input: string): GeorefCell {
  const code = input.replace(/\s+/g, '').toUpperCase()
  const m = GEOREF_RE.exec(code)
  Iif (!m) throw new Error(`Invalid GEOREF "${input}"`)
  const [, lonQuad = '', latQuad = '', lonDeg = '', latDeg = ''] = m
  let west =
    -180 +
    QUADRANGLE_LON.indexOf(lonQuad) * QUADRANGLE_DEGREES +
    DEGREE_LETTERS.indexOf(lonDeg)
  let south =
    -90 +
    QUADRANGLE_LAT.indexOf(latQuad) * QUADRANGLE_DEGREES +
    DEGREE_LETTERS.indexOf(latDeg)
  let size = 1
  const lonDigits = m[5] ?? m[7]
  const latDigits = m[6] ?? m[8]
  if (lonDigits !== undefined && latDigits !== undefined) {
    const divisor = lonDigits.length === 2 ? 1 : 10
    const lonMinutes = Number(lonDigits) / divisor
    const latMinutes = Number(latDigits) / divisor
    if (lonMinutes >= MINUTES_PER_DEGREE || latMinutes >= MINUTES_PER_DEGREE)
      throw new Error('GEOREF minutes must be under 60')
    west += lonMinutes / MINUTES_PER_DEGREE
    south += latMinutes / MINUTES_PER_DEGREE
    size = 1 / MINUTES_PER_DEGREE / divisor
  }
  Iif (south >= 90) throw new Error('GEOREF latitude is past the pole')
  return { west, south, east: west + size, north: south + size }
}