diff --git a/javascript/demo/polyline-test.html b/javascript/demo/polyline-test.html new file mode 100644 index 0000000..007dd41 --- /dev/null +++ b/javascript/demo/polyline-test.html @@ -0,0 +1,101 @@ + + + + + + +
+ + + + + + diff --git a/javascript/package-lock.json b/javascript/package-lock.json index 8395e6f..e88de32 100644 --- a/javascript/package-lock.json +++ b/javascript/package-lock.json @@ -9,8 +9,6 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@here/flexpolyline": "^0.1.0", - "@mapbox/polyline": "^1.2.1", "@types/geojson": "^7946.0.10" }, "devDependencies": { @@ -60,6 +58,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/highlight": "^7.24.7", @@ -431,6 +430,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -480,6 +480,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.24.7", @@ -2001,12 +2002,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@here/flexpolyline": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@here/flexpolyline/-/flexpolyline-0.1.0.tgz", - "integrity": "sha512-aiFWozFqTbeAG6S8AOH9QPF7UYjChE1ApV+0dK4kiWp+S2MIJXD/WzHZ9kwd8hDC8gHRAl/yVtJ50uVf26/ygQ==", - "license": "MIT" - }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -2956,17 +2951,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mapbox/polyline": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@mapbox/polyline/-/polyline-1.2.1.tgz", - "integrity": "sha512-sn0V18O3OzW4RCcPoUIVDWvEGQaBNH9a0y5lgqrf5hUycyw1CzrhEoxV5irzrMNXKCkw1xRsZXcaVbsVZggHXA==", - "dependencies": { - "meow": "^9.0.0" - }, - "bin": { - "polyline": "bin/polyline.bin.js" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3533,12 +3517,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", - "license": "MIT" - }, "node_modules/@types/node": { "version": "20.14.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", @@ -3549,12 +3527,6 @@ "undici-types": "~5.26.4" } }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "license": "MIT" - }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -3912,6 +3884,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^1.9.0" @@ -3991,15 +3964,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/async": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", @@ -4395,28 +4359,12 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "license": "MIT", - "dependencies": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001643", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz", @@ -4442,6 +4390,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", @@ -4598,6 +4547,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "1.1.3" @@ -4607,6 +4557,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, "license": "MIT" }, "node_modules/commondir": { @@ -4829,40 +4780,6 @@ } } }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decamelize-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", - "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", - "license": "MIT", - "dependencies": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decamelize-keys/node_modules/map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", @@ -5031,6 +4948,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -5180,6 +5098,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.8.0" @@ -5797,6 +5716,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6050,15 +5970,6 @@ "dev": true, "license": "MIT" }, - "node_modules/hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -6073,6 +5984,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -6137,6 +6049,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -6145,36 +6058,6 @@ "node": ">= 0.4" } }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/hosted-git-info/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -6249,15 +6132,6 @@ "node": ">=0.8.19" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -6313,6 +6187,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, "license": "MIT" }, "node_modules/is-bigint": { @@ -6378,6 +6253,7 @@ "version": "2.15.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -6520,15 +6396,6 @@ "node": ">=8" } }, - "node_modules/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", @@ -8596,6 +8463,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -8642,6 +8510,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -8688,15 +8557,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -8735,6 +8595,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, "license": "MIT" }, "node_modules/load-json-file": { @@ -8887,18 +8748,6 @@ "tmpl": "1.0.5" } }, - "node_modules/map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/marked": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", @@ -8921,44 +8770,6 @@ "node": ">= 0.10.0" } }, - "node_modules/meow": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", - "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", - "license": "MIT", - "dependencies": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize": "^1.2.0", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/type-fest": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -9000,15 +8811,6 @@ "node": ">=6" } }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -9022,20 +8824,6 @@ "node": "*" } }, - "node_modules/minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "license": "MIT", - "dependencies": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -9088,33 +8876,6 @@ "dev": true, "license": "MIT" }, - "node_modules/normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -9359,6 +9120,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -9388,6 +9150,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -9406,6 +9169,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9435,6 +9199,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -9475,6 +9240,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -9718,15 +9484,6 @@ ], "license": "MIT" }, - "node_modules/quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -9749,135 +9506,6 @@ "node": ">=4" } }, - "node_modules/read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "license": "MIT", - "dependencies": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "license": "ISC" - }, - "node_modules/read-pkg-up/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/read-pkg-up/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "license": "MIT", - "dependencies": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/read-pkg/node_modules/type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=8" - } - }, "node_modules/read-pkg/node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -9921,19 +9549,6 @@ "semver": "bin/semver" } }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "license": "MIT", - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -10044,6 +9659,7 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", @@ -10424,6 +10040,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "spdx-expression-parse": "^3.0.0", @@ -10434,12 +10051,14 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", @@ -10450,6 +10069,7 @@ "version": "3.0.18", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", + "dev": true, "license": "CC0-1.0" }, "node_modules/sprintf-js": { @@ -10684,18 +10304,6 @@ "node": ">=6" } }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "license": "MIT", - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -10713,6 +10321,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -10725,6 +10334,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10807,15 +10417,6 @@ "node": ">=8.0" } }, - "node_modules/trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -11485,6 +11086,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", @@ -11779,15 +11381,6 @@ "node": ">=12" } }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/yargs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", diff --git a/javascript/package.json b/javascript/package.json index 093f4ba..82d0eb7 100644 --- a/javascript/package.json +++ b/javascript/package.json @@ -40,8 +40,6 @@ "runTest": "never" }, "dependencies": { - "@here/flexpolyline": "^0.1.0", - "@mapbox/polyline": "^1.2.1", "@types/geojson": "^7946.0.10" }, "devDependencies": { diff --git a/javascript/src/__tests__/index.test.ts b/javascript/src/__tests__/index.test.ts index fdf8d42..e2bc8a0 100644 --- a/javascript/src/__tests__/index.test.ts +++ b/javascript/src/__tests__/index.test.ts @@ -164,6 +164,11 @@ describe.each([ }).toThrow(Error); }); + it("encodeFromLngLatArray produces empty results for empty inputs", () => { + const encodedLine = polyline.encodeFromLngLatArray([]); + expect(encodedLine).toEqual(""); + }); + // Validate basic output checks for decoded data that validate for the requested geometry type it("decodeToLineString throws an error if the input has < 2 positions", () => { @@ -447,6 +452,7 @@ describe("Decoding invalid data throws an error", () => { ["longitude too high", "BGg0lxJg0rn5K_zlxJ_zrn5K"], // [[181, 5], [0, 0]] ["latitude too low", "BG_rmytFg0lxJgsmytF_zlxJ"], // [[5, -91], [0, 0]] ["latitude too high", "BGgsmytFg0lxJ_rmytF_zlxJ"], // [[5, 91], [0, 0]] + ["invalid header version", "CGgsmytFg0lxJ_rmytF_zlxJ"], // Header version != 1 ])( "FlexiblePolyline: Decoding throws an error with invalid coordinates: %s", (invalidMsg: string, encodedLine: string) => { @@ -502,6 +508,7 @@ describe("Decoding 3D data with FlexiblePolyline produces the expected results", // Test encoder/decoder with 3D data describe.each([ + ["level", polyline.ThirdDimension.Level], ["altitude", polyline.ThirdDimension.Altitude], ["elevation", polyline.ThirdDimension.Elevation], ])( diff --git a/javascript/src/algorithm/decoder.ts b/javascript/src/algorithm/decoder.ts new file mode 100644 index 0000000..95b9fb0 --- /dev/null +++ b/javascript/src/algorithm/decoder.ts @@ -0,0 +1,194 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +// This class implements both the Encoded Polyline Algorithm Format +// (https://developers.google.com/maps/documentation/utilities/polylinealgorithm) +// and the Flexible-Polyline variation of the algorithm (https://github.com/heremaps/flexible-polyline). + +// This implementation has two differences to improve usability: +// - It uses well-defined rounding to ensure deterministic results across all programming languages. +// - It caps the max encoding/decoding precision to 11 decimal places (1 micrometer), because 15 places will +// lose precision when using 64-bit floating-point numbers. + +import { + CompressionParameters, + FlexiblePolylineFormatVersion, + ThirdDimension, +} from "../polyline-types"; + +export class PolylineDecoder { + // decodingTable is a lookup table that converts ASCII values from 0x00-0x7F + // to the appropriate decoded 0x00-0x3F value. Polyline and Flexible-Polyline + // use different character encodings, so they need different decoding tables. + readonly decodingTable: number[]; + + // containsHeader is true if the format includes a header (Flexible-Polyline), + // and false if it doesn't (Polyline). + readonly containsHeader: boolean; + + constructor(decodingTable: number[], containsHeader: boolean) { + this.decodingTable = decodingTable; + this.containsHeader = containsHeader; + } + + // Given an encoded string and a starting index, this decodes a single encoded signed value. + // The decoded value will be an integer that still needs the decimal place moved over based + // on the number of digits of encoded precision. + private decodeSignedValue( + encoded: string, + startIndex: number, + ): [result: number, nextIndex: number] { + // decode an unsigned value + const [unsignedValue, nextIndex] = this.decodeUnsignedValue( + encoded, + startIndex, + ); + // If the unsigned value has a 1 encoded in its least significant bit, + // it's negative, so flip the bits. + let signedValue = unsignedValue; + if (unsignedValue & 1) { + signedValue = ~signedValue; + } + // Shift the result by one to remove the encoded sign bit. + signedValue >>= 1; + return [signedValue, nextIndex]; + } + + // Given an encoded string and a starting index, this decodes a single encoded + // unsigned value. The flexible-polyline algorithm uses this directly to decode + // the header bytes, since those are encoded without the sign bit as the header + // values are known to be unsigned (which saves 2 bits). + private decodeUnsignedValue( + encoded: string, + startIndex: number, + ): [result: number, nextIndex: number] { + let result = 0; + let shift = 0; + let index = startIndex; + + // For each ASCII character, get the 6-bit (0x00 - 0x3F) value that + // it represents. Shift the accumulated result by 5 bits, add the new + // 5-bit chunk to the bottom, and keep going for as long as the 6th bit + // is set. + while (index < encoded.length) { + const charCode = encoded.charCodeAt(index); + const value = this.decodingTable[charCode]; + if (value < 0) { + throw Error( + `Invalid input. Encoded character '${charCode}' doesn't exist in the decoding table.`, + ); + } + result |= (value & 0x1f) << shift; + shift += 5; + index++; + + // We've reached the final 5-bit chunk for this value, so return. + // We also return the index, which represents the starting index of the + // next value to decode. + if ((value & 0x20) === 0) { + return [result, index]; + } + } + + // If we've run out of encoded characters without finding an empty 6th bit, + // something has gone wrong. + throw Error( + "Invalid encoding - last block contained an extra 0x20 'continue' bit.", + ); + } + + private decodeHeader( + encoded: string, + ): [header: CompressionParameters, index: number] { + // If the data has a header, the first value is expected to be the header version + // and the second value is compressed metadata containing precision and dimension information. + const [headerVersion, metadataIndex] = this.decodeUnsignedValue(encoded, 0); + if (headerVersion !== FlexiblePolylineFormatVersion) { + throw new Error("Invalid format version"); + } + const [metadata, nextIndex] = this.decodeUnsignedValue( + encoded, + metadataIndex, + ); + return [ + { + precisionLngLat: metadata & 0x0f, + thirdDimension: (metadata >> 4) & 0x07, + precisionThirdDimension: (metadata >> 7) & 0x0f, + }, + nextIndex, + ]; + } + + decode( + encoded: string, + encodePrecision: number = 0, + ): [lngLatArray: Array>, header: CompressionParameters] { + // If the data doesn't have a header, default to the passed-in precision and no 3rd dimension. + let header: CompressionParameters = { + precisionLngLat: encodePrecision, + thirdDimension: ThirdDimension.None, + precisionThirdDimension: 0, + }; + + // Track the index of the next character to decode from the encoded string. + let index = 0; + + if (this.containsHeader) { + [header, index] = this.decodeHeader(encoded); + } + + const numDimensions = header.thirdDimension ? 3 : 2; + const outputLngLatArray: Array> = []; + + // The data either contains lat/lng or lat/lng/z values that will be decoded. + // precisionDivisors are the divisors needed to convert the values from integers + // back to floating-point. + const precisionDivisors = [ + 10 ** header.precisionLngLat, + 10 ** header.precisionLngLat, + 10 ** header.precisionThirdDimension, + ]; + + // maxAllowedValues are the maximum absolute values allowed for lat/lng/z. This is used for + // error-checking the coordinate values as they're being decoded. + const maxAllowedValues = [90, 180, Infinity]; + + // While decoding, we want to switch from lat/lng/z to lng/lat/z, so this index tells us + // what position to put the dimension in for the resulting coordinate. + const resultDimensionIndex = [1, 0, 2]; + + // Decoded values are deltas from the previous coordinate values, so track the previous values. + const lastScaledCoordinate = [0, 0, 0]; + + // Keep decoding until we reach the end of the string. + while (index < encoded.length) { + // Each time through the loop we'll decode one full coordinate. + const coordinate: number[] = []; + let deltaValue = 0; + + // Decode each dimension for the coordinate. + for (let dimension = 0; dimension < numDimensions; dimension += 1) { + if (index >= encoded.length) { + throw Error("Encoding unexpectedly ended early."); + } + + [deltaValue, index] = this.decodeSignedValue(encoded, index); + lastScaledCoordinate[dimension] += deltaValue; + // Get the final lat/lng/z value by scaling the integer back down based on the number of + // digits of precision. + const value = + lastScaledCoordinate[dimension] / precisionDivisors[dimension]; + if (Math.abs(value) > maxAllowedValues[dimension]) { + throw Error( + `Invalid input. Compressed data contains invalid coordinate value: ${value}`, + ); + } + coordinate[resultDimensionIndex[dimension]] = value; + } + outputLngLatArray.push(coordinate); + } + + return [outputLngLatArray, header]; + } +} diff --git a/javascript/src/algorithm/encoder.ts b/javascript/src/algorithm/encoder.ts new file mode 100644 index 0000000..10052cf --- /dev/null +++ b/javascript/src/algorithm/encoder.ts @@ -0,0 +1,174 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +// This class implements both the Encoded Polyline Algorithm Format +// (https://developers.google.com/maps/documentation/utilities/polylinealgorithm) +// and the Flexible-Polyline variation of the algorithm (https://github.com/heremaps/flexible-polyline). + +// This implementation has two differences to improve usability: +// - It uses well-defined rounding to ensure deterministic results across all programming languages. +// The Flexible-Polyline algorithm definition says to use the rounding rules of the programming +// language, but this can cause inconsistent rounding depending on what language happens to be used +// on both the encoding and decoding sides. +// - It caps the max encoding/decoding precision to 11 decimal places (1 micrometer), because 12+ places can +// lose precision when using 64-bit floating-point numbers to store integers. + +import { + FlexiblePolylineFormatVersion, + ThirdDimension, +} from "../polyline-types"; + +export class PolylineEncoder { + // encodingTable is a lookup table that converts values from 0x00-0x3F + // to the appropriate encoded ASCII character. Polyline and Flexible-Polyline + // use different character encodings. + readonly encodingTable: string; + + // includeHeader is true if the format includes a header (Flexible-Polyline), + // and false if it doesn't (Polyline). + readonly includeHeader: boolean; + + constructor(encodingTable: string, includeHeader: boolean) { + this.encodingTable = encodingTable; + this.includeHeader = includeHeader; + } + + // The original polyline algorithm supposedly uses "round to nearest, ties away from 0" + // for its rounding rule. Flexible-polyline uses the rounding rules of the implementing + // language. Our generalized implementation will use the "round to nearest, ties away from 0" + // rule for all languages to keep the encoding deterministic across implementations. + private polylineRound(value: number): number { + return Math.sign(value) * Math.floor(Math.abs(value) + 0.5); + } + + encode( + lngLatArray: Array>, + precision: number, + thirdDim: number = ThirdDimension.None, + thirdDimPrecision: number = 0, + ): string { + if (precision < 0 || precision > 11) { + throw Error( + "Only precision values of 0-11 decimal digits are supported.", + ); + } + if (!Object.values(ThirdDimension).includes(+thirdDim)) { + throw Error("thirdDim is an invalid ThirdDimension value."); + } + if (thirdDimPrecision < 0 || thirdDimPrecision > 11) { + throw Error( + "Only thirdDimPrecision values of 0-11 decimal digits are supported.", + ); + } + + if (!lngLatArray.length) { + return ""; + } + + const numDimensions = thirdDim ? 3 : 2; + + // The data will either encode lat/lng or lat/lng/z values. + // precisionMultipliers are the multipliers needed to convert the values + // from floating-point to scaled integers. + const precisionMultipliers = [ + 10 ** precision, + 10 ** precision, + 10 ** thirdDimPrecision, + ]; + + // While encoding, we want to switch from lng/lat/z to lat/lng/z, so this index tells us + // what index to grab from the input coordinate when encoding each dimension. + const inputDimensionIndex = [1, 0, 2]; + + // maxAllowedValues are the maximum absolute values allowed for lat/lng/z. This is used for + // error-checking the coordinate values as they're being encoded. + const maxAllowedValues = [90, 180, Infinity]; + + // Encoded values are deltas from the previous coordinate values, so track the previous lat/lng/z values. + const lastScaledCoordinate = [0, 0, 0]; + + let output = ""; + + // Flexible-polyline starts with an encoded header that contains precision and dimension metadata. + if (this.includeHeader) { + output = this.encodeHeader(precision, thirdDim, thirdDimPrecision); + } + + lngLatArray.forEach((coordinate) => { + if (coordinate.length != numDimensions) { + throw Error( + "Invalid input. All coordinates need to have the same number of dimensions.", + ); + } + + for (let dimension = 0; dimension < numDimensions; dimension++) { + // Even though our input data is in lng/lat/z order, this is where we grab them in + // lat/lng/z order for encoding. + const inputValue = coordinate[inputDimensionIndex[dimension]]; + // While looping through, also verify the input data is valid + if (Math.abs(inputValue) > maxAllowedValues[dimension]) { + throw Error( + `Invalid input. Input coordinates must contain valid lng/lat coordinate data. Found ${coordinate}.`, + ); + } + // Scale the value based on the number of digits of precision, encode the delta between + // it and the previous value to the output, and track it as the previous value for encoding + // the next delta. + const scaledValue = this.polylineRound( + inputValue * precisionMultipliers[dimension], + ); + output += this.encodeSignedValue( + scaledValue - lastScaledCoordinate[dimension], + ); + lastScaledCoordinate[dimension] = scaledValue; + } + }); + + return output; + } + + private encodeHeader( + precision: number, + thirdDim: number, + thirdDimPrecision: number, + ): string { + // Combine all the metadata about the encoded data into a single value for the header. + const metadataValue = + (thirdDimPrecision << 7) | (thirdDim << 4) | precision; + return ( + this.encodeUnsignedValue(FlexiblePolylineFormatVersion) + + this.encodeUnsignedValue(metadataValue) + ); + } + + // Given a single input unsigned scaled value, this encodes into a series of + // ASCII characters. The flexible-polyline algorithm uses this directly to encode + // the header bytes, since those are known not to need a sign bit. + private encodeUnsignedValue(value: number): string { + let encodedString = ""; + let remainingValue = value; + // Loop through each 5-bit chunk in the value, add a 6th bit if there + // will be additional chunks, and encode to an ASCII value. + while (remainingValue > 0x1f) { + const chunk = (remainingValue & 0x1f) | 0x20; + encodedString += this.encodingTable[chunk]; + remainingValue >>= 5; + } + // For the last chunk, set the 6th bit to 0 (since there are no more chunks) and encode it. + return encodedString + this.encodingTable[remainingValue]; + } + + // Given a single input signed scaled value, this encodes into a series of + // ASCII characters. + private encodeSignedValue(value: number): string { + let unsignedValue = value; + // Shift the value over by 1 bit to make room for the sign bit at the end. + unsignedValue <<= 1; + // If the input value is negative, flip all the bits, including the sign bit. + if (value < 0) { + unsignedValue = ~unsignedValue; + } + + return this.encodeUnsignedValue(unsignedValue); + } +} diff --git a/javascript/src/algorithms/flexible-polyline.ts b/javascript/src/algorithms/flexible-polyline.ts deleted file mode 100644 index 5274960..0000000 --- a/javascript/src/algorithms/flexible-polyline.ts +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: MIT-0 - -import { - CompressionParameters, - DataCompressor, - ThirdDimension, -} from "../data-compressor"; -import { - ABSENT, - ALTITUDE, - ELEVATION, - decode as flexPolylineDecode, - encode as flexPolylineEncode, -} from "@here/flexpolyline"; - -// FlexiblePolyline encodes/decodes compressed data using the Flexible Polyline -// encoding ( https://github.com/heremaps/flexible-polyline ), which is a variant of -// the Encoded Polyline Algorithm Format. The algorithm handles both 2D and 3D data. -export class FlexiblePolyline extends DataCompressor { - supports3D(): boolean { - return true; - } - - encodeFromLatLngArray( - latLngArray: Array>, - parameters: CompressionParameters, - ): string { - // Validate parameters. - if (parameters.precisionLngLat < 0 || parameters.precisionLngLat > 15) { - throw new Error( - "Invalid CompressionParameters for FlexiblePolyline: precisionLngLat must be between 0 and 15.", - ); - } - if ( - parameters.precisionThirdDimension < 0 || - parameters.precisionThirdDimension > 15 - ) { - throw new Error( - "Invalid CompressionParameters for FlexiblePolyline: precisionThirdDimension must be between 0 and 15.", - ); - } - - // The underlying algorithm allows for more third dimension types than just Altitude and Elevation, but since - // those are the only acceptable types in the GeoJSON spec, that's all we'll support here. - switch (parameters.thirdDimension) { - case ThirdDimension.Altitude: - return flexPolylineEncode({ - polyline: latLngArray, - precision: parameters.precisionLngLat, - thirdDim: ALTITUDE, - thirdDimPrecision: parameters.precisionThirdDimension, - }); - case ThirdDimension.Elevation: - return flexPolylineEncode({ - polyline: latLngArray, - precision: parameters.precisionLngLat, - thirdDim: ELEVATION, - thirdDimPrecision: parameters.precisionThirdDimension, - }); - default: - return flexPolylineEncode({ - polyline: latLngArray, - precision: parameters.precisionLngLat, - }); - } - } - - decodeToLatLngArray( - polyline: string, - ): [Array>, CompressionParameters] { - const decodedLine = flexPolylineDecode(polyline); - let thirdDimension: ThirdDimension; - switch (decodedLine.thirdDim) { - case ALTITUDE: - thirdDimension = ThirdDimension.Altitude; - break; - case ELEVATION: - thirdDimension = ThirdDimension.Elevation; - break; - case ABSENT: - thirdDimension = ThirdDimension.None; - break; - default: - throw Error("Unsupported/invalid third dimension type."); - } - - return [ - decodedLine.polyline, - { - precisionLngLat: decodedLine.precision, - precisionThirdDimension: decodedLine.thirdDimPrecision, - thirdDimension: thirdDimension, - }, - ]; - } -} diff --git a/javascript/src/algorithms/polyline.ts b/javascript/src/algorithms/polyline.ts deleted file mode 100644 index c7f2656..0000000 --- a/javascript/src/algorithms/polyline.ts +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: MIT-0 - -import { CompressionParameters, DataCompressor } from "../data-compressor"; -import { - encode as polylineEncode, - decode as polylineDecode, -} from "@mapbox/polyline"; - -function validateInput(compressedData: string) { - // The compressed data input for Polyline5 / Polyline6 is expected to be base64-encoded into the - // ASCII range of 63-126. Verify that the input data falls within that range. - - for (let i = 0; i < compressedData.length; i++) { - const charCode = compressedData.charCodeAt(i); - if (charCode < 63 || charCode > 126) { - throw new Error( - `Invalid input. Compressed data must have ASCII values of 63-126. input[${i}] = '${compressedData.charAt(i)}' (ASCII ${charCode}).`, - ); - } - } -} - -// Polyline encodes/decodes compressed data using the Encoded Polyline Algorithm Format -// ( https://developers.google.com/maps/documentation/utilities/polylinealgorithm ). -// The algorithm only supports 2D data. -export class Polyline extends DataCompressor { - readonly precision: number; - constructor(precision: number) { - super(); - this.precision = precision; - } - - supports3D(): boolean { - return false; - } - - encodeFromLatLngArray( - latLngArray: Array>, - /* parameters: CompressionParameters, */ - ): string { - return polylineEncode(latLngArray, this.precision); - } - decodeToLatLngArray( - compressedData: string, - ): [Array>, CompressionParameters] { - validateInput(compressedData); - return [ - polylineDecode(compressedData, this.precision), - { precisionLngLat: this.precision }, - ]; - } -} - -// Polyline5 and Polyline6 encodes/decodes compressed data with 5 or 6 bits of precision respectively. -// While the underlying Polyline implementation allows for an arbitrary -// number of bits of precision to be encoded / decoded, location service providers seem -// to only choose 5 or 6 bits of precision, so those are the two algorithms that we'll explicitly offer here. - -export class Polyline5 extends Polyline { - constructor() { - super(5); - } -} - -export class Polyline6 extends Polyline { - constructor() { - super(6); - } -} diff --git a/javascript/src/compressors/flexible-polyline.ts b/javascript/src/compressors/flexible-polyline.ts new file mode 100644 index 0000000..f2770c5 --- /dev/null +++ b/javascript/src/compressors/flexible-polyline.ts @@ -0,0 +1,74 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +// This class implements the Flexible-Polyline variation of the +// Encoded Polyline algorithm (https://github.com/heremaps/flexible-polyline). +// The algorithm supports both 2D and 3D data. + +import { DataCompressor } from "../data-compressor"; +import { + CompressionParameters, + DefaultPrecision, + ThirdDimension, +} from "../polyline-types"; +import { PolylineEncoder } from "../algorithm/encoder"; +import { PolylineDecoder } from "../algorithm/decoder"; + +export class FlexiblePolyline extends DataCompressor { + readonly DataContainsHeader = true; + readonly FlexPolylineEncodingTable = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + // The lookup table contains conversion values for ASCII characters 0-127. + // Only the characters listed in the encoding table will contain valid + // decoding entries below. + readonly FlexPolylineDecodingTable = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, 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, + ]; + + readonly encoder = new PolylineEncoder( + this.FlexPolylineEncodingTable, + this.DataContainsHeader, + ); + readonly decoder = new PolylineDecoder( + this.FlexPolylineDecodingTable, + this.DataContainsHeader, + ); + + constructor() { + super(); + } + + compressLngLatArray( + lngLatArray: Array>, + parameters: CompressionParameters, + ): string { + // Set any parameters that weren't passed in to their default values. + const DefaultCompressionParameters = { + precisionLngLat: DefaultPrecision, + precisionThirdDimension: DefaultPrecision, + thirdDimension: ThirdDimension.None, + }; + const fullParameters = { ...DefaultCompressionParameters, ...parameters }; + + return this.encoder.encode( + lngLatArray, + fullParameters.precisionLngLat, + fullParameters.thirdDimension, + fullParameters.precisionThirdDimension, + ); + } + + decompressLngLatArray( + encodedData: string, + ): [Array>, CompressionParameters] { + const [lngLatArray, header] = this.decoder.decode(encodedData); + + return [lngLatArray, header]; + } +} diff --git a/javascript/src/compressors/polyline.ts b/javascript/src/compressors/polyline.ts new file mode 100644 index 0000000..d9b205f --- /dev/null +++ b/javascript/src/compressors/polyline.ts @@ -0,0 +1,83 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +// This class implements the Encoded Polyline Algorithm Format +// (https://developers.google.com/maps/documentation/utilities/polylinealgorithm). +// This algorithm is commonly used with either 5 or 6 bits of precision. +// To improve usability and decrease user error, we present Polyline5 and Polyline6 +// as two distinct compression algorithms. + +import { DataCompressor } from "../data-compressor"; +import { CompressionParameters } from "../polyline-types"; +import { PolylineEncoder } from "../algorithm/encoder"; +import { PolylineDecoder } from "../algorithm/decoder"; + +abstract class EncodedPolyline extends DataCompressor { + readonly precision: number; + + // The original Encoded Polyline algorithm doesn't support having a header on the encoded data. + readonly DataContainsHeader = false; + + readonly PolylineEncodingTable: string = + "?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"; + + // The lookup table contains conversion values for ASCII characters 0-127. + // Only the characters listed in the encoding table will contain valid + // decoding entries below. + readonly PolylineDecodingTable = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -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, 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, 52, + 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, -1, + ]; + readonly encoder = new PolylineEncoder( + this.PolylineEncodingTable, + this.DataContainsHeader, + ); + readonly decoder = new PolylineDecoder( + this.PolylineDecodingTable, + this.DataContainsHeader, + ); + + constructor(precision: number) { + super(); + this.precision = precision; + } + + compressLngLatArray( + lngLatArray: Array>, + /* parameters: CompressionParameters, */ + ): string { + return this.encoder.encode(lngLatArray, this.precision); + } + + decompressLngLatArray( + compressedData: string, + ): [Array>, CompressionParameters] { + const [lngLatArray, header] = this.decoder.decode( + compressedData, + this.precision, + ); + return [lngLatArray, { precisionLngLat: header.precisionLngLat }]; + } +} + +// Polyline5 and Polyline6 encodes/decodes compressed data with 5 or 6 bits of precision respectively. +// While the underlying Polyline implementation allows for an arbitrary +// number of bits of precision to be encoded / decoded, location service providers seem +// to only choose 5 or 6 bits of precision, so those are the two algorithms that we'll explicitly offer here. + +export class Polyline5 extends EncodedPolyline { + constructor() { + super(5); + } +} + +export class Polyline6 extends EncodedPolyline { + constructor() { + super(6); + } +} diff --git a/javascript/src/data-compressor.ts b/javascript/src/data-compressor.ts index 6c9cdfa..581473f 100644 --- a/javascript/src/data-compressor.ts +++ b/javascript/src/data-compressor.ts @@ -2,39 +2,7 @@ // SPDX-License-Identifier: MIT-0 import { LineString, Polygon, Feature, GeoJsonProperties } from "geojson"; - -/** Defines how to interpret a third dimension value if it exists. */ -export enum ThirdDimension { - /** No third dimension specified */ - None, - /** Third dimension is altitude (height above the Earth's surface) */ - Altitude, - /** Third dimension is elevation (height of the Earth's surface relative to the reference geoid) */ - Elevation, -} - -/** The optional set of parameters for encoding a set of LngLat coordinates. - * Currently, only the FlexiblePolyline algorithm supports these parameters. The Polyline5 / Polyline6 - * algorithms ignore them, as they don't support 3D data and we've defined them to use - * a fixed precision value. - */ -export type CompressionParameters = { - /** The number of decimal places of precision to use for compressing longitude and latitude. - */ - precisionLngLat?: number; - /** The number of decimal places of precision to use for compressing the third dimension of data. - */ - precisionThirdDimension?: number; - /** The type of third dimension data being encoded - none, altitude, or elevation. - */ - thirdDimension?: ThirdDimension; -}; - -const defaultCompressionParameters = { - precisionLngLat: 6, - precisionThirdDimension: 6, - thirdDimension: ThirdDimension.None, -}; +import { ThirdDimension, CompressionParameters } from "./polyline-types"; // DataCompressor is an abstract base class that defines the interface for // encoding/decoding compressed coordinate arrays. The coordinate arrays represent either @@ -47,24 +15,21 @@ const defaultCompressionParameters = { // These produce a GeoJSON Geometry object that can be manually assembled into a Feature to pass // into MapLibre as a geojson source. -export abstract class DataCompressor { - // True if the specific data compressor supports 3D data, false if it only supports 2D data. - protected abstract supports3D(): boolean; +// Concrete implementations of this class are expected to implement the following APIs: +// - compressLngLatArray(lngLatArray, compressionParameters) -> compressedData +// - decompressLngLatArray(compressedData) -> [lngLatArray, compressionParameters] - // Encode an array of LatLng data into a string of compressed data. The coordinates may optionally have a third - // dimension of data. - // All of the existing algorithms specifically encode from LatLng, which is why we've made that a part of - // API expectation here. - protected abstract encodeFromLatLngArray( - latLngArray: Array>, +export abstract class DataCompressor { + // Encode an array of LngLat data into a string of compressed data. + // The coordinates may optionally have a third dimension of data. + protected abstract compressLngLatArray( + lngLatArray: Array>, parameters: CompressionParameters, ): string; - // Decode a string of compressed data into an array of LatLng data. The coordinates may optionally have a third - // dimension of data. - // All of the existing algorithms specifically decode to LatLng, which is why we've made that a part of - // API expectation here. The results get flipped later into LngLat when assembling into GeoJSON. - protected abstract decodeToLatLngArray( + // Decode a string of compressed data into an array of LngLat data. + // The coordinates may optionally have a third dimension of data. + protected abstract decompressLngLatArray( compressedData: string, ): [Array>, CompressionParameters]; @@ -120,42 +85,11 @@ export abstract class DataCompressor { return true; } - // Helper method that performs the actual LngLat decompression. It also returns the set of compression parameters - // that were used so that we can include them as metadata when generating GeoJSON features. - private decodeLngLat( - compressedData: string, - ): [Array>, CompressionParameters] { - if (!compressedData) { - throw Error("Invalid input. No compressed data provided."); - } - - const [decodedLine, compressionParameters] = - this.decodeToLatLngArray(compressedData); - - // Change LatLng to LngLat by swapping the values for each coordinate entry. - for (const latLng of decodedLine) { - // While looping through, also verify that each latLng value is within valid ranges. - if ( - latLng.length < 2 || - Math.abs(latLng[0]) > 90 || - Math.abs(latLng[1]) > 180 - ) { - throw Error( - `Invalid input. Compressed data must contain valid lat/lng data. Found ${latLng}.`, - ); - } - - [latLng[0], latLng[1]] = [latLng[1], latLng[0]]; - } - - return [decodedLine, compressionParameters]; - } - private decodeLineString( compressedData: string, ): [LineString, CompressionParameters] { const [decodedLine, compressionParameters] = - this.decodeLngLat(compressedData); + this.decompressLngLatArray(compressedData); // Validate that the result is a valid GeoJSON LineString per the RFC 7946 GeoJSON spec: // "The 'coordinates' member is an array of two or more positions" if (decodedLine.length < 2) { @@ -179,7 +113,8 @@ export abstract class DataCompressor { let shouldBeCounterclockwise = true; // The first ring of a polygon should be counterclockwise let compressionParameters: CompressionParameters = {}; for (const ring of compressedData) { - const [decodedRing, ringCompressionParameters] = this.decodeLngLat(ring); + const [decodedRing, ringCompressionParameters] = + this.decompressLngLatArray(ring); // Validate that the result is a valid GeoJSON Polygon linear ring per the RFC 7946 GeoJSON spec. @@ -245,6 +180,12 @@ export abstract class DataCompressor { parameters: CompressionParameters, ): GeoJsonProperties { switch (parameters.thirdDimension) { + case ThirdDimension.Level: + return { + precision: parameters.precisionLngLat, + thirdDimensionPrecision: parameters.precisionThirdDimension, + thirdDimensionType: "level", + }; case ThirdDimension.Elevation: return { precision: parameters.precisionLngLat, @@ -265,55 +206,14 @@ export abstract class DataCompressor { } encodeFromLngLatArray( - lngLatCoords: Array>, + lngLatArray: Array>, parameters: CompressionParameters, ): string { - const fullParameters = { ...defaultCompressionParameters, ...parameters }; - - const is2DData = lngLatCoords[0].length === 2; - if (!is2DData && !this.supports3D()) { - throw Error( - "Invalid input. 3D data was provided but this data compressor does not support 3D data.", - ); - } - - // Flip lngLat to latLng for encoding and verify that all coordinates have the same number of dimensions. - const latLngCoords: Array> = []; - for (const coord of lngLatCoords) { - // While looping through, also verify that each lngLat value is within valid ranges. - if ( - coord.length < 2 || - Math.abs(coord[0]) > 180 || - Math.abs(coord[1]) > 90 - ) { - throw Error( - `Invalid input. Input coordinates must contain valid lng/lat data. Found ${coord}.`, - ); - } - if (coord.length === 2) { - if (!is2DData) { - throw Error( - "Invalid input. All coordinates need to have the same number of dimensions.", - ); - } - latLngCoords.push([coord[1], coord[0]]); - } else if (coord.length === 3) { - if (is2DData) { - throw Error( - "Invalid input. All coordinates need to have the same number of dimensions.", - ); - } - // If the input data has 3D data, preserve that in the data we're encoding. - latLngCoords.push([coord[1], coord[0], coord[2]]); - } else { - throw Error("Invalid input. Coordinates must have 2 or 3 dimensions."); - } - } - return this.encodeFromLatLngArray(latLngCoords, fullParameters); + return this.compressLngLatArray(lngLatArray, parameters); } decodeToLngLatArray(compressedData: string): Array> { - const [decodedLngLatArray] = this.decodeLngLat(compressedData); + const [decodedLngLatArray] = this.decompressLngLatArray(compressedData); return decodedLngLatArray; } diff --git a/javascript/src/index.ts b/javascript/src/index.ts index 8a3071b..2a980fc 100644 --- a/javascript/src/index.ts +++ b/javascript/src/index.ts @@ -1,36 +1,23 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT-0 +import { DataCompressor } from "./data-compressor"; +import { Polyline5, Polyline6 } from "./compressors/polyline"; +import { FlexiblePolyline } from "./compressors/flexible-polyline"; import { ThirdDimension, + CompressionAlgorithm, CompressionParameters, - DataCompressor, -} from "./data-compressor"; -import { FlexiblePolyline } from "./algorithms/flexible-polyline"; -import { Polyline5, Polyline6 } from "./algorithms/polyline"; +} from "./polyline-types"; import { LineString, Polygon, Feature } from "geojson"; -export { ThirdDimension, CompressionParameters }; +export { ThirdDimension, CompressionParameters, CompressionAlgorithm }; // The default algorithm is FlexiblePolyline. This was selected as it is the newest and most flexible format // of the different decoding types supported. let compressor: DataCompressor = new FlexiblePolyline(); -/** Defines the set of compression algorithms that are supported by this library. */ -export enum CompressionAlgorithm { - /** Encoder/decoder for the [Flexible Polyline](https://github.com/heremaps/flexible-polyline) format. */ - FlexiblePolyline, - /** Encoder/decoder for the [Encoded Polyline Algorithm Format](https://developers.google.com/maps/documentation/utilities/polylinealgorithm) - * with 5 bits of precision. - */ - Polyline5, - /** Encoder/decoder for the [Encoded Polyline Algorithm Format](https://developers.google.com/maps/documentation/utilities/polylinealgorithm) - * with 6 bits of precision. - */ - Polyline6, -} - /** Get the currently-selected compression algorithm. * @returns The current compression algorithm. */ diff --git a/javascript/src/polyline-types.ts b/javascript/src/polyline-types.ts new file mode 100644 index 0000000..467b1d9 --- /dev/null +++ b/javascript/src/polyline-types.ts @@ -0,0 +1,51 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +/** Defines the default encoding precision for coordinates */ +export const DefaultPrecision = 6; + +/** The version of flexible-polyline that's supported by this implementation */ +export const FlexiblePolylineFormatVersion = 1; + +/** Defines the set of compression algorithms that are supported by this library. */ +export enum CompressionAlgorithm { + /** Encoder/decoder for the [Flexible Polyline](https://github.com/heremaps/flexible-polyline) format. */ + FlexiblePolyline, + /** Encoder/decoder for the [Encoded Polyline Algorithm Format](https://developers.google.com/maps/documentation/utilities/polylinealgorithm) + * with 5 bits of precision. + */ + Polyline5, + /** Encoder/decoder for the [Encoded Polyline Algorithm Format](https://developers.google.com/maps/documentation/utilities/polylinealgorithm) + * with 6 bits of precision. + */ + Polyline6, +} + +/** Defines how to interpret a third dimension value if it exists. */ +export enum ThirdDimension { + /** No third dimension specified */ + None, + /** Third dimension is level */ + Level = 1, + /** Third dimension is altitude (height above the Earth's surface) */ + Altitude = 2, + /** Third dimension is elevation (height of the Earth's surface relative to the reference geoid) */ + Elevation = 3, +} + +/** The optional set of parameters for encoding a set of LngLat coordinates. + * Currently, only the FlexiblePolyline algorithm supports these parameters. The Polyline5 / Polyline6 + * algorithms ignore them, as they don't support 3D data and we've defined them to use + * a fixed precision value. + */ +export type CompressionParameters = { + /** The number of decimal places of precision to use for compressing longitude and latitude. + */ + precisionLngLat?: number; + /** The number of decimal places of precision to use for compressing the third dimension of data. + */ + precisionThirdDimension?: number; + /** The type of third dimension data being encoded - none, level, altitude, or elevation. + */ + thirdDimension?: ThirdDimension; +};