Created
December 2, 2021 19:35
-
-
Save vyatsyk/9d879cca78f945bf64eb9962dd1ce9dc to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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