Skip to content

Instantly share code, notes, and snippets.

@avanavana
Last active April 11, 2024 07:37
Show Gist options
  • Save avanavana/76ff5851a359100dde276b364d31c561 to your computer and use it in GitHub Desktop.
Save avanavana/76ff5851a359100dde276b364d31c561 to your computer and use it in GitHub Desktop.
plusCodeUtils – Module exposing encode and decode methods for Open Location/Plus Codes (from/to map coordinates)
/**
* @file Open Location/Plus Code Encoding/Decoding Utilities
* @author Avana Vana <dear.avana@gmail.com>
* @module plusCodeUtils
* @version 2.6.0
*/
/**
* @constant {string} _numerals - string listing official base-20 open location/plus code numerals
* @readonly
*/
const _numerals = '23456789CFGHJMPQRVWX';
/**
* WGS-84 geodetic map coordinates, in the form of the array, [ latitude, longitude ], where both latitude and longitude are either numbers or strings
* @typedef {(number[]|string[])} Coordinates
* @property {(number|string)} latitude - WGS-84 geodetic latitude from 90 to -90, as either a number or a string
* @property {(number|string)} longitude - WGS-84 geodetic longitude from 180 to -180, as either a number or a string
*/
module.exports = {
/**
* Encodes an open location/plus code, given map coordinates
*
* @method encodePlusCode
* @requires _numerals
* @param {Coordinates} coords - WGS-84 geodetic latitude and longitude as an array of either strings or numbers
* @param {number} [resolution=5] - number of pairs of digits in the return code, greater is more specific
* @returns {string} an open location/plus code of the specified {@link resolution}
*
* @example
* // encodePlusCode([ 37.944027, -93.635504 ]);
* // returns '8698W9V7+JQ'
*
* @example <caption>Takes either an array of numbers or an array of strings as an argument, so you can do:</caption>
* // encodePlusCode('37.944027, -93.635504'.split(', '));
* // returns '8698W9V7+JQ'
*
* @example <caption>encodePlusCode() is the inverse of decodePlusCode(). A small amount of uncertainty is part of the spec.</caption>
* // encodePlusCode(decodePlusCode('8698W9V7+JQ'));
* // returns '8698W9V7+JQ'
*
* @todo validation...
*/
encodePlusCode: (coords, resolution = 5) => {
const [ latitude, longitude ] = [ Math.floor((+coords[0] + 90) * 8000), Math.floor((+coords[1] + 180) * 8000) ]
.map((meridian) => [ ...Array(resolution) ]
.map((_, i) => Math.floor((meridian / Math.pow(20, i)) % 20).toString())
.reverse());
return latitude
.flatMap((_, digit) => [ latitude, longitude ]
.map((meridian) => meridian[digit]))
.map((digit, i) => (i === 8 ? '+' : '') + _numerals.charAt(digit))
.join('');
// Alternative to above line for earlier versions of Javascript without Array.flatMap():
//
// return latitude
// .map((_, digit) => [ latitude, longitude ]
// .map((meridian) => meridian[digit]))
// .reduce((code, pair) => code.concat(pair))
// .map((digit, i) => (i === 8 ? '+' : '') + _numerals.charAt(digit))
// .join('');
},
/**
* Decodes an open location/plus code, given a valid code
*
* @method decodePlusCode
* @requires _numerals
* @param {string} plusCode - a valid open location/plus code
* @param {number} [places=6] - the number of decimal places for all return values
* @returns {Coordinates} WGS-84 geodetic latitude and longitude, as an array of numbers, each with the number of specified decimal {@link places}
*
* @example
* // decodePlusCode('8698W9V7+JQ');
* // returns [ 37.944063, -93.635563 ]
*
* @example <caption>decodePlusCode() is the inverse of encodePlusCode(). A small amount of uncertainty is part of the spec.</caption>
* // decodePlusCode(encodePlusCode([ 37.944027, -93.635504 ]));
* // returns [ 37.944063, -93.635563 ]
*
* @todo validation...
* @todo handle odd-length codes with length > 10
* @todo handle spacer chars
*/
decodePlusCode: (plusCode, places = 6) => {
const [ latitude, longitude ] = plusCode
.replace('+','')
.split('')
.map((digit) => _numerals.indexOf(digit))
.reduce(([ lat, lon ], digit, i) => i % 2 === 0 ? [ [ ...lat, digit ], lon ] : [ lat, [ ...lon, digit ] ], [ [], [] ])
.map((meridian) => meridian.reduce((total, digit, i) => total + digit * Math.pow(20, 1 - i), 0))
.map((meridian) => meridian + Math.pow(20, 2 - plusCode.replace('+', '').length / 2) / 2);
return [ +(latitude - 90).toFixed(places), +(longitude - 180).toFixed(places) ];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment