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)
];
}
}