Would you like to clone this notebook?

When you clone a notebook you are able to make changes without affecting the original notebook.

Cancel

Encode/Decode Open Location Code/Plus Code

node v14.20.1
version: 10.0.0
endpointsharetweet
Encode and decode functions for Google Maps Plus Codes (aka Open Location Codes).
const axios = require('axios'); /** * @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 */ /** * Recovers the missing initial digits of a short code, given a region, via forward geocoding and converts it to a full plus code/open location code * * @method _shortCodeToPlusCode * @requires axios * @param {string} shortCode - the original open location short code to recover * @param {string} regionQuery - an address, city, or region to forward geocode in order to recover the whole plus code/open location code * @returns {string} a complete open location/plus code based on the given short code and region parameters * * @example * _shortCodeToPlusCode('W9V7+JQ', 'New York, NY'); * // returns '8698W9V7+JQ' */ const _shortCodeToPlusCode = async (shortCode, regionQuery) => { const { data } = await axios.get(`http://api.positionstack.com/v1/forward?access_key=${process.env.POSITIONSTACK_API_KEY}&query=${encodeURIComponent(regionQuery)}&limit=1`); if (data.status >= 400) throw new Error('Unable to geocode specified region'); const { latitude, longitude } = data.data[0]; if (!latitude && !longitude) throw new Error('Unable to geolocate region from specified query.'); const regionCode = module.exports.encodePlusCode([ latitude, longitude ]); return `${regionCode.substr(0, 8 - shortCode.split('+')[0].length)}${shortCode}`; } 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((_, d) => [latitude, longitude] .map(meridian => meridian[d])) .map((d, i) => (i === 8 ? '+' : '') + _numerals.charAt(d)) .join('') // Alternative to above line for earlier versions of Javascript without Array.flatMap(): // // return latitude // .map((_, d) => [latitude, longitude] // .map(meridian => meridian[d])) // .reduce((code, pair) => code.concat(pair)) // .map((d, i) => (i === 8 ? '+' : '') + _numerals.charAt(d)) // .join(''); }, /** * Decodes an open location/plus code, given a valid code * * @param {string} plusCode - a valid open location/plus code * @param {?string=} shortCodeRegion - a region in which to base the result if a shortcode is provided (e.g. a plus code with a "+" separator and fewer than 8 digits in front of it) * @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(encodePlusCode([ 37.944027, -93.635504 ])); * // returns [ 37.944063, -93.635563 ] * * @todo validation... * @todo handle odd-length codes with length > 10 * @todo handle spacer chars * @todo handle short codes */ decodePlusCode: async (plusCode, shortCodeRegion = null, places = 6) => { if (plusCode.search(/\+/) > 0 && plusCode.split('+')[0].length < 8 && !shortCodeRegion) throw new Error('You must supply a region argument to use with a short code'); if (plusCode.search(/\+/) > 0 && plusCode.split('+')[0].length < 8 && shortCodeRegion) plusCode = await _shortCodeToPlusCode(plusCode, shortCodeRegion); console.log(plusCode); const [ latitude, longitude ] = plusCode .replace('+','') .split('') .map(d => _numerals.indexOf(d)) .reduce(([lat, lon], d, i) => i % 2 === 0 ? [[...lat, d], lon] : [lat, [...lon, d]], [[], []]) .map(meridian => meridian.reduce((total, d, i) => total + d * 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) ]; } }
const { encodePlusCode, decodePlusCode } = module.exports; // or const { encodePlusCode, decodePlusCode } = require('@runkit/avanavana/encode-decode-open-location-code-plus-code/releases/7.2.1');
Encoding from an array of latitude and longitude numbers
encodePlusCode([37.944027, -93.635504]);
Encoding from a latitude and longitude string
encodePlusCode('37.944027, -93.635504'.split(', '));
Decoding an open location code/plus code
decodePlusCode('8698W9V7+JQ');
Decoding and encoding are inverse operations
decodePlusCode(encodePlusCode([37.944027, -93.635504]));
Decoding and encoding are inverse operations
encodePlusCode(decodePlusCode('8698W9V7+JQ'));
Express Endpoint
const express = require('@runkit/runkit/express-endpoint/1.0.0'); const app = express(exports); app.get('/encode/:coordinates', (req, res) => res.status(200).send(encodePlusCode(decodeURIComponent(req.params.coordinates)))); app.get('/decode/:plusCode', (req, res) => { try { let result = ''; if (!req.params.plusCode) throw new Error('Plus Code is a required URL parameter (e.g. .../decode/:plusCode)'); if (req.query.q) result = decodePlusCode(decodeURIComponent(req.params.plusCode), decodeURIComponent(req.query.q)); else result = decodePlusCode(decodeURIComponent(req.params.plusCode)); return res.status(200).send({ result }); } catch (error) { return res.status(400).send(error.message); } });
Decoding a Short Code, given a region to search using PositionStack API
decodePlusCode('XFCC+6P4', 'Astore, Pakistan');
Loading…

no comments

    sign in to comment