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 | 4118x 4118x 1x 4122x 4122x 4122x 4122x 4122x 329x 329x 4122x 205x 205x 4122x 8386x 8386x 4211x 4122x 4122x 4122x 4122x 4122x 3x 4120x 4120x 4120x 4120x 189x 189x 4120x 4120x 4120x 2x 16x 6156x 6156x 6156x 6156x 6156x 4138x 4138x 6156x 6066x 6156x 6063x 525x 525x 6063x 6063x 6062x 6156x 5026x 5026x 5026x 2069x 2069x 2957x 1008x 1008x 1949x 1949x 5026x 3078x 3078x 3078x 3015x 3015x 3014x 3014x | /** Latitude and longitude as decimal degrees, degrees-minutes-seconds and degrees-decimal-minutes. */
import { type LatLon, isValidLatLon } from '../../geo/ellipsoid'
export type AngleKind = 'DD' | 'DMS' | 'DDM'
/** A value to fixed decimals, without the minus sign when it rounds to zero ("-0.000000"). */
export function toFixedUnsignedZero(value: number, digits: number): string {
const rounded = value.toFixed(digits)
return Number(rounded) === 0 ? (0).toFixed(digits) : rounded
}
export function formatDD({ lat, lon }: LatLon, digits = 6): string {
return `${toFixedUnsignedZero(lat, digits)}, ${toFixedUnsignedZero(lon, digits)}`
}
function splitDMS(value: number, secDigits: number): [number, number, number] {
const abs = Math.abs(value)
let d = Math.floor(abs)
let m = Math.floor((abs - d) * 60)
let s = Number(((abs - d - m / 60) * 3600).toFixed(secDigits))
if (s >= 60) {
s = 0
m += 1
}
if (m >= 60) {
m = 0
d += 1
}
return [d, m, s]
}
/** The hemisphere letter. A value that rounds to zero is N or E, so the text reads back the same. */
export function hemi(
value: number,
axis: 'lat' | 'lon',
isZero: boolean,
): string {
const isNegative = value < 0 && !isZero
if (axis === 'lat') return isNegative ? 'S' : 'N'
return isNegative ? 'W' : 'E'
}
export function formatDMSValue(
value: number,
axis: 'lat' | 'lon',
secDigits = 2,
): string {
const [d, m, s] = splitDMS(value, secDigits)
const pad = axis === 'lat' ? 2 : 3
const sStr = s.toFixed(secDigits).padStart(secDigits ? secDigits + 3 : 2, '0')
const isZero = d === 0 && m === 0 && s === 0
return `${String(d).padStart(pad, '0')}° ${String(m).padStart(2, '0')}′ ${sStr}″ ${hemi(value, axis, isZero)}`
}
export function formatDMS(p: LatLon, secDigits = 2): string {
return `${formatDMSValue(p.lat, 'lat', secDigits)}, ${formatDMSValue(p.lon, 'lon', secDigits)}`
}
export function formatDDMValue(
value: number,
axis: 'lat' | 'lon',
minDigits = 4,
): string {
const abs = Math.abs(value)
let d = Math.floor(abs)
let m = Number(((abs - d) * 60).toFixed(minDigits))
if (m >= 60) {
m = 0
d += 1
}
const pad = axis === 'lat' ? 2 : 3
const isZero = d === 0 && m === 0
return `${String(d).padStart(pad, '0')}° ${m.toFixed(minDigits).padStart(minDigits + 3, '0')}′ ${hemi(value, axis, isZero)}`
}
export function formatDDM(p: LatLon, minDigits = 4): string {
return `${formatDDMValue(p.lat, 'lat', minDigits)}, ${formatDDMValue(p.lon, 'lon', minDigits)}`
}
export interface LatLonParse {
kind: AngleKind
point: LatLon
}
const NUM = String.raw`[-+]?\d+(?:\.\d+)?`
/** Parse one angle, e.g. `51°30'26.5"N`, `51 30.441 N`, `-0.1278`, `W 0 7 40`. */
function parseAngle(
raw: string,
axis: 'lat' | 'lon',
): { value: number; kind: AngleKind } | null {
let s = raw.trim().toUpperCase()
let sign = 1
const hemiRe = axis === 'lat' ? /[NS]/ : /[EW]/
const hm = s.match(hemiRe)
if (hm) {
if (hm[0] === 'S' || hm[0] === 'W') sign = -1
s = s.replace(hemiRe, ' ')
}
if (/[A-Z]/.test(s)) return null
const nums = s.match(new RegExp(NUM, 'g'))?.map(Number)
if (!nums || nums.length === 0 || nums.length > 3) return null
if (nums[0]! < 0) {
sign = -sign
nums[0] = -nums[0]!
}
const [d, m = 0, sec = 0] = nums
if (m < 0 || m >= 60 || sec < 0 || sec >= 60) return null
const kind: AngleKind =
nums.length === 1 ? 'DD' : nums.length === 2 ? 'DDM' : 'DMS'
return { value: sign * (d! + m / 60 + sec / 3600), kind }
}
export function parseLatLon(input: string): LatLonParse | null {
const s = input.trim().toUpperCase()
let latStr: string | undefined
let lonStr: string | undefined
// "51 30 N 0 7 W" style: split after the first hemisphere letter.
const hemi = /^[NS]/.test(s)
? /^([NS][^EW]*?)\s*[,;]?\s*([EW].*)$/.exec(s)
: /^(.*?[NS])\s*[,;]?\s*(.*[EW].*)$/.exec(s)
if (hemi) {
latStr = hemi[1]
lonStr = hemi[2]
} else if (/[,;]/.test(s)) {
const parts = s.split(/\s*[;,]\s*(?=[-+\dNSEW])/)
Eif (parts.length === 2) [latStr, lonStr] = parts
} else {
const nums = s.split(/\s+/)
if (nums.length === 2) [latStr, lonStr] = nums
}
if (!latStr || !lonStr) return null
const lat = parseAngle(latStr, 'lat')
const lon = parseAngle(lonStr, 'lon')
if (!lat || !lon) return null
const point = { lat: lat.value, lon: lon.value }
if (!isValidLatLon(point)) return null
const rank: Record<AngleKind, number> = { DD: 0, DDM: 1, DMS: 2 }
return { kind: rank[lat.kind] >= rank[lon.kind] ? lat.kind : lon.kind, point }
}
|