UNPKG

@pranavpatel.ca/algo-gridpointcode

Version:

Grid Point Code (GPC) is a global geocoding system that provides a unique, lossless, and compact alphanumeric code for any geographic location (home, office, or other places). It enables precise identification and offline conversion between geographic coo

235 lines (234 loc) 9.34 kB
"use strict"; // Copyright 2017 Pranavkumar Patel // Licensed under the Apache License, Version 2.0 Object.defineProperty(exports, "__esModule", { value: true }); exports.GPC = void 0; const algo_kombin_1 = require("@pranavpatel.ca/algo-kombin"); /** * The GPC (Grid Point Code) class provides methods for encoding geographic * coordinates (latitude and longitude) into a compact alphanumeric string, * and decoding such strings back to coordinates. * * This implementation ensures consistency, precision, and compactness * using a custom base-27 encoding and a fixed 11-character format. */ class GPC { static MIN_LAT = -90; static MAX_LAT = 90; static MIN_LONG = -180; static MAX_LONG = 180; static MAX_POINT = 648009999999999n; static ELEVEN = 205881132094649n; static CHARACTERS = "CDFGHJKLMNPRTVWXY0123456789"; static GPC_LENGTH = 11; static LatLongTable = new algo_kombin_1.Table(180, 360, true); /** * Encodes a latitude and longitude into a Grid Point Code (GPC). * @param latitude Latitude in decimal degrees. * @param longitude Longitude in decimal degrees. * @param formatted Whether to format the GPC with separators (default: true). * @returns A formatted or raw 11-character GPC string. * @throws RangeError if coordinates are out of valid range. */ static encode(latitude, longitude, formatted = true) { const [valid, message] = this.isValidCoordinates(latitude, longitude); if (!valid) throw new RangeError(`${message.toUpperCase()}: value out of valid range.`); const point = this.getPoint(latitude, longitude); let gridPointCode = this.encodePoint(point + this.ELEVEN); if (formatted) { gridPointCode = this.formatGPC(gridPointCode); } return gridPointCode; } /** * Decodes a Grid Point Code (GPC) into latitude and longitude. * @param gridPointCode The encoded GPC string (formatted or raw). * @returns A tuple [latitude, longitude] in decimal degrees. * @throws Error or RangeError for null, malformed, or invalid GPC strings. */ static decode(gridPointCode) { if (!gridPointCode || gridPointCode.trim() === '') { throw new Error("GPC_NULL: Invalid GPC."); } gridPointCode = gridPointCode.replace(/[\s#-]/g, '').toUpperCase(); let [valid, message] = this.validateGPC(gridPointCode); if (!valid) throw new RangeError(`${message}: Invalid GPC.`); const point = this.decodeToPoint(gridPointCode) - this.ELEVEN; [valid, message] = this.validatePoint(point); if (!valid) throw new RangeError(`${message}: Invalid GPC.`); return this.getCoordinates(point); } /** * Validates the latitude and longitude ranges. * @param latitude Latitude in decimal degrees. * @param longitude Longitude in decimal degrees. * @returns [true, ""] if valid, otherwise [false, "LATITUDE" or "LONGITUDE"]. */ static isValidCoordinates(latitude, longitude) { if (latitude <= this.MIN_LAT || latitude >= this.MAX_LAT) return [false, "LATITUDE"]; if (longitude <= this.MIN_LONG || longitude >= this.MAX_LONG) return [false, "LONGITUDE"]; return [true, ""]; } /** * Validates the given Grid Point Code (GPC). * @param gridPointCode GPC string to validate. * @returns [true, ""] if valid, otherwise [false, error message]. */ static isValid(gridPointCode) { gridPointCode = gridPointCode.replace(/[\s#-]/g, '').toUpperCase(); if (!gridPointCode) return [false, "GPC_NULL"]; let [valid, message] = this.validateGPC(gridPointCode); if (!valid) return [false, message]; [valid, message] = this.validatePoint(this.decodeToPoint(gridPointCode) - this.ELEVEN); if (!valid) return [false, message]; return [true, ""]; } /** * Converts latitude and longitude into a unique numeric point ID. * Used internally before encoding to base-27. * @param lat Latitude. * @param long Longitude. * @returns A bigint representing the point. */ static getPoint(lat, long) { const lat7 = this.splitTo7(lat); const long7 = this.splitTo7(long); let point = BigInt(Math.pow(10, 10) * (this.LatLongTable.GetIndexOfElements((lat7[1] * 2) + (lat7[0] === -1 ? 1 : 0), (long7[1] * 2) + (long7[0] === -1 ? 1 : 0)) + 1)); let power = 9; for (let i = 2; i <= 6; i++) { point += BigInt(Math.pow(10, power--)) * BigInt(lat7[i]); point += BigInt(Math.pow(10, power--)) * BigInt(long7[i]); } return point; } /** * Converts a decimal coordinate to a 7-part array: * [sign, integer, fractional_digit1...fractional_digit5] * @param coord Coordinate value as number. * @returns An array of 7 integers. */ static splitTo7(coord) { const result = new Array(7).fill(0); const coordStr = coord.toFixed(10); result[0] = coordStr.startsWith('-') ? -1 : 1; const cleaned = coordStr.replace(/^[-+]/, ''); const [integerPart, fractionalPartRaw = ''] = cleaned.split('.'); result[1] = parseInt(integerPart, 10); const fractionalPart = (fractionalPartRaw + '00000').slice(0, 5); for (let i = 0; i < 5; i++) { result[i + 2] = parseInt(fractionalPart[i], 10); } return result; } /** * Encodes a numeric point into a base-27 GPC string. * @param point Bigint point number. * @returns Encoded GPC string (unformatted). */ static encodePoint(point) { let gpc = ''; const base = BigInt(27); while (point > 0) { gpc = this.CHARACTERS[Number(point % base)] + gpc; point = point / base; } return gpc; } /** * Formats an 11-character GPC into #XXXX-XXXX-XXX layout. * @param gpc Unformatted 11-character GPC string. * @returns Formatted GPC string. */ static formatGPC(gpc) { return `#${gpc.slice(0, 4)}-${gpc.slice(4, 8)}-${gpc.slice(8, 11)}`; } /** * Validates the character content and length of a GPC string. * @param gpc GPC string to validate. * @returns [true, ""] if valid, otherwise [false, error reason]. */ static validateGPC(gpc) { if (gpc.length !== this.GPC_LENGTH) return [false, "GPC_LENGTH"]; for (const char of gpc) { if (!this.CHARACTERS.includes(char)) return [false, "GPC_CHAR"]; } return [true, ""]; } /** * Validates that the decoded point value is within allowed range. * @param point GPC-decoded point. * @returns [true, ""] if valid, otherwise [false, "GPC_RANGE"]. */ static validatePoint(point) { if (point > this.MAX_POINT) return [false, "GPC_RANGE"]; return [true, ""]; } /** * Decodes an 11-character GPC string into a numeric point. * @param gpc GPC string (unformatted). * @returns Decoded point as bigint. */ static decodeToPoint(gpc) { let point = 0n; for (let i = 0; i < this.GPC_LENGTH; i++) { point *= 27n; point += BigInt(this.CHARACTERS.indexOf(gpc[i])); } return point; } /** * Converts a valid point number into latitude and longitude. * @param point Valid GPC-decoded point. * @returns Tuple [latitude, longitude] in decimal degrees. */ static getCoordinates(point) { const latLongIndex = Number(point / 10000000000n); const fractional = point - BigInt(latLongIndex) * 10000000000n; const [lat7, long7] = this.splitPointTo7(latLongIndex, fractional); let power = 0; let tempLat = 0, tempLong = 0; for (let i = 6; i >= 1; i--) { tempLat += lat7[i] * Math.pow(10, power); tempLong += long7[i] * Math.pow(10, power++); } return [ tempLat / Math.pow(10, 5) * lat7[0], tempLong / Math.pow(10, 5) * long7[0] ]; } /** * Reconstructs lat/long 7-part arrays from a point index and fractional component. * Used in decoding to rebuild original coordinate precision. * @param index Index from table lookup. * @param fractional The remaining digits of the point. * @returns A tuple of two arrays: [lat7[], long7[]] */ static splitPointTo7(index, fractional) { const lat7 = new Array(7); const long7 = new Array(7); const [tLat, tLong] = this.LatLongTable.GetElementsAtIndex(index - 1); lat7[0] = tLat % 2 !== 0 ? -1 : 1; lat7[1] = lat7[0] === -1 ? (tLat - 1) / 2 : tLat / 2; long7[0] = tLong % 2 !== 0 ? -1 : 1; long7[1] = long7[0] === -1 ? (tLong - 1) / 2 : tLong / 2; let power = 9; for (let i = 2; i <= 6; i++) { lat7[i] = Number((fractional / BigInt(Math.pow(10, power--))) % 10n); long7[i] = Number((fractional / BigInt(Math.pow(10, power--))) % 10n); } return [lat7, long7]; } } exports.GPC = GPC;