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 | 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 2091x 135x 134x 1x 2091x 2091x 15x 15x 2091x 2091x 2091x 2091x 10427x 10427x 10427x 10427x 10427x 10427x 10427x 10427x 2091x 2076x 2076x 2076x 2076x 2076x 2076x 8x 8x 8x 8x 8x 8x 8x 2076x 15x 2076x 2076x 2076x 2080x 2080x 1007x 1007x 1007x 1007x 1007x 1007x 5029x 5029x 5029x 5029x 5029x 1007x 1x 1x 1x 1x 1x 1007x 15x 8032x 8032x 8032x 2013x 2013x 2012x 8032x 1007x 1007x 1007x | /**
* Open Location Code (Plus Codes), Google.
*
* Five digit pairs (latitude then longitude) at 20°, 1°, 0.05°, 0.0025° and 0.000125°, written
* with a "+" after the eighth digit, then optional grid digits that split a cell 4 × 5.
* Only full codes are read: a short code needs a reference location.
*/
import type { LatLon } from '../../geo/ellipsoid'
const ALPHABET = '23456789CFGHJMPQRVWX'
const BASE = 20
export const PAIR_RESOLUTIONS = [20, 1, 0.05, 0.0025, 0.000125]
const PAIR_LENGTH = 10
const SEPARATOR_POSITION = 8
const GRID_ROWS = 5
const GRID_COLUMNS = 4
const MAX_LATITUDE = 90
const MAX_LONGITUDE = 180
// NOTE: in-range longitudes pass through untouched. The modulo form rounds 179.99999999999994
// up to 180 and wraps it to -180. Unlike geo's wrapLon, 180 itself becomes -180, as in
// Google's reference implementation.
const wrapLon = (lon: number) =>
lon >= -MAX_LONGITUDE && lon < MAX_LONGITUDE
? lon
: ((((lon + MAX_LONGITUDE) % 360) + 360) % 360) - MAX_LONGITUDE
export interface PlusCodeCell {
west: number
south: number
east: number
north: number
}
/** The height of a code's cell, which is how far latitude 90 moves down so it can be decoded. */
function latitudePrecision(length: number): number {
if (length <= PAIR_LENGTH) {
return BASE ** Math.floor(length / -2 + 2)
}
return BASE ** -3 / GRID_ROWS ** (length - PAIR_LENGTH)
}
/** Clip the latitude, wrap the longitude, and move the north pole just inside the top cell. */
function normalise({ lat, lon }: LatLon, length: number): LatLon {
const clippedLat = Math.min(Math.max(lat, -MAX_LATITUDE), MAX_LATITUDE)
return {
lat:
clippedLat === MAX_LATITUDE
? clippedLat - latitudePrecision(length)
: clippedLat,
lon: wrapLon(lon),
}
}
/** The first pair covers 180° of latitude and 360° of longitude in 20° steps. */
const FIRST_PAIR_LAT_DIGITS = 9
const FIRST_PAIR_LON_DIGITS = 18
// NOTE: near 180° longitude, the division below can round 17.999... up to 18, which is not a
// valid first digit. The npm open-location-code 1.0.3 package has the same bug; the cap keeps
// the point in the easternmost cell.
function encodePairs({ lat, lon }: LatLon, pairCount: number): string {
let latValue = lat + MAX_LATITUDE
let lonValue = lon + MAX_LONGITUDE
let digits = ''
for (let pair = 0; pair < pairCount; pair++) {
const resolution = PAIR_RESOLUTIONS[pair]!
const latLimit = pair === 0 ? FIRST_PAIR_LAT_DIGITS : BASE
const lonLimit = pair === 0 ? FIRST_PAIR_LON_DIGITS : BASE
const latIndex = Math.min(latLimit - 1, Math.floor(latValue / resolution))
const lonIndex = Math.min(lonLimit - 1, Math.floor(lonValue / resolution))
digits += ALPHABET.charAt(latIndex) + ALPHABET.charAt(lonIndex)
latValue -= latIndex * resolution
lonValue -= lonIndex * resolution
}
return digits
}
// NOTE: this follows Google's reference implementation step for step, including taking the
// remainder of the whole coordinate, so the grid digit matches it at cell edges.
function encodeGrid({ lat, lon }: LatLon, gridDigits: number): string {
let latPlace = PAIR_RESOLUTIONS[PAIR_RESOLUTIONS.length - 1]!
let lonPlace = latPlace
let latValue = (lat + MAX_LATITUDE) % latPlace
let lonValue = (lon + MAX_LONGITUDE) % lonPlace
let digits = ''
for (let index = 0; index < gridDigits; index++) {
const row = Math.floor(latValue / (latPlace / GRID_ROWS))
const column = Math.floor(lonValue / (lonPlace / GRID_COLUMNS))
latPlace /= GRID_ROWS
lonPlace /= GRID_COLUMNS
latValue -= row * latPlace
lonValue -= column * lonPlace
digits += ALPHABET.charAt(row * GRID_COLUMNS + column)
}
return digits
}
/** The digit pairs (no separator, no padding) for a point, to `pairCount` pairs. */
export function pairDigits(point: LatLon, pairCount: number): string {
return encodePairs(normalise(point, pairCount * 2), pairCount)
}
/** A full Plus Code: 10 digits, or more with grid refinement digits. */
export function toPlusCode(point: LatLon, length = PAIR_LENGTH): string {
const normalised = normalise(point, length)
const digits =
encodePairs(normalised, PAIR_LENGTH / 2) +
encodeGrid(normalised, Math.max(0, length - PAIR_LENGTH))
return withSeparator(digits)
}
/** Put the "+" after the eighth digit, padding a shorter code with zeros. */
export function withSeparator(digits: string): string {
Eif (digits.length >= SEPARATOR_POSITION) {
return `${digits.slice(0, SEPARATOR_POSITION)}+${digits.slice(SEPARATOR_POSITION)}`
}
return `${digits.padEnd(SEPARATOR_POSITION, '0')}+`
}
function fromDigits(digits: string): PlusCodeCell {
let south = -MAX_LATITUDE
let west = -MAX_LONGITUDE
let height = 0
let width = 0
const pairs = Math.min(digits.length, PAIR_LENGTH)
for (let index = 0; index < pairs; index += 2) {
const resolution = PAIR_RESOLUTIONS[index / 2]!
south += ALPHABET.indexOf(digits.charAt(index)) * resolution
west += ALPHABET.indexOf(digits.charAt(index + 1)) * resolution
height = resolution
width = resolution
}
for (let index = PAIR_LENGTH; index < digits.length; index++) {
const value = ALPHABET.indexOf(digits.charAt(index))
height /= GRID_ROWS
width /= GRID_COLUMNS
south += Math.floor(value / GRID_COLUMNS) * height
west += (value % GRID_COLUMNS) * width
}
return { west, south, east: west + width, north: south + height }
}
const FULL_CODE_RE =
/^([2-9C][2-9CFGHJMPQRV])([2-9CFGHJMPQRVWX]{6}|[2-9CFGHJMPQRVWX]{4}00|[2-9CFGHJMPQRVWX]{2}0000|000000)\+([2-9CFGHJMPQRVWX]{2,7})?$/
/** Whether text is a full Plus Code, with or without padding. */
export function isFullPlusCode(text: string): boolean {
const code = text.toUpperCase()
const match = FULL_CODE_RE.exec(code)
if (!match) return false
const isPadded = code.includes('0')
if (isPadded && match[3] !== undefined) return false
const tail = match[3] ?? ''
return tail.length !== 1
}
/** The cell a full Plus Code names. */
export function fromPlusCode(text: string): PlusCodeCell {
Iif (!isFullPlusCode(text)) throw new Error(`Invalid Plus Code "${text}"`)
const digits = text.toUpperCase().replace('+', '').replace(/0+$/, '')
return fromDigits(digits)
}
|