diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1adce3c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,24 @@ +## Quickstart + +To install and build `quadbin-js` locally from source: + +```bash +# install dependencies +yarn + +# build package once +yarn build +``` + +To run tests, coverage, or a linter, you should execute `yarn build`, and afterward: + +```bash +# run tests once +yarn test +``` + +## Releases + +1. Create a new version: `yarn version [ major | minor | patch | prerelease ]` + +2. Execute `yarn publish` \ No newline at end of file diff --git a/README.md b/README.md index 988dc78..dd551d0 100644 --- a/README.md +++ b/README.md @@ -79,4 +79,34 @@ Converts quadbin cell into a xyz tile. function geometryToCells(geometry: GeoJSONGeometry, resolution: bigint): bigint ``` -Returns a list of cells covering a GeoJSON geometry at a given resolution +## cellToBoundary + +```javascript +function cellToBoundary(quadbin: Quadbin): Polygon +``` + +Converts a Quadbin cell identifier into a geographical boundary represented as a polygon + +## cellToOffset + +```javascript +function cellToOffset(quadbin: Quadbin): [number, number, number] +``` + +Converts a Quadbin cell identifier into world coordinates offset values + +## cellToWorldBounds + +```javascript +function cellToWorldBounds(quadbin: Quadbin, coverage: number): [number[], number[]] +``` + +Computes the world bounds (in Web Mercator coordinates) for a given Quadbin cell, taking into account the cell's coverage area + +## getCellPolygon + +```javascript +function getCellPolygon(quadbin: Quadbin, coverage = 1): number[] +``` + +Generates the geographical polygon (in longitude and latitude) that represents the boundaries of a Quadbin cell, optionally taking into account coverage diff --git a/package.json b/package.json index 0efca43..7bcf1b1 100644 --- a/package.json +++ b/package.json @@ -32,20 +32,23 @@ "build:esm": "tsc -p tsconfig/tsconfig.esm.json", "build:types": "tsc -p tsconfig/tsconfig.types.json", "build:umd": "webpack --config tsconfig/webpack.config.cjs", - "lint": "npx prettier --check src", + "lint": "prettier --check src", "test": "yarn lint && yarn test-fast", - "test-fast": "npx ts-node node_modules/tape/bin/tape test/**/*.spec.js" + "test-fast": "ts-node node_modules/tape/bin/tape test/**/*.spec.js", + "prepublishOnly": "yarn build" }, "browser": { "jsdom": false }, "devDependencies": { "@babel/register": "^7.13.0", + "@types/geojson": "^7946.0.14", "babel-loader": "^8.0.0", "babel-preset-minify": "^0.5.0", "prettier": "^2.4.1", "tape": "^5.3.0", "ts-loader": "^9.2.5", + "ts-node": "^10.9.2", "typescript": "^4.4.4", "webpack": "^5.52.1", "webpack-cli": "^4.8.0" @@ -54,6 +57,12 @@ "node": ">=14" }, "dependencies": { - "@mapbox/tile-cover": "3.0.1" + "@mapbox/tile-cover": "3.0.1", + "@math.gl/web-mercator": "^4.1.0" + }, + "packageManager": "yarn@1.22.22", + "volta": { + "node": "14.21.3", + "yarn": "1.22.22" } } diff --git a/src/index.ts b/src/index.ts index a7caf40..561967c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,6 @@ import {tiles} from '@mapbox/tile-cover'; +import {worldToLngLat} from '@math.gl/web-mercator'; +import type {Polygon} from 'geojson'; const B = [ 0x5555555555555555n, @@ -13,6 +15,29 @@ const S = [0n, 1n, 2n, 4n, 8n, 16n]; type Quadbin = bigint; type Tile = {x: number; y: number; z: number}; +const TILE_SIZE = 512; + +export function cellToOffset(quadbin: Quadbin): [number, number, number] { + const {x, y, z} = cellToTile(quadbin); + const scale = TILE_SIZE / (1 << z); + return [x * scale, TILE_SIZE - y * scale, scale]; +} + +export function cellToWorldBounds(quadbin: Quadbin, coverage: number): [number[], number[]] { + const [xOffset, yOffset, scale] = cellToOffset(quadbin); + return [ + [xOffset, yOffset], + [xOffset + coverage * scale, yOffset - coverage * scale] + ]; +} + +export function getCellPolygon(quadbin: Quadbin, coverage = 1): number[] { + const [topLeft, bottomRight] = cellToWorldBounds(quadbin, coverage); + const [w, n] = worldToLngLat(topLeft); + const [e, s] = worldToLngLat(bottomRight); + return [e, n, e, s, w, s, w, n, e, n]; +} + export function hexToBigInt(hex: string): bigint { return BigInt(`0x${hex}`); } @@ -89,3 +114,16 @@ export function geometryToCells(geometry, resolution: bigint): Quadbin[] { max_zoom: zoom }).map(([x, y, z]) => tileToCell({x, y, z})); } + +export function cellToBoundary(cell: Quadbin): Polygon { + const bbox = getCellPolygon(cell); + const boundary = [ + [bbox[0], bbox[1]], + [bbox[2], bbox[3]], + [bbox[4], bbox[5]], + [bbox[6], bbox[7]], + [bbox[0], bbox[1]] + ]; + + return {type: 'Polygon', coordinates: [boundary]}; +} diff --git a/test/index.spec.js b/test/index.spec.js index c722c12..68d9a55 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -5,7 +5,7 @@ import { cellToParent, geometryToCells, getResolution, - hexToBigInt + cellToBoundary } from 'quadbin'; import {tileToQuadkey} from './quadkey-utils.js'; @@ -16,6 +16,8 @@ const TEST_TILES = [ {x: 1023, y: 2412, z: 23, q: 5291729562728627583n} ]; +const ANY_QUADBIN = BigInt(524800); + test('Quadbin conversion', async t => { for (const {x, y, z, q} of TEST_TILES) { const tile = {x, y, z}; @@ -51,7 +53,6 @@ test('Quadbin getParent', async t => { import PointGeometry from './data/PointGeometry.json' assert {type: 'json'}; import MultiPointGeometry from './data/MultiPointGeometry.json' assert {type: 'json'}; import LineStringGeometry from './data/LineStringGeometry.json' assert {type: 'json'}; -import MultiLineStringGeometry from './data/MultiLineStringGeometry.json' assert {type: 'json'}; import PolygonGeometry from './data/PolygonGeometry.json' assert {type: 'json'}; import PolygonAntimeridianGeometry from './data/PolygonAntimeridianGeometry.json' assert {type: 'json'}; import MultiPolygonGeometry from './data/MultiPolygonGeometry.json' assert {type: 'json'}; @@ -78,3 +79,58 @@ test('Quadbin geometryToCells', async t => { } t.end(); }); + +test('Quadbin cellToBoundary', t => { + for (const {quadbin, expectedPolygon} of [ + { + quadbin: BigInt(524800), + expectedPolygon: { + type: 'Polygon', + coordinates: [ + [ + [180, 85.0511287798066], + [180, -85.05112877980659], + [-180, -85.05112877980659], + [-180, 85.0511287798066], + [180, 85.0511287798066] + ] + ] + } + }, + { + quadbin: BigInt(536903670), // Longitude near +180° + expectedPolygon: { + type: 'Polygon', + coordinates: [ + [ + [180, 85.0511287798066], + [180, -85.05112877980659], + [-180, -85.05112877980659], + [-180, 85.0511287798066], + [180, 85.0511287798066] + ] + ] + } + }, + { + quadbin: BigInt(536870921), // Longitude near -180° + expectedPolygon: { + type: 'Polygon', + coordinates: [ + [ + [180, 85.0511287798066], + [180, -85.05112877980659], + [-180, -85.05112877980659], + [-180, 85.0511287798066], + [180, 85.0511287798066] + ] + ] + } + } + ]) { + const result = cellToBoundary(quadbin); + t.deepEquals(result, expectedPolygon); + } + + t.end(); +}); diff --git a/yarn.lock b/yarn.lock index df90cda..98fa8e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13,6 +13,13 @@ pirates "^4.0.5" source-map-support "^0.5.16" +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@discoveryjs/json-ext@^0.5.0": version "0.5.7" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" @@ -50,6 +57,14 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping@^0.3.14", "@jridgewell/trace-mapping@^0.3.9": version "0.3.15" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774" @@ -79,6 +94,45 @@ dependencies: tilebelt "^1.0.1" +"@math.gl/core@4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@math.gl/core/-/core-4.1.0.tgz#2f4a1644c6f8fb50aacae57a02f1297f933aefbd" + integrity sha512-FrdHBCVG3QdrworwrUSzXIaK+/9OCRLscxI2OUy6sLOHyHgBMyfnEGs99/m3KNvs+95BsnQLWklVfpKfQzfwKA== + dependencies: + "@math.gl/types" "4.1.0" + +"@math.gl/types@4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@math.gl/types/-/types-4.1.0.tgz#ce28c06bcfe07d21311e00aeb25de82fecf7f393" + integrity sha512-clYZdHcmRvMzVK5fjeDkQlHUzXQSNdZ7s4xOqC3nJPgz4C/TZkUecTo9YS4PruZqtDda/ag4erndP0MIn40dGA== + +"@math.gl/web-mercator@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@math.gl/web-mercator/-/web-mercator-4.1.0.tgz#b244112b2805ba68cdecc76f3d12578d05271a1d" + integrity sha512-HZo3vO5GCMkXJThxRJ5/QYUYRr3XumfT8CzNNCwoJfinxy5NtKUd7dusNTXn7yJ40UoB8FMIwkVwNlqaiRZZAw== + dependencies: + "@math.gl/core" "4.1.0" + +"@tsconfig/node10@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" + integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + "@types/eslint-scope@^3.7.3": version "3.7.4" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" @@ -105,6 +159,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== +"@types/geojson@^7946.0.14": + version "7946.0.14" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.14.tgz#319b63ad6df705ee2a65a73ef042c8271e696613" + integrity sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg== + "@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" @@ -268,6 +327,18 @@ acorn-import-assertions@^1.7.6: resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== +acorn-walk@^8.1.1: + version "8.3.4" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" + integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== + dependencies: + acorn "^8.11.0" + +acorn@^8.11.0, acorn@^8.4.1: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + acorn@^8.5.0, acorn@^8.7.1: version "8.8.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" @@ -295,6 +366,11 @@ ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + array-buffer-byte-length@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead" @@ -661,6 +737,11 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -725,6 +806,11 @@ defined@^1.0.1: resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.1.tgz#c0b9db27bfaffd95d6f61399419b893df0f91ebf" integrity sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q== +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + dotignore@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/dotignore/-/dotignore-0.1.2.tgz#f942f2200d28c3a76fbdd6f0ee9f3257c8a2e905" @@ -1389,6 +1475,11 @@ make-dir@^3.0.2, make-dir@^3.1.0: dependencies: semver "^6.0.0" +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -1892,6 +1983,25 @@ ts-loader@^9.2.5: micromatch "^4.0.0" semver "^7.3.4" +ts-node@^10.9.2: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + typed-array-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz#18de3e7ed7974b0a729d3feecb94338d1472cd60" @@ -1961,6 +2071,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + watchpack@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" @@ -2083,3 +2198,8 @@ yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==