Skip to content

Instantly share code, notes, and snippets.

@vyatsyk
Created December 2, 2021 19:35
Show Gist options
  • Save vyatsyk/9d879cca78f945bf64eb9962dd1ce9dc to your computer and use it in GitHub Desktop.
Save vyatsyk/9d879cca78f945bf64eb9962dd1ce9dc to your computer and use it in GitHub Desktop.
const BigNumber = require('bignumber.js');
const BigInt = require('big-integer');
const DEFAULT_PRECISION = 5;
const ENCODING_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
const DECODING_TABLE = [
62, -1, -1, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1,
0, 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, -1, -1, -1, -1, 63, -1, 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
];
const FORMAT_VERSION = 1;
const ABSENT = 0;
const LEVEL = 1;
const ALTITUDE = 2;
const ELEVATION = 3;
// Reserved values 4 and 5 should not be selectable
const CUSTOM1 = 6;
const CUSTOM2 = 7;
function decode(encoded) {
const decoder = decodeUnsignedValues(encoded);
const header = decodeHeader(decoder[0], decoder[1]);
const factorDegree = BigInt(10).pow(header.precision);
const factorZ = BigInt(10).pow(header.thirdDimPrecision);
const { thirdDim } = header;
let lastLat = BigNumber(0);
let lastLng = BigNumber(0);
let lastZ = BigNumber(0);
const res = [];
let i = 2;
for (;i < decoder.length;) {
const deltaLat = BigNumber(toSigned(decoder[i]).toString()).div(factorDegree);
const deltaLng = BigNumber(toSigned(decoder[i + 1]).toString()).div(factorDegree);
lastLat = lastLat.plus(deltaLat);
lastLng = lastLng.plus(deltaLng);
if (thirdDim.toJSNumber()) {
const deltaZ = BigNumber(toSigned(decoder[i + 2]).toString()).div(factorZ);
lastZ = lastZ.plus(deltaZ);
res.push([lastLat, lastLng, lastZ]);
i += 3;
} else {
res.push([lastLat, lastLng]);
i += 2;
}
}
if (i !== decoder.length) {
throw new Error('Invalid encoding. Premature ending reached');
}
return {
...header,
polyline: res,
};
}
function decodeChar(char) {
const charCode = char.charCodeAt(0);
return DECODING_TABLE[charCode - 45];
}
function decodeUnsignedValues(encoded) {
let result = BigInt(0);
let shift = BigInt(0);
const resList = [];
encoded.split('').forEach((char) => {
const value = BigInt(decodeChar(char));
result = result.or(value.and(0x1F).shiftLeft(shift));
if (value.and(0x20).equals(BigInt(0))) {
resList.push(result);
result = BigInt(0);
shift = BigInt(0);
} else {
shift = shift.add(5);
}
});
if (shift > 0) {
throw new Error('Invalid encoding');
}
return resList;
}
function decodeHeader(version, encodedHeader) {
if (+version.toString() !== FORMAT_VERSION) {
throw new Error('Invalid format version');
}
const precision = encodedHeader.and(15);
const thirdDim = encodedHeader.shiftRight(4).and(7);
const thirdDimPrecision = encodedHeader.shiftRight(7).and(15);
return { precision, thirdDim, thirdDimPrecision };
}
function toSigned(val) {
// Decode the sign from an unsigned value
let res = val;
if (res.and(1).toJSNumber()) {
res = res.not();
}
res = res.shiftRight(1);
return res;
}
function encode({ precision = DEFAULT_PRECISION, thirdDim = ABSENT, thirdDimPrecision = 0, polyline }) {
// Encode a sequence of lat,lng or lat,lng(,{third_dim}). Note that values should be of type BigNumber
// `precision`: how many decimal digits of precision to store the latitude and longitude.
// `third_dim`: type of the third dimension if present in the input.
// `third_dim_precision`: how many decimal digits of precision to store the third dimension.
const multiplierDegree = 10 ** precision;
const multiplierZ = 10 ** thirdDimPrecision;
const encodedHeaderList = encodeHeader(precision, thirdDim, thirdDimPrecision);
const encodedCoords = [];
let lastLat = BigInt(0);
let lastLng = BigInt(0);
let lastZ = BigInt(0);
polyline.forEach((location) => {
const lat = BigInt(location[0].times(multiplierDegree).integerValue().toString());
encodedCoords.push(encodeScaledValue(lat.minus(lastLat)));
lastLat = lat;
const lng = BigInt(location[1].times(multiplierDegree).integerValue().toString());
encodedCoords.push(encodeScaledValue(lng.minus(lastLng)));
lastLng = lng;
if (thirdDim) {
const z = BigInt(location[2].times(multiplierZ).integerValue().toString());
encodedCoords.push(encodeScaledValue(z.minus(lastZ)));
lastZ = z;
}
});
return [...encodedHeaderList, ...encodedCoords].join('');
}
function encodeHeader(precision, thirdDim, thirdDimPrecision) {
// Encode the `precision`, `third_dim` and `third_dim_precision` into one encoded char
if (precision < 0 || precision > 15) {
throw new Error('precision out of range. Should be between 0 and 15');
}
if (thirdDimPrecision < 0 || thirdDimPrecision > 15) {
throw new Error('thirdDimPrecision out of range. Should be between 0 and 15');
}
if (thirdDim < 0 || thirdDim > 7 || thirdDim === 4 || thirdDim === 5) {
throw new Error('thirdDim should be between 0, 1, 2, 3, 6 or 7');
}
const res = (thirdDimPrecision << 7) | (thirdDim << 4) | precision;
return encodeUnsignedNumber(FORMAT_VERSION) + encodeUnsignedNumber(res);
}
function encodeUnsignedNumber(val) {
// Uses variable integer encoding to encode an unsigned integer. Returns the encoded string.
let res = '';
let bigIntVal = BigInt(val);
while (bigIntVal.gt(0x1F)) {
const pos = bigIntVal.and(0x1F).or(0x20);
res += ENCODING_TABLE[pos.toJSNumber()];
bigIntVal = bigIntVal.shiftRight(5);
}
return res + ENCODING_TABLE[bigIntVal];
}
function encodeScaledValue(value) {
// Transform a integer `value` into a variable length sequence of characters.
let bigIntValue = BigInt(value);
const negative = bigIntValue.lt(0);
bigIntValue = bigIntValue.shiftLeft(1);
if (negative) {
bigIntValue = bigIntValue.not();
}
return encodeUnsignedNumber(bigIntValue);
}
module.exports = {
encode,
decode,
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment