├── .dockerignore ├── .gitignore ├── .gitmodules ├── docker ├── Dockerfile.node18.test ├── Dockerfile.node16.test └── Dockerfile.node20.test ├── test.sh ├── index.js ├── publish-osx.sh ├── publish-linux.sh ├── src ├── loop.h ├── polygon.h ├── point.h ├── cell.h ├── s2.cc ├── builder.h ├── latlng.h ├── cell_union.h ├── polyline.h ├── earth.h ├── polygon.cc ├── cell_id.h ├── loop.cc ├── region_coverer.h ├── cell.cc ├── point.cc ├── builder.cc ├── latlng.cc ├── earth.cc ├── polyline.cc ├── cell_id.cc ├── cell_union.cc └── region_coverer.cc ├── test ├── Point.test.js ├── LatLng.test.js ├── Cell.test.js ├── Earth.test.js ├── Polyline.test.js ├── CellId.test.js ├── CellUnion.test.js └── RegionCoverer.test.js ├── CHANGELOG.md ├── .circleci └── config.yml ├── package.json ├── index.d.ts ├── README.md ├── jest.config.js ├── binding.gyp └── LICENSE /.dockerignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | .vscode/ 4 | radarlabs-s2-0.0.1.tgz 5 | *.log 6 | *.txt 7 | lib 8 | *.swp 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "third_party/s2geometry"] 2 | path = third_party/s2geometry 3 | url = https://github.com/google/s2geometry 4 | -------------------------------------------------------------------------------- /docker/Dockerfile.node18.test: -------------------------------------------------------------------------------- 1 | FROM node:18.12.1 2 | 3 | WORKDIR /app 4 | COPY . /app 5 | 6 | RUN npm install 7 | RUN JOBS=max PATH=$(npm bin):$PATH node-pre-gyp rebuild 8 | CMD npm run test -------------------------------------------------------------------------------- /docker/Dockerfile.node16.test: -------------------------------------------------------------------------------- 1 | FROM node:16.5.0 2 | 3 | WORKDIR /app 4 | COPY . /app 5 | 6 | RUN npm install 7 | RUN JOBS=max PATH=$(npm bin):$PATH node-pre-gyp rebuild 8 | CMD npm run test 9 | -------------------------------------------------------------------------------- /docker/Dockerfile.node20.test: -------------------------------------------------------------------------------- 1 | FROM node:20.0.0 2 | 3 | WORKDIR /app 4 | COPY . /app 5 | 6 | RUN npm install 7 | RUN JOBS=max PATH=$(npm bin):$PATH node-pre-gyp rebuild 8 | CMD npm run test 9 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | git submodule init 6 | git submodule sync 7 | git submodule update 8 | 9 | for node in 16 18 20 10 | do 11 | # build image 12 | docker build -t test$node -f ./docker/Dockerfile.node$node.test . 13 | 14 | # run image 15 | docker run test$node:latest 16 | done -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // magic incantation from step 3 @ https://github.com/mapbox/node-pre-gyp#readme 2 | const binary = require('@mapbox/node-pre-gyp'); 3 | const path = require('path'); 4 | var binding_path = binary.find(path.resolve(path.join(__dirname,'./package.json'))); 5 | const s2 = require(binding_path); 6 | module.exports = s2; 7 | -------------------------------------------------------------------------------- /publish-osx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # to publish x86_64, run with arch -x86_64 4 | 5 | set -e 6 | 7 | # source ~/.zshrc 8 | 9 | # loop through node LTS versions 16 - 20, unpublish and publish them 10 | for node in v16 v18 v20 11 | do 12 | nvm install $node 13 | nvm use $node 14 | rm -rf node_modules 15 | npm install 16 | JOBS=max npx node-pre-gyp build package unpublish publish 17 | done 18 | -------------------------------------------------------------------------------- /publish-linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | export DOCKER_DEFAULT_PLATFORM="${DOCKER_DEFAULT_PLATFORM:-linux/amd64}" 6 | 7 | for node in 20.0.0 18.12.1 16.5.0 8 | do 9 | # run image 10 | docker run -it \ 11 | -v "$PWD":/app \ 12 | -e AWS_DEFAULT_REGION \ 13 | -e AWS_ACCESS_KEY_ID \ 14 | -e AWS_SECRET_ACCESS_KEY \ 15 | -e AWS_SESSION_TOKEN \ 16 | node:$node \ 17 | bash -c 'cd /app && npm install && JOBS=max npx node-pre-gyp build package unpublish publish' 18 | done -------------------------------------------------------------------------------- /src/loop.h: -------------------------------------------------------------------------------- 1 | #ifndef RADAR_LOOP 2 | #define RADAR_LOOP 3 | 4 | #include 5 | #include "latlng.h" 6 | #include "s2/s2loop.h" 7 | #include "s2/s2error.h" 8 | #include "s2/base/stringprintf.h" 9 | 10 | class Loop : public Napi::ObjectWrap { 11 | 12 | public: 13 | static Napi::FunctionReference constructor; 14 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 15 | 16 | Loop(const Napi::CallbackInfo& info); 17 | 18 | std::shared_ptr s2loop; 19 | }; 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /src/polygon.h: -------------------------------------------------------------------------------- 1 | #ifndef RADAR_POLYGON 2 | #define RADAR_POLYGON 3 | 4 | #include 5 | #include "s2/s2polygon.h" 6 | #include "s2/s2error.h" 7 | #include "s2/base/stringprintf.h" 8 | 9 | class Polygon : public Napi::ObjectWrap { 10 | 11 | public: 12 | static Napi::FunctionReference constructor; 13 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 14 | 15 | Polygon(const Napi::CallbackInfo& info); 16 | 17 | std::shared_ptr s2polygon; 18 | }; 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /test/Point.test.js: -------------------------------------------------------------------------------- 1 | // magic incantation from step 3 @ https://github.com/mapbox/node-pre-gyp#readme 2 | const binary = require('@mapbox/node-pre-gyp'); 3 | const path = require('path'); 4 | var binding_path = binary.find(path.resolve('./package.json')); 5 | const s2 = require(binding_path); 6 | 7 | test("Point#constructor accepts x,y,z values", () => { 8 | const [x, y, z] = [-0.6193073896908822, 0.5249960533039503, 0.5838128990434704]; 9 | const point = new s2.Point(x,y,z); 10 | 11 | expect(point.x()).toEqual(x); 12 | expect(point.y()).toEqual(y); 13 | expect(point.z()).toEqual(z); 14 | }); 15 | -------------------------------------------------------------------------------- /src/point.h: -------------------------------------------------------------------------------- 1 | #ifndef RADAR_POINT 2 | #define RADAR_POINT 3 | 4 | #include 5 | #include 6 | #include "s2/s2point.h" 7 | 8 | class Point : public Napi::ObjectWrap { 9 | public: 10 | Point(const Napi::CallbackInfo& info); 11 | S2Point Get(); 12 | 13 | static Napi::FunctionReference constructor; 14 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 15 | 16 | private: 17 | Napi::Value X(const Napi::CallbackInfo &info); 18 | Napi::Value Y(const Napi::CallbackInfo &info); 19 | Napi::Value Z(const Napi::CallbackInfo &info); 20 | S2Point s2Point; 21 | }; 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /src/cell.h: -------------------------------------------------------------------------------- 1 | #ifndef RADAR_CELL 2 | #define RADAR_CELL 3 | 4 | #include 5 | #include 6 | 7 | #include "cell_id.h" 8 | #include "latlng.h" 9 | #include "point.h" 10 | #include "s2/s2cell.h" 11 | 12 | class Cell : public Napi::ObjectWrap { 13 | public: 14 | Cell(const Napi::CallbackInfo& info); 15 | S2Cell Get(); 16 | 17 | static Napi::FunctionReference constructor; 18 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 19 | 20 | private: 21 | Napi::Value GetVertex(const Napi::CallbackInfo &info); 22 | Napi::Value GetCenter(const Napi::CallbackInfo &info); 23 | S2Cell s2Cell; 24 | }; 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /src/s2.cc: -------------------------------------------------------------------------------- 1 | #include "builder.h" 2 | #include "cell.h" 3 | #include "cell_id.h" 4 | #include "cell_union.h" 5 | #include "earth.h" 6 | #include "latlng.h" 7 | #include "loop.h" 8 | #include "point.h" 9 | #include "polygon.h" 10 | #include "polyline.h" 11 | #include "region_coverer.h" 12 | 13 | Napi::Object InitAll(Napi::Env env, Napi::Object exports) { 14 | Builder::Init(env, exports); 15 | Cell::Init(env, exports); 16 | CellId::Init(env, exports); 17 | Earth::Init(env, exports); 18 | LatLng::Init(env, exports); 19 | Loop::Init(env, exports); 20 | Point::Init(env, exports); 21 | Polygon::Init(env, exports); 22 | Polyline::Init(env, exports); 23 | CellUnion::Init(env, exports); 24 | return RegionCoverer::Init(env, exports); 25 | } 26 | 27 | NODE_API_MODULE(NODE_GYP_MODULE_NAME, InitAll) 28 | -------------------------------------------------------------------------------- /src/builder.h: -------------------------------------------------------------------------------- 1 | #ifndef RADAR_BUILDER 2 | #define RADAR_BUILDER 3 | 4 | #include 5 | #include "loop.h" 6 | #include "polygon.h" 7 | #include "s2/s2builder.h" 8 | #include "s2/s2builderutil_s2polygon_layer.h" 9 | #include "s2/s2polygon.h" 10 | #include "s2/third_party/absl/memory/memory.h" 11 | 12 | class Builder : public Napi::ObjectWrap { 13 | 14 | public: 15 | Builder(const Napi::CallbackInfo& info); 16 | 17 | static Napi::FunctionReference constructor; 18 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 19 | 20 | private: 21 | std::unique_ptr s2builder; 22 | std::unique_ptr s2builderOptions; 23 | std::unique_ptr output; 24 | 25 | Napi::Value AddLoop(const Napi::CallbackInfo &info); 26 | Napi::Value Build(const Napi::CallbackInfo &info); 27 | }; 28 | 29 | #endif 30 | -------------------------------------------------------------------------------- /src/latlng.h: -------------------------------------------------------------------------------- 1 | #ifndef RADAR_LATLNG 2 | #define RADAR_LATLNG 3 | 4 | #include 5 | #include "point.h" 6 | #include "s2/s2latlng.h" 7 | 8 | class LatLng : public Napi::ObjectWrap { 9 | 10 | public: 11 | LatLng(const Napi::CallbackInfo& info); 12 | 13 | static Napi::FunctionReference constructor; 14 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 15 | static Napi::Object NewInstance(Napi::Value lat, Napi::Value lng); 16 | 17 | S2LatLng Get(); 18 | 19 | private: 20 | Napi::Value ToString(const Napi::CallbackInfo &info); 21 | Napi::Value Normalized(const Napi::CallbackInfo &info); 22 | Napi::Value Latitude(const Napi::CallbackInfo &info); 23 | Napi::Value Longitude(const Napi::CallbackInfo &info); 24 | Napi::Value ApproxEquals(const Napi::CallbackInfo &info); 25 | S2LatLng s2latlng; 26 | }; 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Version 0.0.5 2 | 3 | - Support Node 18 & 20, minimum tested version is now 16 4 | - Update node-pre-gyp, node-addon-api, and node-gyp 5 | 6 | ## Version 0.0.4 7 | 8 | - Added CellId.rangeMin & CellId.rangeMax (thanks to @mast) 9 | - Only test Node versions with LTS 10 | - Update s2geometry C++ lib to a4dddf40647c68cd0104eafc31e9c8fb247a6308 11 | - add Earth support (thanks to @looksgood) 12 | - add Polyline support (thanks to @looksgood) 13 | - add LatLng#approxEquals (thanks to @looksgood) 14 | 15 | ## Version 0.0.3 16 | 17 | - Expose Cell.getCenter method 18 | - Library upgrades 19 | - Publish pre-compiled libraries 20 | 21 | ## Version 0.0.2 22 | 23 | - Introduce s2.RegionCoverer.getRadiusCovering methods that take a lat/lng and radius and generates a covering 24 | - Expose the Point & Cell class and Cell#getVertex method 25 | 26 | ## Version 0.0.1 27 | 28 | - First release of @radarlabs/s2 29 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | dockerbuild: &dockerbuild 4 | steps: 5 | - checkout 6 | - setup_remote_docker: 7 | docker_layer_caching: true 8 | - run: 9 | name: "Pull submodules" 10 | command: | 11 | git submodule init 12 | git submodule sync 13 | git submodule update 14 | - run: 15 | name: Install dependencies 16 | command: | 17 | npm install 18 | when: on_success 19 | - run: 20 | name: Test 21 | command: npm run test 22 | when: on_success 23 | 24 | jobs: 25 | "node-16": 26 | <<: *dockerbuild 27 | docker: 28 | - image: circleci/node:16.5.0 29 | "node-18": 30 | <<: *dockerbuild 31 | docker: 32 | - image: node:18.12.1 33 | "node-20": 34 | <<: *dockerbuild 35 | docker: 36 | - image: node:20.0.0 37 | 38 | workflows: 39 | version: 2 40 | build: 41 | jobs: 42 | - "node-16" 43 | - "node-18" 44 | - "node-20" 45 | 46 | -------------------------------------------------------------------------------- /src/cell_union.h: -------------------------------------------------------------------------------- 1 | #ifndef RADAR_CELL_UNION 2 | #define RADAR_CELL_UNION 3 | 4 | #include 5 | #include "cell_id.h" 6 | #include "s2/s2cell_union.h" 7 | 8 | class CellUnion : public Napi::ObjectWrap { 9 | 10 | public: 11 | static Napi::FunctionReference constructor; 12 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 13 | 14 | CellUnion(const Napi::CallbackInfo& info); 15 | 16 | S2CellUnion Get(); 17 | 18 | private: 19 | S2CellUnion s2cellunion; 20 | 21 | Napi::Value Contains(const Napi::CallbackInfo &info); 22 | Napi::Value Intersects(const Napi::CallbackInfo &info); 23 | 24 | Napi::Value Union(const Napi::CallbackInfo &info); 25 | Napi::Value Intersection(const Napi::CallbackInfo &info); 26 | Napi::Value Difference(const Napi::CallbackInfo &info); 27 | 28 | Napi::Value Ids(const Napi::CallbackInfo &info); 29 | Napi::Value CellIds(const Napi::CallbackInfo &info); 30 | Napi::Value Tokens(const Napi::CallbackInfo &info); 31 | 32 | }; 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /src/polyline.h: -------------------------------------------------------------------------------- 1 | #ifndef RADAR_POLYLINE 2 | #define RADAR_POLYLINE 3 | 4 | #include 5 | 6 | #include "latlng.h" 7 | #include "cell.h" 8 | #include "s2/s1angle.h" 9 | #include "s2/s2earth.h" 10 | #include "s2/s2pointutil.h" 11 | #include "s2/s2polyline.h" 12 | #include "s2/base/stringprintf.h" 13 | 14 | class Polyline : public Napi::ObjectWrap { 15 | public: 16 | Polyline(const Napi::CallbackInfo& info); 17 | 18 | static Napi::FunctionReference constructor; 19 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 20 | 21 | S2Polyline* Get(); 22 | 23 | private: 24 | Napi::Value Contains(const Napi::CallbackInfo &info); 25 | Napi::Value NearlyCovers(const Napi::CallbackInfo &info); 26 | Napi::Value GetLength(const Napi::CallbackInfo &info); 27 | Napi::Value GetCentroid(const Napi::CallbackInfo &info); 28 | Napi::Value Interpolate(const Napi::CallbackInfo &info); 29 | Napi::Value Project(const Napi::CallbackInfo &info); 30 | 31 | S2Polyline s2polyline; 32 | }; 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /src/earth.h: -------------------------------------------------------------------------------- 1 | #ifndef RADAR_EARTH 2 | #define RADAR_EARTH 3 | 4 | #include 5 | #include "latlng.h" 6 | #include "s2/s1angle.h" 7 | #include "s2/s2latlng.h" 8 | #include "s2/s2earth.h" 9 | 10 | class Earth : public Napi::ObjectWrap { 11 | public: 12 | Earth(const Napi::CallbackInfo& info); 13 | 14 | static Napi::FunctionReference constructor; 15 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 16 | 17 | S2Earth Get(); 18 | 19 | private: 20 | static Napi::Value ToMeters(const Napi::CallbackInfo &info); 21 | static Napi::Value ToKm(const Napi::CallbackInfo &info); 22 | static Napi::Value GetRadians(const Napi::CallbackInfo &info); 23 | static Napi::Value GetDegrees(const Napi::CallbackInfo &info); 24 | static Napi::Value GetDistanceKm(const Napi::CallbackInfo &info); 25 | static Napi::Value GetDistanceMeters(const Napi::CallbackInfo &info); 26 | static Napi::Value GetInitalBearingDegrees(const Napi::CallbackInfo &info); 27 | 28 | static bool PreCheck(const Napi::CallbackInfo &info, S2LatLng &latlng1, S2LatLng &latlng2); 29 | 30 | S2Earth s2earth; 31 | }; 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /src/polygon.cc: -------------------------------------------------------------------------------- 1 | #include "polygon.h" 2 | 3 | Napi::FunctionReference Polygon::constructor; 4 | 5 | Napi::Object Polygon::Init(Napi::Env env, Napi::Object exports) { 6 | Napi::HandleScope scope(env); 7 | 8 | Napi::Function func = DefineClass(env, "Polygon", {}); 9 | 10 | constructor = Napi::Persistent(func); 11 | constructor.SuppressDestruct(); 12 | 13 | exports.Set("Polygon", func); 14 | return exports; 15 | } 16 | 17 | Polygon::Polygon(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) { 18 | Napi::Env env = info.Env(); 19 | Napi::HandleScope scope(env); 20 | 21 | int length = info.Length(); 22 | 23 | if (length < 1 || !info[0].IsArrayBuffer()) { 24 | Napi::TypeError::New(env, "(encoded: ArrayBuffer) expected.").ThrowAsJavaScriptException(); 25 | return; 26 | } 27 | 28 | Napi::ArrayBuffer encoded = info[0].As(); 29 | 30 | this->s2polygon = std::make_shared(); 31 | Decoder decoder(encoded.Data(), encoded.ByteLength()); 32 | if (!this->s2polygon->Decode(&decoder)) { 33 | Napi::TypeError::New(env, "malformed ArrayBuffer for S2Polygon.").ThrowAsJavaScriptException(); 34 | return; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@radarlabs/s2", 3 | "version": "0.0.5", 4 | "description": "Node.js JavaScript and TypeScript bindings for the Google S2 geolocation library.", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/radarlabs/s2.git" 10 | }, 11 | "scripts": { 12 | "build": "node-pre-gyp install --build-from-source", 13 | "install": "node-pre-gyp install --fallback-to-build", 14 | "test": "jest --logHeapUsage --verbose", 15 | "publish-to-s3": "node-pre-gyp package unpublish publish" 16 | }, 17 | "author": "Jeff Kao (jeff@radar.com)", 18 | "license": "Apache-2.0", 19 | "keywords": [ 20 | "geolocation" 21 | ], 22 | "devDependencies": { 23 | "jest": "^24.8.0", 24 | "aws-sdk": "2.x", 25 | "bindings": "^1.5.0" 26 | }, 27 | "binary": { 28 | "module_name": "s2", 29 | "module_path": "./lib/binding/{configuration}/{node_abi}-{platform}-{arch}/", 30 | "remote_path": "./{module_name}/v{version}/{configuration}/", 31 | "package_name": "{module_name}-v{version}-{node_abi}-{platform}-{arch}.tar.gz", 32 | "host": "https://node-s2-binaries.s3.amazonaws.com" 33 | }, 34 | "dependencies": { 35 | "@mapbox/node-pre-gyp": "^1.0.10", 36 | "node-addon-api": "^1.7.2", 37 | "node-gyp": "^9.3.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/cell_id.h: -------------------------------------------------------------------------------- 1 | #ifndef RADAR_CELLID 2 | #define RADAR_CELLID 3 | 4 | #include 5 | #include 6 | 7 | #include "latlng.h" 8 | #include "s2/s2cell_id.h" 9 | #include "s2/util/coding/varint.h" 10 | 11 | class CellId : public Napi::ObjectWrap { 12 | 13 | public: 14 | CellId(const Napi::CallbackInfo& info); 15 | 16 | static Napi::FunctionReference constructor; 17 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 18 | 19 | S2CellId Get(); 20 | 21 | private: 22 | static Napi::Value FromToken(const Napi::CallbackInfo &info); 23 | 24 | Napi::Value Id(const Napi::CallbackInfo &info); 25 | Napi::Value IdString(const Napi::CallbackInfo &info); 26 | Napi::Value Token(const Napi::CallbackInfo &info); 27 | 28 | Napi::Value Contains(const Napi::CallbackInfo &info); 29 | Napi::Value Intersects(const Napi::CallbackInfo &info); 30 | 31 | Napi::Value Parent(const Napi::CallbackInfo &info); 32 | Napi::Value Child(const Napi::CallbackInfo &info); 33 | Napi::Value Next(const Napi::CallbackInfo &info); 34 | Napi::Value Level(const Napi::CallbackInfo &info); 35 | Napi::Value IsLeaf(const Napi::CallbackInfo &info); 36 | 37 | Napi::Value RangeMin(const Napi::CallbackInfo &info); 38 | Napi::Value RangeMax(const Napi::CallbackInfo &info); 39 | 40 | S2CellId s2cellid; 41 | 42 | }; 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /test/LatLng.test.js: -------------------------------------------------------------------------------- 1 | // magic incantation from step 3 @ https://github.com/mapbox/node-pre-gyp#readme 2 | const binary = require('@mapbox/node-pre-gyp'); 3 | const path = require('path'); 4 | var binding_path = binary.find(path.resolve('./package.json')); 5 | const s2 = require(binding_path); 6 | 7 | const tokyoTower = [35.6586, 139.7454]; 8 | 9 | test("LatLng#constructor accepts Point", () => { 10 | const point = new s2.Point(-0.6193073896908822, 0.5249960533039503, 0.5838128990434704); 11 | const ll = new s2.LatLng(point); 12 | expect(ll.toString()).toEqual('35.719171,139.711561'); 13 | }); 14 | 15 | test("LatLng#latitude works", () => { 16 | const ll = new s2.LatLng(...tokyoTower); 17 | expect(ll.latitude()).toBe(35.6586); 18 | }); 19 | 20 | test("LatLng#longitude works", () => { 21 | const ll = new s2.LatLng(...tokyoTower); 22 | expect(ll.longitude()).toBe(139.7454); 23 | }); 24 | 25 | test("LatLng#toString works", () => { 26 | const ll = new s2.LatLng(...tokyoTower); 27 | expect(ll.toString()).toBe('35.658600,139.745400'); 28 | }); 29 | 30 | test("LatLng#normalize works", () => { 31 | const denormalized = [215.6586, 319.7454]; 32 | const ll = new s2.LatLng(...denormalized); 33 | const normalized = ll.normalized(); 34 | expect(normalized.toString()).toBe('90.000000,-40.254600'); 35 | }); 36 | 37 | test("LatLng#approxEquals works", () => { 38 | const midLakePavilion = [30.248566, 120.139686]; 39 | const midLakePavilion2 = [30.248569, 120.139686]; 40 | 41 | const ll1 = new s2.LatLng(...midLakePavilion); 42 | const ll2 = new s2.LatLng(...midLakePavilion2); 43 | 44 | expect(ll1.approxEquals(ll2, 1e-7)).toBe(true); 45 | }); 46 | -------------------------------------------------------------------------------- /test/Cell.test.js: -------------------------------------------------------------------------------- 1 | // magic incantation from step 3 @ https://github.com/mapbox/node-pre-gyp#readme 2 | const binary = require('@mapbox/node-pre-gyp'); 3 | const path = require('path'); 4 | var binding_path = binary.find(path.resolve('./package.json')); 5 | const s2 = require(binding_path); 6 | 7 | test("Cell#constructor accepts cellId", () => { 8 | const id = 6924439526941130752n; 9 | const cellId = new s2.CellId(id); 10 | const cell = new s2.Cell(cellId); 11 | expect(cell.constructor).toBe(s2.Cell); 12 | }); 13 | 14 | test("Cell#getVertex returns Point for vertex position", () => { 15 | const id = 6924439526941130752n; 16 | const cellId = new s2.CellId(id); 17 | const cell = new s2.Cell(cellId); 18 | 19 | const v1 = cell.getVertex(0); 20 | const v2 = cell.getVertex(1); 21 | const v3 = cell.getVertex(2); 22 | const v4 = cell.getVertex(3); 23 | 24 | expect([v1.x(), v1.y(), v1.z()]).toEqual([-0.6193073896908822, 0.5249960533039503, 0.5838128990434704]); 25 | expect([v2.x(), v2.y(), v2.z()]).toEqual([-0.6194499844134254, 0.5251169329637175, 0.5835528455289937]); 26 | expect([v3.x(), v3.y(), v3.z()]).toEqual([-0.6195734250744275, 0.5248419898946621, 0.5836691328012421]); 27 | expect([v4.x(), v4.y(), v4.z()]).toEqual([-0.6194307451080033, 0.5247211253861704, 0.583929184566429]); 28 | }); 29 | 30 | test("Cell#getCenter returns Point for center position", () => { 31 | const id = 6924439526941130752n; 32 | const cellId = new s2.CellId(id); 33 | const cell = new s2.Cell(cellId); 34 | 35 | const center = cell.getCenter(); 36 | expect([center.x(), center.y(), center.z()]).toEqual([-0.6194403961888982, 0.5249190405535087, 0.5837410354204379]); 37 | }); 38 | -------------------------------------------------------------------------------- /src/loop.cc: -------------------------------------------------------------------------------- 1 | #include "loop.h" 2 | #include "s2/s2pointutil.h" 3 | 4 | Napi::FunctionReference Loop::constructor; 5 | 6 | Napi::Object Loop::Init(Napi::Env env, Napi::Object exports) { 7 | Napi::HandleScope scope(env); 8 | 9 | Napi::Function func = DefineClass(env, "Loop", {}); 10 | 11 | constructor = Napi::Persistent(func); 12 | constructor.SuppressDestruct(); 13 | 14 | exports.Set("Loop", func); 15 | return exports; 16 | } 17 | 18 | Loop::Loop(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) { 19 | Napi::Env env = info.Env(); 20 | Napi::HandleScope scope(env); 21 | 22 | int length = info.Length(); 23 | 24 | if (length <= 0 || !info[0].IsArray()) { 25 | Napi::TypeError::New(env, "(latlngs: s2.LatLng[]) expected.").ThrowAsJavaScriptException(); 26 | return; 27 | } 28 | 29 | Napi::Array llArray = info[0].As(); 30 | uint32_t arrayLength = llArray.Length(); 31 | if (arrayLength <= 0) { 32 | Napi::RangeError::New(env, "(latlngs: s2.LatLng[]) was empty.").ThrowAsJavaScriptException(); 33 | return; 34 | } 35 | 36 | std::vector pointVector; 37 | for (uint32_t i = 0; i < arrayLength; i++) { 38 | Napi::Value obj = llArray[i]; 39 | LatLng* ll = LatLng::Unwrap(obj.As()); 40 | S2Point point = ll->Get().Normalized().ToPoint().Normalize(); 41 | pointVector.push_back(point); 42 | } 43 | 44 | this->s2loop = std::make_shared(pointVector, S2Debug::ALLOW); 45 | 46 | S2Error error; 47 | if (this->s2loop->FindValidationError(&error)) { 48 | Napi::Error::New(env, StringPrintf("Loop is invalid: %s", error.text().c_str())).ThrowAsJavaScriptException(); 49 | return; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/region_coverer.h: -------------------------------------------------------------------------------- 1 | #ifndef RADAR_REGION_COVERER 2 | #define RADAR_REGION_COVERER 3 | 4 | #include 5 | #include "polygon.h" 6 | #include "cell_id.h" 7 | #include "cell_union.h" 8 | #include "s2/s1angle.h" 9 | #include "s2/s2builder.h" 10 | #include "s2/s2builderutil_s2polygon_layer.h" 11 | #include "s2/s2cap.h" 12 | #include "s2/s2cell_union.h" 13 | #include "s2/s2earth.h" 14 | #include "s2/s2polygon.h" 15 | #include "s2/s2region_coverer.h" 16 | #include "s2/third_party/absl/memory/memory.h" 17 | 18 | class RegionCoverer : public Napi::ObjectWrap { 19 | 20 | public: 21 | static Napi::FunctionReference constructor; 22 | static Napi::Object Init(Napi::Env env, Napi::Object exports); 23 | 24 | RegionCoverer(const Napi::CallbackInfo& info); 25 | 26 | private: 27 | static Napi::Value GetCoveringIds(const Napi::CallbackInfo &info); 28 | static Napi::Value GetCoveringTokens(const Napi::CallbackInfo &info); 29 | static Napi::Value GetCoveringCellUnion(const Napi::CallbackInfo &info); 30 | 31 | static Napi::Value GetRadiusCoveringIds(const Napi::CallbackInfo &info); 32 | static Napi::Value GetRadiusCoveringTokens(const Napi::CallbackInfo &info); 33 | static Napi::Value GetRadiusCoveringCellUnion(const Napi::CallbackInfo &info); 34 | 35 | static void GetS2Options( 36 | Napi::Object optionsObject, 37 | S2RegionCoverer::Options &options 38 | ); 39 | 40 | static S2CellUnion GetCovering( 41 | Napi::Env env, 42 | std::vector &vertices, 43 | S2RegionCoverer::Options &coverOptions, 44 | S2Error &loopError, 45 | S2Error &buildError, 46 | S2Error &outputError 47 | ); 48 | 49 | static S2CellUnion GetRadiusCovering( 50 | LatLng* ll, 51 | double radiusM, 52 | S2RegionCoverer::Options &coverOptions 53 | ); 54 | }; 55 | 56 | #endif 57 | -------------------------------------------------------------------------------- /src/cell.cc: -------------------------------------------------------------------------------- 1 | #include "cell.h" 2 | 3 | Napi::FunctionReference Cell::constructor; 4 | 5 | Napi::Object Cell::Init(Napi::Env env, Napi::Object exports) { 6 | Napi::HandleScope scope(env); 7 | 8 | Napi::Function func = DefineClass(env, "Cell", { 9 | InstanceMethod("getVertex", &Cell::GetVertex), 10 | InstanceMethod("getCenter", &Cell::GetCenter), 11 | }); 12 | 13 | constructor = Napi::Persistent(func); 14 | constructor.SuppressDestruct(); 15 | 16 | exports.Set("Cell", func); 17 | return exports; 18 | } 19 | 20 | Cell::Cell(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) { 21 | Napi::Env env = info.Env(); 22 | Napi::HandleScope scope(env); 23 | 24 | int length = info.Length(); 25 | string badArgs = "CellId expected."; 26 | 27 | if (length <= 0 || !info[0].IsObject()) { 28 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 29 | return; 30 | } 31 | 32 | Napi::Object object = info[0].As(); 33 | bool isCellId = object.InstanceOf(CellId::constructor.Value()); 34 | if (!isCellId) { 35 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 36 | return; 37 | } 38 | 39 | CellId* cellId = CellId::Unwrap(object); 40 | this->s2Cell = S2Cell(cellId->Get()); 41 | } 42 | 43 | Napi::Value Cell::GetVertex(const Napi::CallbackInfo &info) { 44 | Napi::Env env = info.Env(); 45 | 46 | if (info.Length() <= 0 || !info[0].IsNumber()) { 47 | Napi::TypeError::New(env, "(vertex: number) expected.").ThrowAsJavaScriptException(); 48 | return env.Null(); 49 | } 50 | 51 | Napi::Number vertex = info[0].As(); 52 | S2Point point = s2Cell.GetVertex(vertex); 53 | 54 | return Point::constructor.New({ 55 | Napi::External::New(env, &point) 56 | }); 57 | } 58 | 59 | Napi::Value Cell::GetCenter(const Napi::CallbackInfo &info) { 60 | Napi::Env env = info.Env(); 61 | 62 | S2Point point = s2Cell.GetCenter(); 63 | 64 | return Point::constructor.New({ 65 | Napi::External::New(env, &point) 66 | }); 67 | } 68 | 69 | S2Cell Cell::Get() { 70 | return s2Cell; 71 | } 72 | -------------------------------------------------------------------------------- /src/point.cc: -------------------------------------------------------------------------------- 1 | #include "point.h" 2 | 3 | Napi::FunctionReference Point::constructor; 4 | 5 | Napi::Object Point::Init(Napi::Env env, Napi::Object exports) { 6 | Napi::HandleScope scope(env); 7 | 8 | Napi::Function func = DefineClass(env, "Point", { 9 | InstanceMethod("x", &Point::X), 10 | InstanceMethod("y", &Point::Y), 11 | InstanceMethod("z", &Point::Z), 12 | }); 13 | 14 | constructor = Napi::Persistent(func); 15 | constructor.SuppressDestruct(); 16 | 17 | exports.Set("Point", func); 18 | return exports; 19 | } 20 | 21 | Point::Point(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) { 22 | Napi::Env env = info.Env(); 23 | Napi::HandleScope scope(env); 24 | 25 | int length = info.Length(); 26 | string badArgs = "(number, number, number) expected."; 27 | 28 | if (length != 1 && length != 3) { 29 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 30 | return; 31 | } 32 | 33 | if (length == 1) { // S2 Point 34 | if (!info[0].IsExternal()) { 35 | Napi::TypeError::New(env, "S2Point expected.").ThrowAsJavaScriptException(); 36 | return; 37 | } 38 | Napi::External external = info[0].As>(); 39 | this->s2Point = *external.Data(); 40 | 41 | } else { // (x, y, z) 42 | if (!info[0].IsNumber() || !info[1].IsNumber() || !info[2].IsNumber()) { 43 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 44 | return; 45 | } 46 | 47 | this->s2Point = S2Point( 48 | info[0].As().DoubleValue(), 49 | info[1].As().DoubleValue(), 50 | info[2].As().DoubleValue() 51 | ); 52 | } 53 | } 54 | 55 | Napi::Value Point::X(const Napi::CallbackInfo &info) { 56 | return Napi::Number::New(info.Env(), s2Point.x()); 57 | } 58 | 59 | Napi::Value Point::Y(const Napi::CallbackInfo &info) { 60 | return Napi::Number::New(info.Env(), s2Point.y()); 61 | } 62 | 63 | Napi::Value Point::Z(const Napi::CallbackInfo &info) { 64 | return Napi::Number::New(info.Env(), s2Point.z()); 65 | } 66 | 67 | S2Point Point::Get() { 68 | return s2Point; 69 | } 70 | -------------------------------------------------------------------------------- /src/builder.cc: -------------------------------------------------------------------------------- 1 | #include "builder.h" 2 | 3 | using s2builderutil::S2PolygonLayer; 4 | using absl::make_unique; 5 | 6 | Napi::FunctionReference Builder::constructor; 7 | 8 | Napi::Object Builder::Init(Napi::Env env, Napi::Object exports) { 9 | Napi::HandleScope scope(env); 10 | 11 | Napi::Function func = DefineClass(env, "Builder", { 12 | InstanceMethod("addLoop", &Builder::AddLoop), 13 | InstanceMethod("build", &Builder::Build) 14 | }); 15 | 16 | constructor = Napi::Persistent(func); 17 | constructor.SuppressDestruct(); 18 | 19 | exports.Set("Builder", func); 20 | return exports; 21 | } 22 | 23 | Builder::Builder(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) { 24 | Napi::Env env = info.Env(); 25 | Napi::HandleScope scope(env); 26 | 27 | this->s2builderOptions = make_unique(); 28 | this->s2builder = make_unique(*this->s2builderOptions); 29 | } 30 | 31 | Napi::Value Builder::AddLoop(const Napi::CallbackInfo &info) { 32 | Napi::Env env = info.Env(); 33 | 34 | if (info.Length() <= 0 || !info[0].IsObject()) { 35 | Napi::TypeError::New(env, "(loop: s2.Loop) expected.").ThrowAsJavaScriptException(); 36 | return env.Null(); 37 | } 38 | 39 | Napi::Object object = info[0].As(); 40 | bool isLoop = object.InstanceOf(Loop::constructor.Value()); 41 | if (!isLoop) { 42 | Napi::TypeError::New(env, "(loop: s2.Loop) expected.").ThrowAsJavaScriptException(); 43 | return env.Null(); 44 | } 45 | 46 | Loop* loop = Loop::Unwrap(object); 47 | 48 | this->output = make_unique(); 49 | this->s2builder->StartLayer( 50 | make_unique(this->output.get()) 51 | ); 52 | this->s2builder->AddLoop(*loop->s2loop); 53 | 54 | return env.Null(); 55 | } 56 | 57 | Napi::Value Builder::Build(const Napi::CallbackInfo &info) { 58 | Napi::Env env = info.Env(); 59 | 60 | if (this->output == nullptr) { 61 | Napi::Error::New(env, "Add a loop first.").ThrowAsJavaScriptException(); 62 | return env.Null(); 63 | } 64 | 65 | S2Error error; 66 | bool buildOk = this->s2builder->Build(&error); 67 | if (!buildOk) { 68 | Napi::Error::New(env, StringPrintf("Build failed: %s", error.text().c_str())).ThrowAsJavaScriptException(); 69 | return env.Null(); 70 | } 71 | 72 | if (!this->output->IsValid()) { 73 | Napi::Error::New(env, "Output is invalid.").ThrowAsJavaScriptException(); 74 | return env.Null(); 75 | } 76 | 77 | // encode the output and return it as a JS object 78 | Encoder encoder; 79 | this->output->Encode(&encoder); 80 | 81 | return Polygon::constructor.New({ 82 | Napi::ArrayBuffer::New(env, (void *)encoder.base(), encoder.length()) 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /test/Earth.test.js: -------------------------------------------------------------------------------- 1 | // magic incantation from step 3 @ https://github.com/mapbox/node-pre-gyp#readme 2 | const binary = require('@mapbox/node-pre-gyp'); 3 | const path = require('path'); 4 | var binding_path = binary.find(path.resolve('./package.json')); 5 | const s2 = require(binding_path); 6 | 7 | const westLakeUniversity = [30.135703, 120.075485]; 8 | const apsaraPark = [30.135770, 120.074877]; 9 | 10 | test("Earth#constructor", () => { 11 | const earth = new s2.Earth(); 12 | expect(earth.constructor).toBe(s2.Earth); 13 | }); 14 | 15 | 16 | test("Earth#toMeters Earth#getDistanceMeters Earth#toKm Earth#getDistanceKm Earth#getRadians Earth#getDegrees", () => { 17 | const latlng1 = new s2.LatLng(westLakeUniversity[0], westLakeUniversity[1]); 18 | const latlng2 = new s2.LatLng(apsaraPark[0], apsaraPark[1]); 19 | 20 | const distanceToMeters = s2.Earth.toMeters(latlng1, latlng2); 21 | const distanceMeters = s2.Earth.getDistanceMeters(latlng1, latlng2); 22 | 23 | const distanceToKm = s2.Earth.toKm(latlng1, latlng2); 24 | const distanceKm = s2.Earth.getDistanceKm(latlng1, latlng2); 25 | 26 | const radians = s2.Earth.getRadians(latlng1, latlng2); 27 | const degrees = s2.Earth.getDegrees(latlng1, latlng2); 28 | 29 | expect(distanceToMeters).toBe(58.941537292477996); 30 | expect(distanceMeters).toBe(58.941537292477996); 31 | expect(distanceToKm).toBe(0.058941537292477996); 32 | expect(distanceKm).toBe(0.058941537292477996); 33 | 34 | expect(radians).toBe(0.00000925152170416904); 35 | expect(degrees).toBe(0.0005300731477225649); 36 | }); 37 | 38 | test("Earth#getInitalBearingDegrees", () => { 39 | const centerLatLng = [30.135703, 120.075485]; 40 | const eastLatLng = [30.135717, 120.076004]; 41 | const southLatLng = [30.135510, 120.075515]; 42 | const westLatLng = [30.135749, 120.075085]; 43 | const northLatLng = [30.136150, 120.075611]; 44 | 45 | const center = new s2.LatLng(centerLatLng[0], centerLatLng[1]); 46 | const east = new s2.LatLng(eastLatLng[0], eastLatLng[1]); 47 | const south = new s2.LatLng(southLatLng[0], southLatLng[1]); 48 | const west = new s2.LatLng(westLatLng[0], westLatLng[1]); 49 | const north = new s2.LatLng(northLatLng[0], northLatLng[1]); 50 | 51 | const degreeEast = s2.Earth.getInitalBearingDegrees(center, east); 52 | expect(degreeEast).toBe(88.21335159225947); 53 | 54 | const degreeSouth = s2.Earth.getInitalBearingDegrees(center, south); 55 | expect(degreeSouth).toBe(172.34356352357213); 56 | 57 | const degreeWest = s2.Earth.getInitalBearingDegrees(center, west); 58 | expect(degreeWest).toBe(-82.42555355986279); 59 | 60 | const degreeNorth = s2.Earth.getInitalBearingDegrees(center, north); 61 | expect(degreeNorth).toBe(13.70028061338925); 62 | }); 63 | -------------------------------------------------------------------------------- /test/Polyline.test.js: -------------------------------------------------------------------------------- 1 | // magic incantation from step 3 @ https://github.com/mapbox/node-pre-gyp#readme 2 | const binary = require('@mapbox/node-pre-gyp'); 3 | const path = require('path'); 4 | var binding_path = binary.find(path.resolve('./package.json')); 5 | const s2 = require(binding_path); 6 | 7 | const PRECISION = 6; 8 | 9 | const baiCauseway = [[30.261254, 120.147342],[30.260202, 120.146395],[30.258967, 120.145267],[30.257545, 120.144089], [30.256662, 120.143349],[30.256027, 120.142843],[30.255445, 120.142336]]; // 白堤。 WGS84 10 | const polyline = new s2.Polyline(baiCauseway.map((latlng) => { 11 | const [lat, lng] = latlng; 12 | return new s2.LatLng(lat, lng); 13 | })); 14 | 15 | test("Polyline#contains", () => { 16 | const [lat, lng] = baiCauseway[1]; 17 | const cellId = new s2.CellId(new s2.LatLng(lat, lng)); 18 | const cell = new s2.Cell(cellId); 19 | expect(polyline.contains(cell)).toBe(false); 20 | }); 21 | 22 | test("Polyline#nearlyCovers true", () => { 23 | const newBaiCauseway = [[30.261254, 120.147342],[30.260202, 120.146395],[30.258967, 120.145267],[30.257545, 120.144089], [30.256662, 120.143349],[30.256027, 120.142843],[30.255445, 120.142336],[30.254703, 120.141366]]; 24 | const newLLs = newBaiCauseway.map((latlng) => { 25 | const [lat, lng] = latlng; 26 | return new s2.LatLng(lat, lng); 27 | }); 28 | const newPolyline = new s2.Polyline(newLLs); 29 | 30 | expect(polyline.nearlyCovers(newPolyline, 1e-5)).toBe(false); 31 | expect(newPolyline.nearlyCovers(polyline, 1e-5)).toBe(true); 32 | }); 33 | 34 | test("Polyline#nearlyCovers false", () => { 35 | const tempPoints = [[30.261076, 120.153102],[30.259816, 120.154280], [30.258923, 120.152020],[30.258824, 120.151923 ],[30.258739, 120.151840],[30.258642, 120.151754]]; 36 | const newBaiCauseway = tempPoints.concat([]); 37 | const newLLs = newBaiCauseway.map((latlng) => { 38 | const [lat, lng] = latlng; 39 | return new s2.LatLng(lat, lng); 40 | }); 41 | const newPolyline = new s2.Polyline(newLLs); 42 | 43 | expect(polyline.nearlyCovers(newPolyline, 1e-5)).toBe(false); 44 | }); 45 | 46 | test("Polyline#getLength", () => { 47 | expect(polyline.getLength().toFixed(PRECISION)).toBe("805.441648"); 48 | }); 49 | 50 | test("Polyline#getCentroid", () => { 51 | const centroid = polyline.getCentroid(); 52 | expect(centroid.latitude().toFixed(PRECISION)).toBe("30.258369"); 53 | expect(centroid.longitude().toFixed(PRECISION)).toBe("120.144809"); 54 | }); 55 | 56 | test("Polyline#interpolate", () => { 57 | const interpolated = polyline.interpolate(0.75); 58 | expect(interpolated.latitude().toFixed(PRECISION)).toBe("30.256914"); 59 | expect(interpolated.longitude().toFixed(PRECISION)).toBe("120.143561"); 60 | }); 61 | 62 | test("Polyline#project", () => { 63 | const westLakeMuseum = [30.259710, 120.140802];// 西湖博览会博物馆 64 | const westLakeMuseumLatLng = new s2.LatLng(westLakeMuseum[0], westLakeMuseum[1]); 65 | 66 | const westLakeMuseumNearPoint = polyline.project(westLakeMuseumLatLng); 67 | expect(westLakeMuseumNearPoint.point.latitude().toFixed(PRECISION)).toBe("30.257633"); 68 | expect(westLakeMuseumNearPoint.point.longitude().toFixed(PRECISION)).toBe("120.144162"); 69 | expect(westLakeMuseumNearPoint.index).toBe(3); 70 | 71 | const midLakePavilion = [30.248636, 120.139723]; // 西湖湖心亭 72 | const midLakePavilionLatLng = new s2.LatLng(midLakePavilion[0], midLakePavilion[1]); 73 | 74 | const midLakePavilionPoint = polyline.project(midLakePavilionLatLng); 75 | expect(midLakePavilionPoint.point.latitude().toFixed(PRECISION)).toBe("30.255445"); 76 | expect(midLakePavilionPoint.point.longitude().toFixed(PRECISION)).toBe("120.142336"); 77 | expect(midLakePavilionPoint.index).toBe(7); 78 | }) 79 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@radarlabs/s2' { 2 | 3 | export type ChildPosition = 0 | 1 | 2 | 3; 4 | 5 | export class Point { 6 | constructor(x: number, y: number, z: number); 7 | 8 | public x(): number; 9 | public y(): number; 10 | public z(): number; 11 | } 12 | 13 | export class LatLng { 14 | constructor(latDegrees: number, lngDegrees: number); 15 | constructor(point: Point); 16 | 17 | public normalized(): LatLng; 18 | 19 | public latitude(): number; 20 | public longitude(): number; 21 | 22 | public approxEquals(ll: LatLng, maxErrorRadians?: number): number; 23 | 24 | public toString(): string; 25 | } 26 | 27 | export class CellId { 28 | public constructor(id: bigint); 29 | public constructor(token: string); 30 | public constructor(ll: LatLng); 31 | 32 | public id(): bigint; 33 | public idString(): string; 34 | public token(): string; 35 | 36 | public contains(other: CellId): boolean; 37 | public intersects(other: CellId): boolean; 38 | public isLeaf(): boolean; 39 | 40 | public parent(level?: number): CellId; 41 | public child(position: ChildPosition): CellId; 42 | public next(): CellId; 43 | 44 | public level(): number; 45 | 46 | public rangeMin(): CellId; 47 | public rangeMax(): CellId; 48 | 49 | public static fromToken(token: string): CellId; 50 | } 51 | 52 | export class Cell { 53 | public constructor(cellId: CellId); 54 | 55 | public getVertex(pos: number): Point; 56 | public getCenter(): Point; 57 | } 58 | 59 | export class CellUnion { 60 | public constructor(tokens: string[]); 61 | public constructor(cellIds: CellId[]); 62 | 63 | public contains(cells: CellId | CellUnion): boolean; 64 | public intersects(cells: CellId | CellUnion): boolean; 65 | 66 | public union(cells: CellUnion): CellUnion; 67 | public intersection(cells: CellUnion): CellUnion; 68 | public difference(cells: CellUnion): CellUnion; 69 | 70 | public ids(): BigUint64Array; 71 | public cellIds(): CellId[]; 72 | public tokens(): string[]; 73 | } 74 | 75 | export interface RegionCovererOptions { 76 | min?: number; 77 | max?: number; 78 | max_cells?: number; 79 | } 80 | 81 | export class RegionCoverer { 82 | public static getCoveringIds(lls: LatLng[], options: RegionCovererOptions): BigUint64Array | null; 83 | public static getCoveringTokens(lls: LatLng[], options: RegionCovererOptions): string[] | null; 84 | public static getCovering(lls: LatLng[], options: RegionCovererOptions): CellUnion | null; 85 | 86 | public static getRadiusCoveringIds(ll: LatLng, radiusM: number, options: RegionCovererOptions): BigUint64Array | null; 87 | public static getRadiusCoveringTokens(ll: LatLng, radiusM: number, options: RegionCovererOptions): string[] | null; 88 | public static getRadiusCovering(ll: LatLng, radiusM: number, options: RegionCovererOptions): CellUnion | null; 89 | } 90 | 91 | export class Earth { 92 | public static toMeters(a: LatLng, b: LatLng): number; 93 | public static getDistanceMeters(a: LatLng, b: LatLng): number; 94 | 95 | public static toKm(a: LatLng, b: LatLng): number; 96 | public static getDistanceKm(a: LatLng, b: LatLng): number; 97 | 98 | public static getDegrees(a: LatLng, b: LatLng): number; 99 | public static getRadians(a: LatLng, b: LatLng): number; 100 | 101 | public static getInitialBearingDegrees(a: LatLng, b: LatLng): number; 102 | } 103 | 104 | export class Polyline { 105 | public constructor(lls: LatLng[]); 106 | 107 | public contains(cell: Cell): boolean; 108 | public nearlyCovers(other: Polyline, maxError: number): boolean; 109 | 110 | public getLength(): number; 111 | public getCentroid(): LatLng; 112 | 113 | public interpolate(fraction: number): LatLng; 114 | public project(ll: LatLng): LatLng; 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/latlng.cc: -------------------------------------------------------------------------------- 1 | #include "latlng.h" 2 | 3 | Napi::FunctionReference LatLng::constructor; 4 | 5 | Napi::Object LatLng::Init(Napi::Env env, Napi::Object exports) { 6 | Napi::HandleScope scope(env); 7 | 8 | Napi::Function func = DefineClass(env, "LatLng", { 9 | InstanceMethod("latitude", &LatLng::Latitude), 10 | InstanceMethod("longitude", &LatLng::Longitude), 11 | InstanceMethod("toString", &LatLng::ToString), 12 | InstanceMethod("normalized", &LatLng::Normalized), 13 | InstanceMethod("approxEquals", &LatLng::ApproxEquals) 14 | }); 15 | 16 | constructor = Napi::Persistent(func); 17 | constructor.SuppressDestruct(); 18 | 19 | exports.Set("LatLng", func); 20 | return exports; 21 | } 22 | 23 | Napi::Object LatLng::NewInstance(Napi::Value lat, Napi::Value lng) { 24 | return constructor.New({ lat, lng }); 25 | } 26 | 27 | LatLng::LatLng(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) { 28 | Napi::Env env = info.Env(); 29 | Napi::HandleScope scope(env); 30 | 31 | int length = info.Length(); 32 | 33 | if (length <= 0 || length > 2) { 34 | Napi::TypeError::New(env, "(lat: number, lng: number) | Point expected.").ThrowAsJavaScriptException(); 35 | return; 36 | } 37 | 38 | if (length == 1) { // Point 39 | if (!info[0].IsObject()) { 40 | Napi::TypeError::New(env, "Point expected.").ThrowAsJavaScriptException(); 41 | return; 42 | } 43 | Napi::Object object = info[0].As(); 44 | bool isPoint = object.InstanceOf(Point::constructor.Value()); 45 | if (!isPoint) { 46 | Napi::TypeError::New(env, "Point expected.").ThrowAsJavaScriptException(); 47 | return; 48 | } 49 | 50 | Point* point = Point::Unwrap(object); 51 | this->s2latlng = S2LatLng(point->Get()); 52 | } else { // lat, lng 53 | if (!info[0].IsNumber() || !info[1].IsNumber()) { 54 | Napi::TypeError::New(env, "(lat: number, lng: number) expected.").ThrowAsJavaScriptException(); 55 | return; 56 | } 57 | 58 | Napi::Number lat = info[0].As(); 59 | Napi::Number lng = info[1].As(); 60 | 61 | this->s2latlng = S2LatLng::FromDegrees( 62 | lat.DoubleValue(), 63 | lng.DoubleValue() 64 | ); 65 | } 66 | } 67 | 68 | 69 | S2LatLng LatLng::Get() { 70 | return s2latlng; 71 | } 72 | 73 | Napi::Value LatLng::ToString(const Napi::CallbackInfo& info) { 74 | return Napi::String::New(info.Env(), this->s2latlng.ToStringInDegrees()); 75 | } 76 | 77 | Napi::Value LatLng::Normalized(const Napi::CallbackInfo& info) { 78 | S2LatLng normalized = this->s2latlng.Normalized(); 79 | 80 | return LatLng::NewInstance( 81 | Napi::Number::New(info.Env(), normalized.lat().degrees()), 82 | Napi::Number::New(info.Env(), normalized.lng().degrees()) 83 | ); 84 | } 85 | 86 | Napi::Value LatLng::Latitude(const Napi::CallbackInfo& info) { 87 | return Napi::Number::New( 88 | info.Env(), 89 | this->s2latlng.lat().degrees() 90 | ); 91 | } 92 | 93 | Napi::Value LatLng::Longitude(const Napi::CallbackInfo& info) { 94 | return Napi::Number::New( 95 | info.Env(), 96 | this->s2latlng.lng().degrees() 97 | ); 98 | } 99 | 100 | Napi::Value LatLng::ApproxEquals(const Napi::CallbackInfo& info) { 101 | Napi::Env env = info.Env(); 102 | int length = info.Length(); 103 | S1Angle maxError = S1Angle::Radians(1e-15); 104 | 105 | if (length < 1 || length > 2) { 106 | Napi::TypeError::New(env, "(ll: LatLng, maxErrorRadians?: number) expected.").ThrowAsJavaScriptException(); 107 | return env.Null(); 108 | } 109 | 110 | Napi::Object object = info[0].As(); 111 | bool isLatlng = object.InstanceOf(LatLng::constructor.Value()); 112 | if (!isLatlng) { 113 | Napi::TypeError::New(env, "(ll: LatLng, maxErrorRadians?: number) expected.").ThrowAsJavaScriptException(); 114 | return env.Null(); 115 | } 116 | 117 | LatLng* ll = LatLng::Unwrap(object); 118 | S2LatLng s2LatLng = ll->Get(); 119 | 120 | if (length == 2) { 121 | if (!info[1].IsNumber()) { 122 | Napi::TypeError::New(env, "(ll: LatLng, maxErrorRadians?: number) expected.").ThrowAsJavaScriptException(); 123 | return env.Null(); 124 | } 125 | 126 | double radians = info[1].As().DoubleValue(); 127 | maxError = S1Angle::Radians(radians); 128 | } 129 | 130 | bool isEqual = this->s2latlng.ApproxEquals(s2LatLng, maxError); 131 | return Napi::Boolean::New(info.Env(), isEqual); 132 | } 133 | -------------------------------------------------------------------------------- /test/CellId.test.js: -------------------------------------------------------------------------------- 1 | // magic incantation from step 3 @ https://github.com/mapbox/node-pre-gyp#readme 2 | const binary = require('@mapbox/node-pre-gyp'); 3 | const path = require('path'); 4 | var binding_path = binary.find(path.resolve('./package.json')); 5 | const s2 = require(binding_path); 6 | 7 | const tokyoTower = [35.6586, 139.7454]; 8 | 9 | 10 | test("CellId#constructor accepts id: bigint", () => { 11 | const id = 6924439526941130752n; 12 | const cellId = new s2.CellId(id); 13 | expect(cellId.id()).toBe(6924439526941130752n); 14 | expect(cellId.idString()).toBe('6924439526941130752'); 15 | expect(cellId.token()).toBe('60188d1'); 16 | expect(cellId.level()).toBe(12); 17 | }); 18 | 19 | test("CellId#constructor accepts ll: LatLng", () => { 20 | const cellId = new s2.CellId(new s2.LatLng(...tokyoTower)); 21 | expect(cellId.id()).toBe(6924438073539270971n); 22 | expect(cellId.idString()).toBe('6924438073539270971'); 23 | expect(cellId.token()).toBe('60188bbd9a7f993b'); 24 | expect(cellId.level()).toBe(30); 25 | }); 26 | 27 | test('CellId#constructor accepts token: string', () => { 28 | const cellId = new s2.CellId('89c258c'); 29 | expect(cellId.id()).toBe(9926594110334959616n); 30 | expect(cellId.idString()).toBe('9926594110334959616'); 31 | expect(cellId.token()).toBe('89c258c'); 32 | expect(cellId.level()).toBe(11); 33 | }); 34 | 35 | test('CellId#FromToken works', () => { 36 | const cellId = s2.CellId.fromToken('89c258c'); 37 | expect(cellId.id()).toBe(9926594110334959616n); 38 | expect(cellId.idString()).toBe('9926594110334959616'); 39 | expect(cellId.token()).toBe('89c258c'); 40 | expect(cellId.level()).toBe(11); 41 | }); 42 | 43 | test("CellId#intersects works", () => { 44 | const intersecting1 = s2.CellId.fromToken('89c2584'); 45 | const intersecting2 = s2.CellId.fromToken('89c259'); 46 | const outside1 = s2.CellId.fromToken('89c258c'); 47 | 48 | expect(intersecting1.intersects(intersecting2)).toBe(true); 49 | expect(intersecting1.intersects(outside1)).toBe(false); 50 | 51 | // parent relationship 52 | expect(intersecting1.parent().intersects(intersecting1)).toBe(true); 53 | }); 54 | 55 | test("CellId#contains works", () => { 56 | const parent = new s2.CellId(6924438358710026240n); 57 | const child = new s2.CellId(6924439526941130752n); 58 | 59 | expect(parent.contains(child)).toBe(true); 60 | expect(child.contains(parent)).toBe(false); 61 | 62 | // parent relationship 63 | expect(child.parent().contains(child)).toBe(true); 64 | }); 65 | 66 | test("CellId#parent works", () => { 67 | const parent = new s2.CellId(6924438358710026240n); 68 | const child = new s2.CellId(6924439526941130752n); 69 | 70 | // check basic child-parent relationships 71 | const parentId = child.parent(parent.level()).id(); 72 | expect(parentId).toBe(parent.id()); 73 | expect(child.level()).toBe(12); 74 | expect(child.parent().level()).toBe(11); 75 | 76 | // level-0 cell parent should return the same cell 77 | const topLevelCell = s2.CellId.fromToken('9'); 78 | expect(topLevelCell.level()).toBe(0); 79 | 80 | expect(topLevelCell.parent().token()).toBe('9'); 81 | expect(topLevelCell.parent().level()).toBe(0); 82 | 83 | expect(topLevelCell.parent(-1).token()).toBe('9'); 84 | expect(topLevelCell.parent(-1).level()).toBe(0); 85 | }); 86 | 87 | test("CellId#child works", () => { 88 | const parent = new s2.CellId(12281386552583520256n); 89 | 90 | expect(parent.level()).toBe(7); 91 | expect(parent.child(0).id()).toBe(12281333776025387008n); 92 | expect(parent.child(0).level()).toBe(8); 93 | expect(parent.contains(parent.child(0))).toBe(true); 94 | 95 | expect(parent.child(1).id()).toBe(12281368960397475840n); 96 | expect(parent.child(1).level()).toBe(8); 97 | expect(parent.contains(parent.child(1))).toBe(true); 98 | 99 | expect(parent.child(2).id()).toBe(12281404144769564672n); 100 | expect(parent.child(2).level()).toBe(8); 101 | expect(parent.contains(parent.child(2))).toBe(true); 102 | 103 | expect(parent.child(3).id()).toBe(12281439329141653504n); 104 | expect(parent.child(3).level()).toBe(8); 105 | expect(parent.contains(parent.child(3))).toBe(true); 106 | 107 | // no further children after leaf 108 | const leaf = new s2.CellId(new s2.LatLng(...tokyoTower)); 109 | expect(leaf.id()).toBe(6924438073539270971n); 110 | expect(leaf.level()).toBe(30); 111 | expect(leaf.isLeaf()).toBe(true); 112 | expect(leaf.child(0).id()).toBe(6924438073539270971n); 113 | expect(leaf.child(0).level()).toBe(30); 114 | }); 115 | 116 | test("CellId#next works", () => { 117 | const cellId = new s2.CellId(6924439526941130752n); 118 | expect(cellId.next().id()).toBe(6924439664380084224n); 119 | }); 120 | 121 | test("CellId#isLeaf works", () => { 122 | const cellId = new s2.CellId(6924439526941130752n); 123 | expect(cellId.isLeaf()).toBe(false); 124 | expect(cellId.parent(30).isLeaf()).toBe(true); 125 | }); 126 | 127 | test("CellId#rangeMin works", () => { 128 | const cell = new s2.CellId(6924439526941130752n); 129 | expect(cell.rangeMin().id()).toBe(6924439458221654017n) 130 | }); 131 | 132 | test("CellId#rangeMax works", () => { 133 | const cell = new s2.CellId(6924439526941130752n); 134 | expect(cell.rangeMax().id()).toBe(6924439595660607487n) 135 | }); 136 | -------------------------------------------------------------------------------- /test/CellUnion.test.js: -------------------------------------------------------------------------------- 1 | // magic incantation from step 3 @ https://github.com/mapbox/node-pre-gyp#readme 2 | const binary = require('@mapbox/node-pre-gyp'); 3 | const path = require('path'); 4 | var binding_path = binary.find(path.resolve('./package.json')); 5 | const s2 = require(binding_path); 6 | 7 | // https://s2.sidewalklabs.com/regioncoverer/?center=40.719527%2C-73.960561&zoom=13&cells=89c25915%2C89c25917%2C89c25919%2C89c2593%2C89c2595%2C89c25967f%2C89c2596c%2C89c25eb4%2C89c25ecc%2C89c25ed4 8 | const tokens = [ 9 | '89c25915', '89c25917', '89c25919', '89c2593', 10 | '89c2595', '89c25967f', '89c2596c', '89c25eb4', 11 | '89c25ecc', '89c25ed4' 12 | ]; 13 | 14 | const ids = [ 15 | 9926594475407179776n, 16 | 9926594483997114368n, 17 | 9926594492587048960n, 18 | 9926594591371296768n, 19 | 9926594728810250240n, 20 | 9926594831621029888n, 21 | 9926594849069334528n, 22 | 9926600655865118720n, 23 | 9926600758944333824n, 24 | 9926600793304072192n, 25 | ]; 26 | 27 | test("CellUnion#constructor works", () => { 28 | const cellIds = tokens.map(t => s2.CellId.fromToken(t)); 29 | 30 | const cu1 = new s2.CellUnion(tokens); 31 | const cu2 = new s2.CellUnion(cellIds); 32 | 33 | expect(new Set(cu1.tokens())).toEqual(new Set(tokens)) ; 34 | expect(new Set(cu2.tokens())).toEqual(new Set(tokens)) ; 35 | expect(new Set(cu1.cellIds().map(id => id.token()))).toEqual(new Set(tokens)) ; 36 | expect(new Set(cu2.cellIds().map(id => id.token()))).toEqual(new Set(tokens)) ; 37 | expect(new Set(cu1.ids())).toEqual(new Set(ids)) ; 38 | expect(new Set(cu2.ids())).toEqual(new Set(ids)) ; 39 | }); 40 | 41 | test("CellUnion#contains works", () => { 42 | const cu = new s2.CellUnion(tokens); 43 | 44 | const pointInside = s2.CellId.fromToken('89c259475f3'); 45 | const pointOutside = s2.CellId.fromToken('89c25db57c1'); 46 | 47 | const unionInside = new s2.CellUnion([ 48 | '89c259380c', '89c259387', '89c25938c', '89c2593f54', '89c25940c', 49 | '89c259413', '89c259465', '89c25946c', '89c259474', '89c25947c' 50 | ]); 51 | 52 | const unionOutside = new s2.CellUnion([ 53 | '89c2595', '89c25bf', '89c25c1', '89c25c3', 54 | '89c25c404', '89c25c6c', '89c25dd', '89c25e5c', 55 | '89c25e7', '89c25ec' 56 | ]); 57 | 58 | expect(cu.contains(pointInside)).toBe(true) ; 59 | expect(cu.contains(pointOutside)).toBe(false) ; 60 | expect(cu.contains(unionInside)).toBe(true) ; 61 | expect(cu.contains(unionOutside)).toBe(false) ; 62 | }); 63 | 64 | test("CellUnion#intersects works", () => { 65 | const cu = new s2.CellUnion(tokens); 66 | 67 | const pointInside = s2.CellId.fromToken('89c25915'); 68 | 69 | const unionInside = new s2.CellUnion([ 70 | '89c259380c', '89c259387', '89c25938c', '89c2593f54', 71 | '89c25940c', '89c259413', '89c259465', '89c25946c', 72 | '89c259474', '89c25947c' 73 | ]); 74 | 75 | const unionIntersects = new s2.CellUnion([ 76 | '89c25935','89c25937','89c25939','89c2593f','89c25944', 77 | '89c2594c','89c25eb34','89c25eb5','89c25ecc' 78 | ]); 79 | 80 | const unionOutside = new s2.CellUnion([ 81 | '89c25d8c', '89c25df', '89c25e1', '89c25e3', 82 | '89c2604', '89c260c', '89c2614', '89c261b', 83 | '89c266c', '89c2674' 84 | ]); 85 | 86 | expect(cu.contains(pointInside)).toBe(true) ; 87 | expect(cu.contains(unionInside)).toBe(true) ; 88 | expect(cu.contains(unionIntersects)).toBe(true) ; 89 | expect(cu.contains(unionOutside)).toBe(false) ; 90 | }); 91 | 92 | test("CellUnion#union works", () => { 93 | const area1 = new s2.CellUnion([ 94 | '89c25be5','89c25be64','89c25be684','89c25bec','89c25bf4', 95 | '89c25bfc','89c25c01','89c25c07','89c25c09','89c25c0b' 96 | ]); 97 | 98 | const area2 = new s2.CellUnion([ 99 | '89c25c2b','89c25c2d','89c25c34','89c25dd','89c25de4', 100 | '89c25dec','89c25e0c','89c25e74','89c25e7c','89c25e81' 101 | ]); 102 | 103 | const unioned = area1.union(area2); 104 | expect(new Set(unioned.tokens())).toEqual(new Set([ 105 | '89c25be5','89c25be64','89c25be684','89c25bec', 106 | '89c25bf4','89c25bfc', '89c25c01','89c25c07', 107 | '89c25c09','89c25c0b','89c25c2b','89c25c2d', 108 | '89c25c34','89c25dd','89c25de4','89c25dec', 109 | '89c25e0c','89c25e74','89c25e7c','89c25e81' 110 | ])); 111 | }); 112 | 113 | test("CellUnion#intersection works", () => { 114 | const area1 = new s2.CellUnion([ 115 | '89c25be5','89c25be64','89c25be684','89c25bec','89c25bf4', 116 | '89c25bfc','89c25c01','89c25c07','89c25c09','89c25c0b' 117 | ]); 118 | 119 | const area2 = new s2.CellUnion([ 120 | '89c25c2b','89c25c2d','89c25c34','89c25dd','89c25de4', 121 | '89c25dec','89c25e0c','89c25e74','89c25e7c','89c25e81' 122 | ]); 123 | 124 | // area 1 and area2 don't intersect 125 | const intersection1 = area1.intersection(area2); 126 | expect(new Set(intersection1.tokens())).toEqual(new Set()); 127 | 128 | // area 1 should intersect after union 129 | const unioned = area1.union(area2); 130 | const intersection2 = unioned.intersection(area1); 131 | expect(new Set(intersection2.tokens())).toEqual(new Set(area1.tokens())); 132 | 133 | // area3 is inside area1 134 | const area3 = new s2.CellUnion([ 135 | '89c25be5','89c25be64','89c25be684','89c25bec','89c25bf4', 136 | ]); 137 | const intersection3 = area1.intersection(area3); 138 | expect(new Set(intersection3.tokens())).toEqual(new Set(area3.tokens())); 139 | }); 140 | 141 | test("CellUnion#difference works", () => { 142 | const area1 = new s2.CellUnion(['89c25be5','89c25be64','89c25be684','89c25bec','89c25bf4']); 143 | const area2 = new s2.CellUnion(['89c25be5','89c25be64','89c25be684','89c25bec','89c25bf5']); 144 | 145 | const difference = area1.difference(area2); 146 | 147 | // diff should get the 3 cells in the rest of the quadrant 148 | expect(new Set(difference.tokens())) 149 | .toEqual(new Set(['89c25bf1','89c25bf3','89c25bf7'])); 150 | }); 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/radarlabs/s2.svg?style=svg&circle-token=ed5b9fcba959e9b786eb5e8d714f9715253e020c)](https://circleci.com/gh/radarlabs/s2) 2 | 3 | Node.js JavaScript & TypeScript bindings for [Google S2](http://s2geometry.io/). 4 | 5 | ## What is S2? 6 | 7 | S2 is a library from Google for easily storing, indexing, and retrieving geographic locations. 8 | 9 | Geographic regions can be indexed by S2 cell ids of various levels in a data store and then later retrieved by these ids for extremely quick geolocation lookups. 10 | 11 | ## The Library 12 | 13 | The goal of this library is to maintain Node.js TypeScript bindings for the latest version of [Google's C++ S2 library](https://github.com/google/s2geometry). 14 | 15 | Other JavaScript projects available on GitHub appear unmaintained. 16 | 17 | The project has been built against Node's N-API, meaning that it's compatible across Node.js versions that support BigInt. 18 | This means that Node.js version 9 and below are unsupported. 19 | 20 | As of today, the library is built and tested against Node.js 16, 18 and 20. The library has been in production use at [Radar](https://radar.io) and has been built against OS X and Linux. Feel free to open an issue or PR if you'd like other platform support. 21 | 22 | See [test.sh](https://github.com/radarlabs/s2/blob/master/test.sh) for more details. 23 | 24 | ## Usage 25 | 26 | To install: 27 | 28 | ``` 29 | npm install @radarlabs/s2 30 | ``` 31 | 32 | To run tests (you'll need Docker): 33 | ```bash 34 | ./test.sh 35 | ``` 36 | 37 | S2 Cells can be generated from BigInt S2 IDs or string tokens: 38 | 39 | ```javascript 40 | const s2 = require('@radarlabs/s2'); 41 | 42 | const cell1 = new s2.CellId(9926595695177891840n); 43 | console.log(cell1.token()); 44 | > 89c25a31 45 | 46 | const cell2 = new s2.CellId('89c25a31'); 47 | console.log(cell2.id()); 48 | > 9926595695177891840n 49 | 50 | ``` 51 | 52 | To generate a covering for a given area: 53 | 54 | ```javascript 55 | const s2 = require('@radarlabs/s2'); 56 | 57 | # an array of lat/lng pairs representing a region (a part of Brooklyn, in this case) 58 | const loopLLs = [[40.70113825399865,-73.99229764938354],[40.70113825399865,-73.98766279220581],[40.70382234072197,-73.98766279220581],[40.70382234072197,-73.99229764938354]]; 59 | 60 | # map to an array of normalized s2.LatLng 61 | const s2LLs = loopLLs.map(([lat, lng]) => (new s2.LatLng(lat, lng))); 62 | 63 | # generate s2 cells to cover this polygon 64 | const s2level = 14; 65 | const covering = s2.RegionCoverer.getCoveringTokens(s2LLs, { min: s2level, max: s2level }); 66 | covering.forEach(c => console.log(c)); 67 | 68 | > 89c25a31 69 | > 89c25a33 70 | > 89c25a35 71 | > 89c25a37 72 | 73 | # check if a point is contained inside this region 74 | const point = new s2.CellId(new s2.LatLng(40.70248844447621, -73.98991584777832)); 75 | const pointAtLevel14 = point.parent(s2level); 76 | console.log(pointAtLevel14.token()); 77 | > 89c25a31 78 | 79 | const coveringSet = new Set(covering); 80 | console.log(coveringSet.contains(pointAtLevel14.token())); 81 | > true 82 | 83 | ``` 84 | 85 | To generate a covering for a given radius around a point: 86 | 87 | ```javascript 88 | const s2 = require('@radarlabs/s2'); 89 | 90 | # make an S2 latlng object for downtown San Diego 91 | const s2LatLong = new s2.LatLng(32.715651, -117.160542); 92 | 93 | # set cell covering options so the biggest region is a 6 and smallest is a 13, and limit to 10 cells 94 | const cellCoveringOptions = {min: 6, max: 13, max_cells: 10}; 95 | 96 | # get the cells (with the size range allowed) covering a 10,000 meter search radius centered on the given location 97 | # Note that this call returns a CellUnion object instead of a list of tokens, which is useful for comparisons 98 | const coveringCells = s2.RegionCoverer.getRadiusCovering(s2LatLong, 10000, cellCoveringOptions); 99 | # For this example though, we'll loop over the cellIds within the CellUnion and get their tokens 100 | console.log(coveringCells.cellIds().map((cellId) => cellId.token())); 101 | 102 | > 80d94d 103 | > 80d951 104 | > 80d953 105 | > 80d955 106 | > 80d956c 107 | > 80dbffc 108 | > 80dc01 109 | > 80deab 110 | > 80dead 111 | > 80deb2ac 112 | 113 | # the "coveringCells" CellUnion is like the "coveringSet" from the previous example, so can be used directly without converting to a set 114 | # test by checking a cell centered at our lat long 115 | console.log(coveringCells.has(new s2.CellId(s2LatLong))); 116 | > true 117 | 118 | ``` 119 | Here's a [visualization](http://s2.sidewalklabs.com/regioncoverer/?center=32.716657%2C-117.180841&zoom=11&cells=80d94d%2C80d951%2C80d953%2C80d955%2C80d956c%2C80dbffc%2C80dc01%2C80deab%2C80dead%2C80deb2ac) of the above set of covering cells. The center of the 10k radius is in downtown San Diego. 120 | 121 | 122 | Check if a cell is contained in another: 123 | 124 | ```javascript 125 | const c1 = s2.CellId.fromToken('89c25a37') 126 | const c2 = s2.CellId.fromToken('89c25') 127 | c2.contains(c1) 128 | > true 129 | c1.contains(c2) 130 | > false 131 | ``` 132 | 133 | If you'd like to see more functionality, feel free to open an issue or create a pull request. 134 | 135 | More detailed usage can be found in the [tests](https://github.com/radarlabs/s2/tree/master/test) folder. 136 | 137 | ## Versioning 138 | 139 | The Node S2 is library is at its infancy, so APIs are likely to change. 140 | In order to help with versioning, we publish TypeScript bindings so that your compiler can check 141 | if anything has changed. To keep up with updates, see [CHANGELOG.md](https://github.com/radarlabs/s2/blob/master/CHANGELOG.md) 142 | 143 | ## Resources 144 | 145 | - [Introductory blog post from Radar](https://radar.io/blog/open-source-node-js-typescript-s2-library) 146 | - [A detailed explanation on the S2 library](http://s2geometry.io/) 147 | - [Sidewalk Lab's S2 map for visualizing S2 tokens](https://s2.sidewalklabs.com/regioncoverer/) 148 | -------------------------------------------------------------------------------- /src/earth.cc: -------------------------------------------------------------------------------- 1 | #include "earth.h" 2 | 3 | Napi::FunctionReference Earth::constructor; 4 | 5 | Napi::Object Earth::Init(Napi::Env env, Napi::Object exports) { 6 | Napi::HandleScope scope(env); 7 | 8 | Napi::Function func = DefineClass(env, "Earth", { 9 | StaticMethod("toMeters", &Earth::ToMeters), 10 | StaticMethod("toKm", &Earth::ToKm), 11 | StaticMethod("getRadians", &Earth::GetRadians), 12 | StaticMethod("getDegrees", &Earth::GetDegrees), 13 | StaticMethod("getDistanceKm", &Earth::GetDistanceKm), 14 | StaticMethod("getDistanceMeters", &Earth::GetDistanceMeters), 15 | StaticMethod("getInitalBearingDegrees", &Earth::GetInitalBearingDegrees), 16 | }); 17 | 18 | constructor = Napi::Persistent(func); 19 | constructor.SuppressDestruct(); 20 | 21 | exports.Set("Earth", func); 22 | return exports; 23 | } 24 | 25 | Earth::Earth(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) { 26 | Napi::Env env = info.Env(); 27 | Napi::HandleScope scope(env); 28 | } 29 | 30 | bool Earth::PreCheck(const Napi::CallbackInfo &info, S2LatLng &latlng1, S2LatLng &latlng2){ 31 | int length = info.Length(); 32 | 33 | if (length < 2 || !info[0].IsObject() || !info[1].IsObject()){ 34 | return false; 35 | } 36 | 37 | Napi::Object objectA = info[0].As(); 38 | bool isLatlng1 = objectA.InstanceOf(LatLng::constructor.Value()); 39 | if (!isLatlng1) { 40 | return false; 41 | } 42 | 43 | LatLng* ll1 = LatLng::Unwrap(objectA); 44 | latlng1 = ll1->Get(); 45 | 46 | Napi::Object objectB = info[1].As(); 47 | bool isLatlng2 = objectB.InstanceOf(LatLng::constructor.Value()); 48 | if (!isLatlng2) { 49 | return false; 50 | } 51 | 52 | LatLng* ll2 = LatLng::Unwrap(objectB); 53 | latlng2 = ll2->Get(); 54 | 55 | return true; 56 | } 57 | 58 | Napi::Value Earth::ToMeters(const Napi::CallbackInfo &info){ 59 | Napi::Env env = info.Env(); 60 | 61 | S2LatLng s2LatLngA; 62 | S2LatLng s2LatLngB; 63 | 64 | bool isValid = Earth::PreCheck(info, s2LatLngA, s2LatLngB); 65 | if (!isValid){ 66 | Napi::TypeError::New(env, "Expected arguments (a: LatLng, b: LatLng)").ThrowAsJavaScriptException(); 67 | return env.Null(); 68 | } 69 | 70 | S1Angle s1Angle = S1Angle(s2LatLngA, s2LatLngB); 71 | double distance = S2Earth::ToMeters(s1Angle); 72 | 73 | return Napi::Number::New(env, distance); 74 | } 75 | 76 | Napi::Value Earth::ToKm(const Napi::CallbackInfo &info){ 77 | Napi::Env env = info.Env(); 78 | 79 | S2LatLng s2LatLngA; 80 | S2LatLng s2LatLngB; 81 | 82 | bool isValid = Earth::PreCheck(info, s2LatLngA, s2LatLngB); 83 | if (!isValid){ 84 | Napi::TypeError::New(env, "Expected arguments (a: LatLng, b: LatLng)").ThrowAsJavaScriptException(); 85 | return env.Null(); 86 | } 87 | 88 | S1Angle s1Angle = S1Angle(s2LatLngA, s2LatLngB); 89 | 90 | double distance = S2Earth::ToKm(s1Angle); 91 | 92 | return Napi::Number::New(env, distance); 93 | } 94 | 95 | Napi::Value Earth::GetRadians(const Napi::CallbackInfo &info){ 96 | Napi::Env env = info.Env(); 97 | 98 | S2LatLng s2LatLngA; 99 | S2LatLng s2LatLngB; 100 | 101 | bool isValid = Earth::PreCheck(info, s2LatLngA, s2LatLngB); 102 | if (!isValid) { 103 | Napi::TypeError::New(env, "Expected arguments (a: LatLng, b: LatLng)").ThrowAsJavaScriptException(); 104 | return env.Null(); 105 | } 106 | 107 | S1Angle s1Angle = S1Angle(s2LatLngA, s2LatLngB); 108 | 109 | double radians = s1Angle.radians(); 110 | return Napi::Number::New(env, radians); 111 | } 112 | 113 | Napi::Value Earth::GetDegrees(const Napi::CallbackInfo &info){ 114 | Napi::Env env = info.Env(); 115 | 116 | S2LatLng s2LatLngA; 117 | S2LatLng s2LatLngB; 118 | 119 | bool isValid = Earth::PreCheck(info, s2LatLngA, s2LatLngB); 120 | if (!isValid){ 121 | Napi::TypeError::New(env, "Expected arguments (a: LatLng, b: LatLng)").ThrowAsJavaScriptException(); 122 | return env.Null(); 123 | } 124 | 125 | S1Angle s1Angle = S1Angle(s2LatLngA, s2LatLngB); 126 | 127 | double degrees = s1Angle.degrees(); 128 | return Napi::Number::New(env, degrees); 129 | } 130 | 131 | Napi::Value Earth::GetDistanceKm(const Napi::CallbackInfo &info){ 132 | Napi::Env env = info.Env(); 133 | 134 | S2LatLng s2LatLngA; 135 | S2LatLng s2LatLngB; 136 | 137 | bool isValid = Earth::PreCheck(info, s2LatLngA, s2LatLngB); 138 | if (!isValid){ 139 | Napi::TypeError::New(env, "Expected arguments (a: LatLng, b: LatLng)").ThrowAsJavaScriptException(); 140 | return env.Null(); 141 | } 142 | 143 | double distance = S2Earth::GetDistanceKm(s2LatLngA, s2LatLngB); 144 | 145 | return Napi::Number::New(env, distance); 146 | } 147 | 148 | Napi::Value Earth::GetDistanceMeters(const Napi::CallbackInfo &info){ 149 | Napi::Env env = info.Env(); 150 | 151 | S2LatLng s2LatLngA; 152 | S2LatLng s2LatLngB; 153 | 154 | bool isValid = Earth::PreCheck(info, s2LatLngA, s2LatLngB); 155 | if (!isValid){ 156 | Napi::TypeError::New(env, "Expected arguments (a: LatLng, b: LatLng)").ThrowAsJavaScriptException(); 157 | return env.Null(); 158 | } 159 | 160 | double distance = S2Earth::GetDistanceMeters(s2LatLngA, s2LatLngB); 161 | 162 | return Napi::Number::New(env, distance); 163 | } 164 | 165 | Napi::Value Earth::GetInitalBearingDegrees(const Napi::CallbackInfo &info){ 166 | Napi::Env env = info.Env(); 167 | 168 | S2LatLng s2LatLngA; 169 | S2LatLng s2LatLngB; 170 | 171 | bool isValid = Earth::PreCheck(info, s2LatLngA, s2LatLngB); 172 | if (!isValid){ 173 | Napi::TypeError::New(env, "Expected arguments (a: LatLng, b: LatLng)").ThrowAsJavaScriptException(); 174 | return env.Null(); 175 | } 176 | 177 | S1Angle s1Angle = S2Earth::GetInitialBearing(s2LatLngA, s2LatLngB); 178 | 179 | double degrees = s1Angle.degrees(); 180 | return Napi::Number::New(env, degrees); 181 | } 182 | 183 | S2Earth Earth::Get() { 184 | return this->s2earth; 185 | } 186 | -------------------------------------------------------------------------------- /src/polyline.cc: -------------------------------------------------------------------------------- 1 | #include "polyline.h" 2 | 3 | Napi::FunctionReference Polyline::constructor; 4 | 5 | Napi::Object Polyline::Init(Napi::Env env, Napi::Object exports) { 6 | Napi::HandleScope scope(env); 7 | 8 | Napi::Function func = DefineClass(env, "Polyline", { 9 | InstanceMethod("contains", &Polyline::Contains), 10 | InstanceMethod("nearlyCovers", &Polyline::NearlyCovers), 11 | InstanceMethod("getLength", &Polyline::GetLength), 12 | InstanceMethod("getCentroid", &Polyline::GetCentroid), 13 | InstanceMethod("interpolate", &Polyline::Interpolate), 14 | InstanceMethod("project", &Polyline::Project), 15 | }); 16 | 17 | constructor = Napi::Persistent(func); 18 | constructor.SuppressDestruct(); 19 | 20 | exports.Set("Polyline", func); 21 | return exports; 22 | } 23 | 24 | Polyline::Polyline(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) { 25 | Napi::Env env = info.Env(); 26 | Napi::HandleScope scope(env); 27 | 28 | int length = info.Length(); 29 | 30 | if (length <= 0 || !info[0].IsArray()) { 31 | Napi::TypeError::New(env, "(latlngs: s2.LatLng[]) expected.").ThrowAsJavaScriptException(); 32 | return; 33 | } 34 | 35 | Napi::Array llArray = info[0].As(); 36 | uint32_t arrayLength = llArray.Length(); 37 | if (arrayLength <= 0) { 38 | Napi::RangeError::New(env, "(latlngs: s2.LatLng[]) was empty.").ThrowAsJavaScriptException(); 39 | return; 40 | } 41 | 42 | std::vector pointVector; 43 | for (uint32_t i = 0; i < arrayLength; i++) { 44 | Napi::Value obj = llArray[i]; 45 | LatLng* ll = LatLng::Unwrap(obj.As()); 46 | S2Point point = ll->Get().Normalized().ToPoint().Normalize(); 47 | pointVector.push_back(point); 48 | } 49 | 50 | this->s2polyline = S2Polyline(pointVector, S2Debug::ALLOW); 51 | 52 | S2Error error; 53 | if (this->s2polyline.FindValidationError(&error)) { 54 | Napi::Error::New(env, StringPrintf("Polyline is invalid: %s", error.text().c_str())).ThrowAsJavaScriptException(); 55 | return; 56 | } 57 | } 58 | 59 | Napi::Value Polyline::Contains(const Napi::CallbackInfo& info){ 60 | bool contains = false; 61 | 62 | Napi::Env env = info.Env(); 63 | int length = info.Length(); 64 | 65 | if (length < 1 || !info[0].IsObject()){ 66 | Napi::TypeError::New(env, "expected cell").ThrowAsJavaScriptException(); 67 | return env.Null(); 68 | } 69 | 70 | Napi::Object object = info[0].As(); 71 | bool isCell = object.InstanceOf(Cell::constructor.Value()); 72 | if (!isCell) { 73 | Napi::TypeError::New(env, "Cell expected.").ThrowAsJavaScriptException(); 74 | return env.Null(); 75 | } 76 | 77 | Cell* cell = Cell::Unwrap(object); 78 | S2Cell s2cell = cell->Get(); 79 | 80 | contains = this->s2polyline.Contains(s2cell); 81 | 82 | return Napi::Boolean::New(info.Env(), contains); 83 | } 84 | 85 | Napi::Value Polyline::NearlyCovers(const Napi::CallbackInfo& info){ 86 | bool isCover = false; 87 | 88 | Napi::Env env = info.Env(); 89 | int length = info.Length(); 90 | 91 | if (length != 2 || !info[0].IsObject() || !info[1].IsNumber()){ 92 | Napi::TypeError::New(env, "(polyline: Polyline, margin: number) expected.").ThrowAsJavaScriptException(); 93 | return env.Null(); 94 | } 95 | 96 | Napi::Object object = info[0].As(); 97 | bool isPolyline = object.InstanceOf(Polyline::constructor.Value()); 98 | if (!isPolyline) { 99 | Napi::TypeError::New(env, "(polyline: Polyline, margin: number) expected.").ThrowAsJavaScriptException(); 100 | return env.Null(); 101 | } 102 | 103 | double max_error_degree = info[1].As().DoubleValue(); 104 | S1Angle max_error = S1Angle::Degrees(max_error_degree); 105 | 106 | Polyline* polyline = Polyline::Unwrap(object); 107 | S2Polyline* s2polyline = polyline->Get(); 108 | 109 | isCover = this->s2polyline.NearlyCovers(*s2polyline, max_error); 110 | 111 | return Napi::Boolean::New(info.Env(), isCover); 112 | } 113 | 114 | S2Polyline* Polyline::Get() { 115 | return this->s2polyline.Clone(); 116 | } 117 | 118 | Napi::Value Polyline::GetLength(const Napi::CallbackInfo& info){ 119 | double length = 1; 120 | Napi::Env env = info.Env(); 121 | S1Angle s1Angle = this->s2polyline.GetLength(); 122 | length = S2Earth::ToMeters(s1Angle); 123 | 124 | return Napi::Number::New(env, length); 125 | } 126 | 127 | Napi::Value Polyline::GetCentroid(const Napi::CallbackInfo& info){ 128 | S2Point s2point = this->s2polyline.GetCentroid(); 129 | S2LatLng s2latlng = S2LatLng(s2point); 130 | return LatLng::NewInstance( 131 | Napi::Number::New(info.Env(), s2latlng.lat().degrees()), 132 | Napi::Number::New(info.Env(), s2latlng.lng().degrees()) 133 | ); 134 | } 135 | 136 | Napi::Value Polyline::Interpolate(const Napi::CallbackInfo& info){ 137 | Napi::Env env = info.Env(); 138 | int length = info.Length(); 139 | 140 | if (length != 1 || !info[0].IsNumber()){ 141 | Napi::TypeError::New(env, "expected fraction").ThrowAsJavaScriptException(); 142 | return env.Null(); 143 | } 144 | 145 | double fraction = info[0].As().DoubleValue(); 146 | S2Point s2point = this->s2polyline.Interpolate(fraction); 147 | S2LatLng s2latlng = S2LatLng(s2point); 148 | return LatLng::NewInstance( 149 | Napi::Number::New(info.Env(), s2latlng.lat().degrees()), 150 | Napi::Number::New(info.Env(), s2latlng.lng().degrees()) 151 | ); 152 | } 153 | 154 | Napi::Value Polyline::Project(const Napi::CallbackInfo& info){ 155 | Napi::Env env = info.Env(); 156 | int length = info.Length(); 157 | 158 | if (length != 1 || !info[0].IsObject()) { 159 | Napi::TypeError::New(env, "(ll: LatLng) expected.").ThrowAsJavaScriptException(); 160 | return env.Null(); 161 | } 162 | 163 | Napi::Object object = info[0].As(); 164 | bool isLatlng = object.InstanceOf(LatLng::constructor.Value()); 165 | if (!isLatlng) { 166 | Napi::TypeError::New(env, "(ll: LatLng) expected.").ThrowAsJavaScriptException(); 167 | return env.Null(); 168 | } 169 | 170 | LatLng* ll = LatLng::Unwrap(object); 171 | S2LatLng sourceLatLng = ll->Get(); 172 | 173 | int index; 174 | S2Point s2point = this->s2polyline.Project(sourceLatLng.ToPoint(), &index); 175 | S2LatLng s2latlng = S2LatLng(s2point); 176 | 177 | Napi::Object newPoint = LatLng::NewInstance( 178 | Napi::Number::New(info.Env(), s2latlng.lat().degrees()), 179 | Napi::Number::New(info.Env(), s2latlng.lng().degrees()) 180 | ); 181 | 182 | Napi::Object returnObj = Napi::Object::New(env); 183 | returnObj.Set("point", newPoint); 184 | returnObj.Set("index", index); 185 | return returnObj; 186 | } 187 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/3m/0td_0c9j1qxcc0htbz_crjlr0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | // coverageDirectory: "coverage", 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: null, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: null, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // An array of directory names to be searched recursively up from the requiring module's location 64 | // moduleDirectories: [ 65 | // "node_modules" 66 | // ], 67 | 68 | // An array of file extensions your modules use 69 | // moduleFileExtensions: [ 70 | // "js", 71 | // "json", 72 | // "jsx", 73 | // "ts", 74 | // "tsx", 75 | // "node" 76 | // ], 77 | 78 | // A map from regular expressions to module names that allow to stub out resources with a single module 79 | // moduleNameMapper: {}, 80 | 81 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 82 | // modulePathIgnorePatterns: [], 83 | 84 | // Activates notifications for test results 85 | // notify: false, 86 | 87 | // An enum that specifies notification mode. Requires { notify: true } 88 | // notifyMode: "failure-change", 89 | 90 | // A preset that is used as a base for Jest's configuration 91 | // preset: null, 92 | 93 | // Run tests from one or more projects 94 | // projects: null, 95 | 96 | // Use this configuration option to add custom reporters to Jest 97 | // reporters: undefined, 98 | 99 | // Automatically reset mock state between every test 100 | // resetMocks: false, 101 | 102 | // Reset the module registry before running each individual test 103 | // resetModules: false, 104 | 105 | // A path to a custom resolver 106 | // resolver: null, 107 | 108 | // Automatically restore mock state between every test 109 | // restoreMocks: false, 110 | 111 | // The root directory that Jest should scan for tests and modules within 112 | // rootDir: null, 113 | 114 | // A list of paths to directories that Jest should use to search for files in 115 | // roots: [ 116 | // "" 117 | // ], 118 | 119 | // Allows you to use a custom runner instead of Jest's default test runner 120 | // runner: "jest-runner", 121 | 122 | // The paths to modules that run some code to configure or set up the testing environment before each test 123 | // setupFiles: [], 124 | 125 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 126 | // setupFilesAfterEnv: [], 127 | 128 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 129 | // snapshotSerializers: [], 130 | 131 | // The test environment that will be used for testing 132 | testEnvironment: "node", 133 | 134 | // Options that will be passed to the testEnvironment 135 | // testEnvironmentOptions: {}, 136 | 137 | // Adds a location field to test results 138 | // testLocationInResults: false, 139 | 140 | // The glob patterns Jest uses to detect test files 141 | testMatch: [ 142 | "**/test/**/*.test.js" 143 | ], 144 | 145 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 146 | // testPathIgnorePatterns: [ 147 | // "/node_modules/" 148 | // ], 149 | 150 | // The regexp pattern or array of patterns that Jest uses to detect test files 151 | // testRegex: [], 152 | 153 | // This option allows the use of a custom results processor 154 | // testResultsProcessor: null, 155 | 156 | // This option allows use of a custom test runner 157 | // testRunner: "jasmine2", 158 | 159 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 160 | // testURL: "http://localhost", 161 | 162 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 163 | // timers: "real", 164 | 165 | // A map from regular expressions to paths to transformers 166 | transform: {}, 167 | 168 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 169 | // transformIgnorePatterns: [ 170 | // "/node_modules/" 171 | // ], 172 | 173 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 174 | // unmockedModulePathPatterns: undefined, 175 | 176 | // Indicates whether each individual test should be reported during the run 177 | // verbose: null, 178 | 179 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 180 | // watchPathIgnorePatterns: [], 181 | 182 | // Whether to use watchman for file crawling 183 | // watchman: true, 184 | }; 185 | -------------------------------------------------------------------------------- /src/cell_id.cc: -------------------------------------------------------------------------------- 1 | #include "cell_id.h" 2 | 3 | Napi::FunctionReference CellId::constructor; 4 | 5 | Napi::Object CellId::Init(Napi::Env env, Napi::Object exports) { 6 | Napi::HandleScope scope(env); 7 | 8 | Napi::Function func = DefineClass(env, "CellId", { 9 | InstanceMethod("id", &CellId::Id), 10 | InstanceMethod("idString", &CellId::IdString), 11 | InstanceMethod("token", &CellId::Token), 12 | InstanceMethod("contains", &CellId::Contains), 13 | InstanceMethod("intersects", &CellId::Intersects), 14 | InstanceMethod("parent", &CellId::Parent), 15 | InstanceMethod("child", &CellId::Child), 16 | InstanceMethod("next", &CellId::Next), 17 | InstanceMethod("level", &CellId::Level), 18 | InstanceMethod("isLeaf", &CellId::IsLeaf), 19 | InstanceMethod("rangeMin", &CellId::RangeMin), 20 | InstanceMethod("rangeMax", &CellId::RangeMax), 21 | StaticMethod("fromToken", &CellId::FromToken), 22 | }); 23 | 24 | constructor = Napi::Persistent(func); 25 | constructor.SuppressDestruct(); 26 | 27 | exports.Set("CellId", func); 28 | return exports; 29 | } 30 | 31 | CellId::CellId(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) { 32 | Napi::Env env = info.Env(); 33 | Napi::HandleScope scope(env); 34 | 35 | int length = info.Length(); 36 | string badArgs = "(id: bigint) | (token: string) | (ll: LatLng) expected."; 37 | 38 | if ( 39 | length <= 0 40 | || ( 41 | !info[0].IsString() 42 | && !info[0].IsObject() 43 | && !info[0].IsBigInt() 44 | && !info[0].IsExternal() 45 | ) 46 | ) { 47 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 48 | return; 49 | } 50 | 51 | if (info[0].IsString()) { // token: string 52 | Napi::String tokenString = info[0].As(); 53 | this->s2cellid = S2CellId::FromToken(tokenString); 54 | if (!this->s2cellid.is_valid()) { 55 | Napi::TypeError::New(env, "Invalid token").ThrowAsJavaScriptException(); 56 | return; 57 | } 58 | } else if (info[0].IsObject()) { // ll: s2.LatLng 59 | Napi::Object object = info[0].As(); 60 | bool isLL = object.InstanceOf(LatLng::constructor.Value()); 61 | if (isLL) { 62 | LatLng* ll = LatLng::Unwrap(object); 63 | this->s2cellid = S2CellId(ll->Get()); 64 | } else { 65 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 66 | return; 67 | } 68 | } else if (info[0].IsBigInt()) { // id: int64 69 | Napi::BigInt id = info[0].As(); 70 | 71 | bool lossless; 72 | uint64_t s2id = id.Uint64Value(&lossless); 73 | 74 | if (lossless) { 75 | this->s2cellid = S2CellId(s2id); 76 | } else { 77 | Napi::TypeError::New(env, "S2 ID was lossy. This might be a malformed S2 ID.").ThrowAsJavaScriptException(); 78 | return; 79 | } 80 | } else if (info[0].IsExternal()) { // S2CellId C++ class 81 | Napi::External external = info[0].As>(); 82 | this->s2cellid = *external.Data(); 83 | } 84 | } 85 | 86 | Napi::Value CellId::Id(const Napi::CallbackInfo &info) { 87 | return Napi::BigInt::New(info.Env(), (uint64_t) s2cellid.id()); 88 | } 89 | 90 | Napi::Value CellId::IdString(const Napi::CallbackInfo &info) { 91 | std::ostringstream idStr; 92 | idStr << s2cellid.id(); 93 | return Napi::String::New(info.Env(), idStr.str()); 94 | } 95 | 96 | Napi::Value CellId::Token(const Napi::CallbackInfo &info) { 97 | return Napi::String::New(info.Env(), s2cellid.ToToken()); 98 | } 99 | 100 | Napi::Value CellId::Contains(const Napi::CallbackInfo &info) { 101 | bool contains = false; 102 | 103 | if (info.Length() > 0 && info[0].IsObject()) { 104 | Napi::Object object = info[0].As(); 105 | if (object.InstanceOf(CellId::constructor.Value())) { 106 | CellId* otherCellId = CellId::Unwrap(object); 107 | contains = s2cellid.contains(otherCellId->Get()); 108 | } 109 | } 110 | 111 | return Napi::Boolean::New(info.Env(), contains); 112 | } 113 | 114 | Napi::Value CellId::Intersects(const Napi::CallbackInfo &info) { 115 | bool intersects = false; 116 | 117 | if (info.Length() > 0 && info[0].IsObject()) { 118 | Napi::Object object = info[0].As(); 119 | if (object.InstanceOf(CellId::constructor.Value())) { 120 | CellId* otherCellId = CellId::Unwrap(object); 121 | intersects = s2cellid.intersects(otherCellId->Get()); 122 | } 123 | } 124 | 125 | return Napi::Boolean::New(info.Env(), intersects); 126 | } 127 | 128 | Napi::Value CellId::Parent(const Napi::CallbackInfo &info) { 129 | Napi::Env env = info.Env(); 130 | 131 | if (s2cellid.is_face()) { 132 | return info.This(); 133 | } 134 | 135 | if (info.Length() <= 0 || !info[0].IsNumber()) { 136 | S2CellId parent = s2cellid.parent(); 137 | return constructor.New({ Napi::External::New(env, &parent) }); 138 | } 139 | 140 | int level = info[0].As().Int32Value(); 141 | int finalLevel = level <= 0 ? 0 : level; 142 | 143 | S2CellId parent = s2cellid.parent(finalLevel); 144 | return constructor.New({ Napi::External::New(env, &parent) }); 145 | } 146 | 147 | Napi::Value CellId::Child(const Napi::CallbackInfo &info) { 148 | Napi::Env env = info.Env(); 149 | 150 | if (info.Length() <= 0 || !info[0].IsNumber()) { 151 | Napi::TypeError::New(info.Env(), "(position: number) expected.").ThrowAsJavaScriptException(); 152 | return env.Null(); 153 | } 154 | 155 | Napi::Number position = info[0].As(); 156 | S2CellId child = s2cellid.child(position.Int32Value()); 157 | 158 | if (!child.is_valid()) { 159 | return info.This(); 160 | } 161 | return constructor.New({ Napi::External::New(env, &child) }); 162 | } 163 | 164 | Napi::Value CellId::Next(const Napi::CallbackInfo &info) { 165 | Napi::Env env = info.Env(); 166 | 167 | S2CellId next = s2cellid.next(); 168 | 169 | if (!next.is_valid()) { 170 | return info.This(); 171 | } 172 | return constructor.New({ Napi::External::New(env, &next) }); 173 | } 174 | 175 | Napi::Value CellId::Level(const Napi::CallbackInfo &info) { 176 | return Napi::Number::New(info.Env(), s2cellid.level()); 177 | } 178 | 179 | Napi::Value CellId::IsLeaf(const Napi::CallbackInfo &info) { 180 | return Napi::Boolean::New(info.Env(), s2cellid.is_leaf()); 181 | } 182 | 183 | Napi::Value CellId::FromToken(const Napi::CallbackInfo &info) { 184 | Napi::Env env = info.Env(); 185 | if (info.Length() > 0 && info[0].IsString()) { 186 | Napi::String token = info[0].As(); 187 | S2CellId cellId = S2CellId::FromToken(token); 188 | if (cellId.is_valid()) { 189 | return constructor.New({ Napi::External::New(env, &cellId) }); 190 | } 191 | } 192 | Napi::TypeError::New(env, "(token: string) expected.").ThrowAsJavaScriptException(); 193 | return env.Null(); 194 | } 195 | 196 | Napi::Value CellId::RangeMin(const Napi::CallbackInfo &info) { 197 | Napi::Env env = info.Env(); 198 | S2CellId range = s2cellid.range_min(); 199 | return constructor.New({ Napi::External::New(env, &range) }); 200 | } 201 | 202 | Napi::Value CellId::RangeMax(const Napi::CallbackInfo &info) { 203 | Napi::Env env = info.Env(); 204 | S2CellId range = s2cellid.range_max(); 205 | return constructor.New({ Napi::External::New(env, &range) }); 206 | } 207 | 208 | S2CellId CellId::Get() { 209 | return s2cellid; 210 | } 211 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | # NOTE: 'module_name' and 'module_path' come from the 'binary' property in package.json 3 | # node-pre-gyp handles passing them down to node-gyp when you build from source 4 | "targets": [{ 5 | "target_name": "<(module_name)", 6 | 7 | "sources": [ 8 | "./src/s2.cc", 9 | 10 | "./src/builder.cc", 11 | "./src/cell.cc", 12 | "./src/cell_id.cc", 13 | "./src/earth.cc", 14 | "./src/latlng.cc", 15 | "./src/loop.cc", 16 | "./src/point.cc", 17 | "./src/polygon.cc", 18 | "./src/polyline.cc", 19 | "./src/region_coverer.cc", 20 | "./src/cell_union.cc", 21 | 22 | "./third_party/s2geometry/src/s2/base/stringprintf.cc", 23 | "./third_party/s2geometry/src/s2/third_party/absl/strings/ascii.cc", 24 | "./third_party/s2geometry/src/s2/util/coding/coder.cc", 25 | "./third_party/s2geometry/src/s2/util/math/exactfloat/exactfloat.cc", 26 | 27 | "./third_party/s2geometry/src/s2/encoded_s2cell_id_vector.cc", 28 | "./third_party/s2geometry/src/s2/encoded_s2point_vector.cc", 29 | "./third_party/s2geometry/src/s2/encoded_s2shape_index.cc", 30 | "./third_party/s2geometry/src/s2/encoded_string_vector.cc", 31 | "./third_party/s2geometry/src/s2/id_set_lexicon.cc", 32 | "./third_party/s2geometry/src/s2/mutable_s2shape_index.cc", 33 | "./third_party/s2geometry/src/s2/r2rect.cc", 34 | "./third_party/s2geometry/src/s2/s1angle.cc", 35 | "./third_party/s2geometry/src/s2/s1chord_angle.cc", 36 | "./third_party/s2geometry/src/s2/s1interval.cc", 37 | "./third_party/s2geometry/src/s2/s2boolean_operation.cc", 38 | "./third_party/s2geometry/src/s2/s2builder.cc", 39 | "./third_party/s2geometry/src/s2/s2builder_graph.cc", 40 | "./third_party/s2geometry/src/s2/s2builderutil_closed_set_normalizer.cc", 41 | "./third_party/s2geometry/src/s2/s2builderutil_find_polygon_degeneracies.cc", 42 | "./third_party/s2geometry/src/s2/s2builderutil_lax_polygon_layer.cc", 43 | "./third_party/s2geometry/src/s2/s2builderutil_s2point_vector_layer.cc", 44 | "./third_party/s2geometry/src/s2/s2builderutil_s2polygon_layer.cc", 45 | "./third_party/s2geometry/src/s2/s2builderutil_s2polyline_layer.cc", 46 | "./third_party/s2geometry/src/s2/s2builderutil_s2polyline_vector_layer.cc", 47 | "./third_party/s2geometry/src/s2/s2builderutil_snap_functions.cc", 48 | "./third_party/s2geometry/src/s2/s2cap.cc", 49 | "./third_party/s2geometry/src/s2/s2cell.cc", 50 | "./third_party/s2geometry/src/s2/s2cell_id.cc", 51 | "./third_party/s2geometry/src/s2/s2cell_index.cc", 52 | "./third_party/s2geometry/src/s2/s2cell_union.cc", 53 | "./third_party/s2geometry/src/s2/s2centroids.cc", 54 | "./third_party/s2geometry/src/s2/s2closest_cell_query.cc", 55 | "./third_party/s2geometry/src/s2/s2closest_edge_query.cc", 56 | "./third_party/s2geometry/src/s2/s2closest_point_query.cc", 57 | "./third_party/s2geometry/src/s2/s2contains_vertex_query.cc", 58 | "./third_party/s2geometry/src/s2/s2convex_hull_query.cc", 59 | "./third_party/s2geometry/src/s2/s2coords.cc", 60 | "./third_party/s2geometry/src/s2/s2crossing_edge_query.cc", 61 | "./third_party/s2geometry/src/s2/s2debug.cc", 62 | "./third_party/s2geometry/src/s2/s2earth.cc", 63 | "./third_party/s2geometry/src/s2/s2edge_clipping.cc", 64 | "./third_party/s2geometry/src/s2/s2edge_crosser.cc", 65 | "./third_party/s2geometry/src/s2/s2edge_crossings.cc", 66 | "./third_party/s2geometry/src/s2/s2edge_distances.cc", 67 | "./third_party/s2geometry/src/s2/s2edge_tessellator.cc", 68 | "./third_party/s2geometry/src/s2/s2error.cc", 69 | "./third_party/s2geometry/src/s2/s2furthest_edge_query.cc", 70 | "./third_party/s2geometry/src/s2/s2latlng.cc", 71 | "./third_party/s2geometry/src/s2/s2latlng_rect.cc", 72 | "./third_party/s2geometry/src/s2/s2latlng_rect_bounder.cc", 73 | "./third_party/s2geometry/src/s2/s2lax_loop_shape.cc", 74 | "./third_party/s2geometry/src/s2/s2lax_polygon_shape.cc", 75 | "./third_party/s2geometry/src/s2/s2lax_polyline_shape.cc", 76 | "./third_party/s2geometry/src/s2/s2loop.cc", 77 | "./third_party/s2geometry/src/s2/s2loop_measures.cc", 78 | "./third_party/s2geometry/src/s2/s2max_distance_targets.cc", 79 | "./third_party/s2geometry/src/s2/s2measures.cc", 80 | "./third_party/s2geometry/src/s2/s2metrics.cc", 81 | "./third_party/s2geometry/src/s2/s2min_distance_targets.cc", 82 | "./third_party/s2geometry/src/s2/s2padded_cell.cc", 83 | "./third_party/s2geometry/src/s2/s2point_compression.cc", 84 | "./third_party/s2geometry/src/s2/s2point_region.cc", 85 | "./third_party/s2geometry/src/s2/s2pointutil.cc", 86 | "./third_party/s2geometry/src/s2/s2polygon.cc", 87 | "./third_party/s2geometry/src/s2/s2polyline.cc", 88 | "./third_party/s2geometry/src/s2/s2polyline_alignment.cc", 89 | "./third_party/s2geometry/src/s2/s2polyline_measures.cc", 90 | "./third_party/s2geometry/src/s2/s2polyline_simplifier.cc", 91 | "./third_party/s2geometry/src/s2/s2predicates.cc", 92 | "./third_party/s2geometry/src/s2/s2projections.cc", 93 | "./third_party/s2geometry/src/s2/s2r2rect.cc", 94 | "./third_party/s2geometry/src/s2/s2region.cc", 95 | "./third_party/s2geometry/src/s2/s2region_coverer.cc", 96 | "./third_party/s2geometry/src/s2/s2region_intersection.cc", 97 | "./third_party/s2geometry/src/s2/s2region_term_indexer.cc", 98 | "./third_party/s2geometry/src/s2/s2region_union.cc", 99 | "./third_party/s2geometry/src/s2/s2shape_index.cc", 100 | "./third_party/s2geometry/src/s2/s2shape_index_buffered_region.cc", 101 | "./third_party/s2geometry/src/s2/s2shape_index_measures.cc", 102 | "./third_party/s2geometry/src/s2/s2shape_measures.cc", 103 | "./third_party/s2geometry/src/s2/s2shapeutil_build_polygon_boundaries.cc", 104 | "./third_party/s2geometry/src/s2/s2shapeutil_coding.cc", 105 | "./third_party/s2geometry/src/s2/s2shapeutil_contains_brute_force.cc", 106 | "./third_party/s2geometry/src/s2/s2shapeutil_edge_iterator.cc", 107 | "./third_party/s2geometry/src/s2/s2shapeutil_get_reference_point.cc", 108 | "./third_party/s2geometry/src/s2/s2shapeutil_range_iterator.cc", 109 | "./third_party/s2geometry/src/s2/s2shapeutil_visit_crossing_edge_pairs.cc", 110 | "./third_party/s2geometry/src/s2/s2text_format.cc", 111 | "./third_party/s2geometry/src/s2/s2wedge_relations.cc", 112 | ], 113 | "defines": [ 114 | "NAPI_CPP_EXCEPTIONS", 115 | "NAPI_EXPERIMENTAL", # required for bigint 116 | "NDEBUG", 117 | "NODE_ADDON_API_DISABLE_DEPRECATED" 118 | ], 119 | "include_dirs": [ 120 | "(info) { 27 | Napi::Env env = info.Env(); 28 | Napi::HandleScope scope(env); 29 | 30 | const int length = info.Length(); 31 | const string badArgs = "(cellIds: s2.CellId[]) expected."; 32 | 33 | if ( 34 | length <= 0 35 | || (!info[0].IsArray() && !info[0].IsExternal()) 36 | ) { 37 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 38 | return; 39 | } 40 | 41 | if (info[0].IsExternal()) { 42 | Napi::External external = info[0].As>(); 43 | if (!external.Data()->IsValid()) { 44 | Napi::TypeError::New(env, "S2 cell union is invalid.").ThrowAsJavaScriptException(); 45 | return; 46 | } 47 | this->s2cellunion = *external.Data(); 48 | } else if (info[0].IsArray()) { 49 | Napi::Array cellIdArray = info[0].As(); 50 | uint32_t arrayLength = cellIdArray.Length(); 51 | if (arrayLength <= 0) { 52 | Napi::RangeError::New(env, "(cellIds: s2.CellId[]) was empty.").ThrowAsJavaScriptException(); 53 | return; 54 | } 55 | 56 | std::vector cellIdVector; 57 | for (uint32_t i = 0; i < arrayLength; i++) { 58 | Napi::Value obj = cellIdArray[i]; 59 | if (obj.IsObject()) { 60 | CellId* cellId = CellId::Unwrap(obj.As()); 61 | cellIdVector.push_back(cellId->Get()); 62 | } else if (obj.IsString()) { 63 | Napi::String token = obj.As(); 64 | cellIdVector.push_back(S2CellId::FromToken(token.Utf8Value())); 65 | } 66 | } 67 | 68 | this->s2cellunion = S2CellUnion(std::move(cellIdVector)); 69 | } 70 | } 71 | 72 | S2CellUnion CellUnion::Get() { 73 | return this->s2cellunion; 74 | } 75 | 76 | Napi::Value CellUnion::Contains(const Napi::CallbackInfo &info) { 77 | const Napi::Env env = info.Env(); 78 | const string badArgs = "(other: s2.CellId | s2.CellUnion) expected."; 79 | bool contains = false; 80 | 81 | if (info.Length() > 0 && info[0].IsObject()) { 82 | Napi::Object object = info[0].As(); 83 | if (object.InstanceOf(CellUnion::constructor.Value())) { 84 | CellUnion* otherCellUnion = CellUnion::Unwrap(object); 85 | contains = s2cellunion.Contains(otherCellUnion->Get()); 86 | } else if (object.InstanceOf(CellId::constructor.Value())) { 87 | CellId* otherCellId = CellId::Unwrap(object); 88 | contains = s2cellunion.Contains(otherCellId->Get()); 89 | } else { 90 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 91 | return env.Null(); 92 | } 93 | } else { 94 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 95 | return env.Null(); 96 | } 97 | 98 | return Napi::Boolean::New(env, contains); 99 | } 100 | 101 | Napi::Value CellUnion::Intersects(const Napi::CallbackInfo &info) { 102 | const Napi::Env env = info.Env(); 103 | const string badArgs = "(other: s2.CellId | s2.CellUnion) expected."; 104 | bool intersects = false; 105 | 106 | if (info.Length() > 0 && info[0].IsObject()) { 107 | Napi::Object object = info[0].As(); 108 | if (object.InstanceOf(CellUnion::constructor.Value())) { 109 | CellUnion* otherCellUnion = CellUnion::Unwrap(object); 110 | intersects = s2cellunion.Intersects(otherCellUnion->Get()); 111 | } else if (object.InstanceOf(CellId::constructor.Value())) { 112 | CellId* otherCellId = CellId::Unwrap(object); 113 | intersects = s2cellunion.Intersects(otherCellId->Get()); 114 | } else { 115 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 116 | return env.Null(); 117 | } 118 | } else { 119 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 120 | return env.Null(); 121 | } 122 | 123 | return Napi::Boolean::New(env, intersects); 124 | } 125 | 126 | Napi::Value CellUnion::Union(const Napi::CallbackInfo &info) { 127 | const Napi::Env env = info.Env(); 128 | const string badArgs = "(other: s2.CellUnion) expected."; 129 | S2CellUnion s2Union; 130 | 131 | if (info.Length() > 0 && info[0].IsObject()) { 132 | Napi::Object object = info[0].As(); 133 | if (object.InstanceOf(CellUnion::constructor.Value())) { 134 | CellUnion* otherCellUnion = CellUnion::Unwrap(object); 135 | s2Union = s2cellunion.Union(otherCellUnion->Get()); 136 | } else { 137 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 138 | return env.Null(); 139 | } 140 | } else { 141 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 142 | return env.Null(); 143 | } 144 | 145 | return constructor.New({ 146 | Napi::External::New(env, &s2Union) 147 | }); 148 | } 149 | 150 | Napi::Value CellUnion::Intersection(const Napi::CallbackInfo &info) { 151 | const Napi::Env env = info.Env(); 152 | const string badArgs = "(other: s2.CellUnion) expected."; 153 | S2CellUnion s2Intersection; 154 | 155 | if (info.Length() > 0 && info[0].IsObject()) { 156 | Napi::Object object = info[0].As(); 157 | if (object.InstanceOf(CellUnion::constructor.Value())) { 158 | CellUnion* otherCellUnion = CellUnion::Unwrap(object); 159 | s2Intersection = s2cellunion.Intersection(otherCellUnion->Get()); 160 | } else { 161 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 162 | return env.Null(); 163 | } 164 | } else { 165 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 166 | return env.Null(); 167 | } 168 | 169 | return constructor.New({ 170 | Napi::External::New(env, new S2CellUnion(s2Intersection)) 171 | }); 172 | } 173 | 174 | Napi::Value CellUnion::Difference(const Napi::CallbackInfo &info) { 175 | const Napi::Env env = info.Env(); 176 | const string badArgs = "(other: s2.CellUnion) expected."; 177 | S2CellUnion s2Difference; 178 | 179 | if (info.Length() > 0 && info[0].IsObject()) { 180 | Napi::Object object = info[0].As(); 181 | if (object.InstanceOf(CellUnion::constructor.Value())) { 182 | CellUnion* otherCellUnion = CellUnion::Unwrap(object); 183 | s2Difference = s2cellunion.Difference(otherCellUnion->Get()); 184 | } else { 185 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 186 | return env.Null(); 187 | } 188 | } else { 189 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 190 | return env.Null(); 191 | } 192 | 193 | return constructor.New({ 194 | Napi::External::New(env, &s2Difference) 195 | }); 196 | } 197 | 198 | Napi::Value CellUnion::Ids(const Napi::CallbackInfo &info) { 199 | const Napi::Env env = info.Env(); 200 | 201 | uint32_t size = this->s2cellunion.size(); 202 | Napi::BigUint64Array returnedIds = Napi::BigUint64Array::New(env, size); 203 | 204 | for (uint32_t i = 0; i < size; i++) { 205 | S2CellId cellId = this->s2cellunion[i]; 206 | returnedIds[i] = cellId.id(); 207 | } 208 | 209 | return returnedIds; 210 | } 211 | 212 | Napi::Value CellUnion::CellIds(const Napi::CallbackInfo &info) { 213 | const Napi::Env env = info.Env(); 214 | 215 | uint32_t size = this->s2cellunion.size(); 216 | Napi::Array returnedIds = Napi::Array::New(env, size); 217 | 218 | for (uint32_t i = 0; i < size; i++) { 219 | S2CellId cellId = this->s2cellunion[i]; 220 | returnedIds[i] = CellId::constructor.New({ Napi::External::New(env, &cellId) }); 221 | } 222 | 223 | return returnedIds; 224 | } 225 | 226 | Napi::Value CellUnion::Tokens(const Napi::CallbackInfo &info) { 227 | const Napi::Env env = info.Env(); 228 | 229 | uint32_t size = this->s2cellunion.size(); 230 | Napi::Array returnedTokens = Napi::Array::New(env, size); 231 | 232 | for (uint32_t i = 0; i < size; i++) { 233 | returnedTokens[i] = Napi::String::New(env, this->s2cellunion[i].ToToken()); 234 | } 235 | 236 | return returnedTokens; 237 | } 238 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 Radar Labs, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /test/RegionCoverer.test.js: -------------------------------------------------------------------------------- 1 | // magic incantation from step 3 @ https://github.com/mapbox/node-pre-gyp#readme 2 | const binary = require('@mapbox/node-pre-gyp'); 3 | const path = require('path'); 4 | var binding_path = binary.find(path.resolve('./package.json')); 5 | const s2 = require(binding_path); 6 | 7 | // long, lat pairs of 11201 postal code 8 | const postalCode11201 = [[-73.995069,40.703102],[-73.995138,40.70298],[-73.996698,40.700877],[-73.9975,40.699653],[-73.998032,40.698762],[-73.998376,40.698048],[-73.998847,40.697141],[-73.998763,40.69711],[-73.998857,40.69692],[-73.99894,40.696941],[-73.998991,40.696812],[-73.999182,40.696885],[-73.999295,40.696795],[-73.999474,40.696704],[-73.999535,40.696578],[-73.999478,40.696466],[-73.999428,40.69641],[-73.99942,40.696361],[-73.999461,40.69628],[-73.999604,40.696069],[-73.999882,40.695605],[-74.000277,40.694959],[-74.000701,40.694388],[-74.000959,40.694069],[-74.001194,40.693484],[-74.001277,40.693298],[-74.001317,40.693208],[-74.001542,40.692786],[-74.001744,40.692407],[-74.001818,40.692222],[-74.001969,40.691843],[-74.001332,40.691225],[-74.000699,40.690612],[-74.00096,40.690132],[-74.001853,40.690388],[-74.001963,40.690136],[-74.002178,40.690206],[-74.002427,40.690273],[-74.002616,40.690324],[-74.002763,40.690364],[-74.002993,40.690427],[-74.003024,40.690349],[-74.003409,40.689613],[-74.00382,40.68893],[-74.003919,40.68886],[-74.004593,40.688215],[-74.005038,40.687561],[-74.00563,40.686784],[-74.005874,40.686207],[-74.00635,40.686285],[-74.006406,40.686289],[-74.006444,40.686296],[-74.006506,40.686314],[-74.006594,40.686347],[-74.006755,40.686413],[-74.006875,40.686464],[-74.006989,40.686509],[-74.00702,40.686526],[-74.007064,40.686504],[-74.00715,40.686475],[-74.007243,40.686449],[-74.007411,40.686388],[-74.007497,40.686344],[-74.007536,40.686315],[-74.007623,40.686285],[-74.00767,40.686269],[-74.00773,40.686254],[-74.007778,40.686245],[-74.007843,40.686238],[-74.007899,40.686251],[-74.007929,40.686276],[-74.00795,40.686293],[-74.008164,40.686175],[-74.008425,40.686016],[-74.008386,40.685969],[-74.008135,40.685615],[-74.005497,40.685847],[-74.004853,40.685668],[-74.004504,40.686301],[-74.002364,40.685737],[-74.002039,40.686404],[-74.001718,40.687069],[-74.001389,40.687751],[-74.001278,40.687715],[-74.000539,40.687481],[-74.000054,40.687318],[-73.999962,40.687286],[-73.999863,40.687251],[-73.999696,40.687198],[-73.998932,40.686948],[-73.998276,40.686729],[-73.998189,40.6867],[-73.997278,40.686398],[-73.996488,40.686138],[-73.996358,40.686096],[-73.99618,40.686037],[-73.995386,40.685775],[-73.994538,40.685495],[-73.994408,40.685453],[-73.994578,40.6851],[-73.992192,40.684178],[-73.991952,40.684084],[-73.989992,40.683319],[-73.989556,40.683956],[-73.989129,40.68459],[-73.988712,40.685224],[-73.988289,40.685869],[-73.987859,40.686504],[-73.987429,40.687138],[-73.987,40.687773],[-73.986568,40.688408],[-73.9844,40.687564],[-73.983988,40.688175],[-73.983572,40.688786],[-73.982715,40.688459],[-73.982416,40.688896],[-73.982098,40.689358],[-73.981068,40.688945],[-73.98087,40.688866],[-73.980258,40.688638],[-73.980429,40.689901],[-73.97951,40.689863],[-73.978624,40.689831],[-73.977939,40.689806],[-73.977098,40.689773],[-73.976102,40.689737],[-73.975106,40.689698],[-73.974109,40.689663],[-73.973264,40.689627],[-73.973126,40.689622],[-73.97354,40.691707],[-73.973855,40.693287],[-73.97422,40.693301],[-73.974985,40.693329],[-73.975848,40.693361],[-73.977827,40.693438],[-73.979168,40.693489],[-73.979195,40.693695],[-73.979218,40.693765],[-73.979259,40.69382],[-73.97933,40.693891],[-73.979481,40.694013],[-73.979805,40.694241],[-73.979883,40.694308],[-73.979944,40.694385],[-73.980139,40.694683],[-73.980243,40.695457],[-73.980263,40.695634],[-73.980356,40.696178],[-73.979537,40.696172],[-73.979529,40.696298],[-73.979524,40.696381],[-73.979529,40.696529],[-73.980388,40.696507],[-73.980447,40.69667],[-73.980515,40.697437],[-73.980544,40.698246],[-73.980462,40.699703],[-73.980455,40.700852],[-73.980729,40.701268],[-73.980649,40.701561],[-73.980455,40.702248],[-73.980224,40.703063],[-73.979988,40.703541],[-73.979603,40.704177],[-73.979466,40.704462],[-73.979328,40.704746],[-73.979168,40.705279],[-73.979171,40.705301],[-73.979372,40.705443],[-73.979233,40.705769],[-73.979479,40.705825],[-73.980332,40.705846],[-73.980907,40.705907],[-73.980987,40.705374],[-73.981856,40.70549],[-73.982033,40.705515],[-73.98221,40.70554],[-73.982329,40.705552],[-73.982449,40.705547],[-73.982455,40.705509],[-73.982572,40.705518],[-73.982748,40.705532],[-73.982741,40.705576],[-73.982825,40.705558],[-73.98308,40.705519],[-73.98308,40.70548],[-73.983227,40.705482],[-73.983351,40.705451],[-73.983398,40.705467],[-73.983426,40.705577],[-73.983442,40.705645],[-73.984189,40.705582],[-73.985852,40.705415],[-73.986434,40.705349],[-73.986429,40.705083],[-73.98653,40.704979],[-73.986623,40.704979],[-73.986641,40.705018],[-73.98669,40.705075],[-73.986762,40.705118],[-73.986866,40.705161],[-73.987271,40.705181],[-73.987449,40.705166],[-73.987577,40.705158],[-73.987957,40.705134],[-73.989224,40.705137],[-73.989424,40.705137],[-73.989495,40.705145],[-73.989608,40.705158],[-73.989861,40.705175],[-73.990549,40.705144],[-73.990696,40.705131],[-73.990882,40.705124],[-73.99148,40.705625],[-73.992722,40.705544],[-73.993853,40.704829],[-73.993713,40.70452],[-73.993693,40.704476],[-73.993775,40.704449],[-73.993807,40.70454],[-73.993861,40.704556],[-73.994341,40.704429],[-73.994498,40.704318],[-73.994565,40.704265],[-73.994618,40.704226],[-73.994797,40.704096],[-73.994714,40.703997],[-73.994816,40.703906],[-73.994881,40.703813],[-73.994882,40.703788],[-73.994992,40.703654],[-73.994858,40.703566],[-73.994663,40.703431],[-73.994806,40.703296],[-73.995069,40.703102]]; 9 | const unnormalized = [ 10 | [-119.74242645808715,39.57982985652839], 11 | [-119.74457144737244,39.579904675483704], 12 | [-119.7435238,39.5835421], 13 | [-119.7378725,39.5808519], 14 | [-119.7393411397934,39.57999050617218], 15 | [-119.74242645808715,39.57982985652839] 16 | ]; 17 | 18 | test("RegionCoverer#getCoveringTokens works", () => { 19 | const lls = postalCode11201.map((latlng) => { 20 | const [lng, lat] = latlng; 21 | return new s2.LatLng(lat, lng); 22 | }); 23 | 24 | // Invalid loop with duplicate points (first and last are the same) 25 | expect(() => new s2.RegionCoverer.getCoveringTokens(null, {})).toThrow('s2.LatLng[]'); 26 | expect(() => new s2.RegionCoverer.getCoveringTokens([], {})).toThrow('was empty'); 27 | expect(() => new s2.RegionCoverer.getCoveringTokens(lls, {})).toThrow('duplicate vertex'); 28 | 29 | // level-14 s2 cells 30 | let covering = s2.RegionCoverer.getCoveringTokens(lls.slice(1), { min: 14, max: 14 }); 31 | let coveringTokens = new Set(covering); 32 | let expectedTokens = new Set([ 33 | '89c25a31', 34 | '89c25a33', 35 | '89c25a35', 36 | '89c25a37', 37 | '89c25a39', 38 | '89c25a3b', 39 | '89c25a41', 40 | '89c25a43', 41 | '89c25a45', 42 | '89c25a47', 43 | '89c25a49', 44 | '89c25a4b', 45 | '89c25a4d', 46 | '89c25a4f', 47 | '89c25a51', 48 | '89c25a53', 49 | '89c25a5b', 50 | '89c25a5d', 51 | '89c25a67', 52 | '89c25bb1', 53 | '89c25bb3', 54 | '89c25bb5', 55 | '89c25bb7', 56 | '89c25bcb', 57 | '89c25bcd', 58 | '89c25bd3' 59 | ]); 60 | 61 | expect(coveringTokens).toEqual(expectedTokens); 62 | 63 | // levels 12-20 64 | covering = s2.RegionCoverer.getCoveringTokens(lls.slice(1), { min: 12, max: 20 }); 65 | coveringTokens = new Set(covering); 66 | 67 | expect(coveringTokens).toEqual(new Set([ 68 | '89c25a34', 69 | '89c25a3c', 70 | '89c25a5', 71 | '89c25a674', 72 | '89c25bb4', 73 | '89c25bcb', 74 | '89c25bcd', 75 | '89c25bd3' 76 | ])); 77 | }); 78 | 79 | test("RegionCoverer can cover unnormalized loops", () => { 80 | const lls = unnormalized.map((latlng) => { 81 | const [lng, lat] = latlng; 82 | return new s2.LatLng(lat, lng); 83 | }); 84 | 85 | // generate s2 cells 86 | let covering = s2.RegionCoverer.getCoveringTokens(lls.slice(1), { min: 14, max: 20 }); 87 | let coveringTokens = new Set(covering); 88 | let expectedTokens = new Set([ 89 | '809938fe4', 90 | '809938ffc', 91 | '809939004', 92 | '809939014', 93 | '80993901c', 94 | '809939021f', 95 | '809939023', 96 | '80993903c', 97 | ]); 98 | 99 | expect(coveringTokens).toEqual(expectedTokens); 100 | 101 | }); 102 | 103 | test("RegionCoverer#getCovering works", () => { 104 | const lls = postalCode11201.map((latlng) => { 105 | const [lng, lat] = latlng; 106 | return new s2.LatLng(lat, lng); 107 | }); 108 | 109 | // Invalid loop with duplicate points (first and last are the same) 110 | expect(() => new s2.RegionCoverer.getCovering(null, {})).toThrow('s2.LatLng[]'); 111 | expect(() => new s2.RegionCoverer.getCovering([], {})).toThrow('was empty'); 112 | expect(() => new s2.RegionCoverer.getCovering(lls, {})).toThrow('duplicate vertex'); 113 | 114 | // level-14 s2 cells 115 | let covering = s2.RegionCoverer.getCovering(lls.slice(1), { min: 14, max: 14 }); 116 | let coveringCellIds = covering.cellIds(); 117 | let coveringTokens = new Set(covering.tokens()); 118 | let expectedTokens = new Set([ 119 | '89c25a31', 120 | '89c25a33', 121 | '89c25a35', 122 | '89c25a37', 123 | '89c25a39', 124 | '89c25a3b', 125 | '89c25a41', 126 | '89c25a43', 127 | '89c25a45', 128 | '89c25a47', 129 | '89c25a49', 130 | '89c25a4b', 131 | '89c25a4d', 132 | '89c25a4f', 133 | '89c25a51', 134 | '89c25a53', 135 | '89c25a5b', 136 | '89c25a5d', 137 | '89c25a67', 138 | '89c25bb1', 139 | '89c25bb3', 140 | '89c25bb5', 141 | '89c25bb7', 142 | '89c25bcb', 143 | '89c25bcd', 144 | '89c25bd3' 145 | ]); 146 | 147 | expect(coveringTokens).toEqual(expectedTokens); 148 | expect(new Set(coveringCellIds.map(id => id.token()))).toEqual(expectedTokens); 149 | 150 | // levels 12-20 151 | covering = s2.RegionCoverer.getCovering(lls.slice(1), { min: 12, max: 20 }); 152 | coveringCellIds = covering.cellIds(); 153 | coveringTokens = new Set(covering.tokens()); 154 | 155 | expectedTokens = new Set([ 156 | '89c25a34', 157 | '89c25a3c', 158 | '89c25a5', 159 | '89c25a674', 160 | '89c25bb4', 161 | '89c25bcb', 162 | '89c25bcd', 163 | '89c25bd3' 164 | ]); 165 | 166 | expect(coveringTokens).toEqual(expectedTokens); 167 | expect(new Set(coveringCellIds.map(id => id.token()))).toEqual(expectedTokens); 168 | }); 169 | 170 | test("RegionCoverer#getCoveringIds works", () => { 171 | const lls = postalCode11201.map((latlng) => { 172 | const [lng, lat] = latlng; 173 | return new s2.LatLng(lat, lng); 174 | }); 175 | 176 | // Invalid loop with duplicate points (first and last are the same) 177 | expect(() => new s2.RegionCoverer.getCoveringIds(null, {})).toThrow('s2.LatLng[]'); 178 | expect(() => new s2.RegionCoverer.getCoveringIds([], {})).toThrow('was empty'); 179 | expect(() => new s2.RegionCoverer.getCoveringIds(lls, {})).toThrow('duplicate vertex'); 180 | 181 | // level-14 s2 cells 182 | let covering = 183 | s2.RegionCoverer 184 | .getCoveringIds(lls.slice(1), { min: 14, max: 14 }) 185 | .sort(); 186 | 187 | let expectedIds = BigUint64Array.of( 188 | 9926595695177891840n, 189 | 9926595703767826432n, 190 | 9926595798257106944n, 191 | 9926595806847041536n, 192 | 9926595815436976128n, 193 | 9926595824026910720n, 194 | 9926595832616845312n, 195 | 9926595841206779904n, 196 | 9926595875566518272n, 197 | 9926595884156452864n, 198 | 9926595927106125824n, 199 | 9926597344445333504n, 200 | 9926595712357761024n, 201 | 9926597353035268096n, 202 | 9926597361625202688n, 203 | 9926597370215137280n, 204 | 9926597456114483200n, 205 | 9926597464704417792n, 206 | 9926597490474221568n, 207 | 9926595720947695616n, 208 | 9926595729537630208n, 209 | 9926595738127564800n, 210 | 9926595763897368576n, 211 | 9926595772487303168n, 212 | 9926595781077237760n, 213 | 9926595789667172352n 214 | ).sort(); 215 | 216 | for (let i = 0; i < expectedIds.length; i++) { 217 | expect(covering[i]).toEqual(expectedIds[i]); 218 | } 219 | 220 | // levels 12-20 221 | covering = 222 | s2.RegionCoverer 223 | .getCoveringIds(lls.slice(1), { min: 12, max: 20 }) 224 | .sort(); 225 | 226 | expectedIds = BigUint64Array.of( 227 | 9926595708062793728n, 228 | 9926595742422532096n, 229 | 9926595828321878016n, 230 | 9926595928179867648n, 231 | 9926597357330235392n, 232 | 9926597456114483200n, 233 | 9926597464704417792n, 234 | 9926597490474221568n, 235 | ).sort(); 236 | 237 | for (let i = 0; i < expectedIds.length; i++) { 238 | expect(covering[i]).toEqual(expectedIds[i]); 239 | } 240 | 241 | }); 242 | 243 | test("RegionCoverer#getRadiusCovering works", () => { 244 | const dumbo = [40.7033, -73.9881]; 245 | const ll = new s2.LatLng(...dumbo); 246 | 247 | const cellUnion = s2.RegionCoverer.getRadiusCovering(ll, 3000, { min: 5, max: 20 }); 248 | const ids = cellUnion.ids().sort(); 249 | const expected = [ 250 | 9926594780349857792n, 251 | 9926594866249203712n, 252 | 9926595003688157184n, 253 | 9926595433184886784n, 254 | 9926595759602401280n, 255 | 9926596532696514560n, 256 | 9926596567056252928n, 257 | 9926597408869842944n, 258 | ].sort(); 259 | 260 | for (let i = 0; i < expected.length; i++) { 261 | expect(ids[i]).toEqual(expected[i]); 262 | } 263 | }); 264 | 265 | test("RegionCoverer#getRadiusCoveringIds works", () => { 266 | const dumbo = [40.7033, -73.9881]; 267 | const ll = new s2.LatLng(...dumbo); 268 | 269 | const ids = s2.RegionCoverer.getRadiusCoveringIds(ll, 3000, { min: 5, max: 20 }).sort(); 270 | const expected = [ 271 | 9926594780349857792n, 272 | 9926594866249203712n, 273 | 9926595003688157184n, 274 | 9926595433184886784n, 275 | 9926595759602401280n, 276 | 9926596532696514560n, 277 | 9926596567056252928n, 278 | 9926597408869842944n, 279 | ].sort(); 280 | 281 | for (let i = 0; i < expected.length; i++) { 282 | expect(ids[i]).toEqual(expected[i]); 283 | } 284 | }); 285 | 286 | test("RegionCoverer#getRadiusCoveringTokens works", () => { 287 | const dumbo = [40.7033, -73.9881]; 288 | const ll = new s2.LatLng(...dumbo); 289 | 290 | const tokens = s2.RegionCoverer.getRadiusCoveringTokens(ll, 1000, { min: 12, max: 12 }).sort(); 291 | const expected = [ 292 | '89c25a3', 293 | '89c25a5', 294 | '89c25bb', 295 | '89c25bd', 296 | ].sort(); 297 | 298 | for (let i = 0; i < expected.length; i++) { 299 | expect(tokens[i]).toEqual(expected[i]); 300 | } 301 | }); 302 | -------------------------------------------------------------------------------- /src/region_coverer.cc: -------------------------------------------------------------------------------- 1 | #include "region_coverer.h" 2 | 3 | using absl::make_unique; 4 | 5 | Napi::FunctionReference RegionCoverer::constructor; 6 | 7 | Napi::Object RegionCoverer::Init(Napi::Env env, Napi::Object exports) { 8 | Napi::HandleScope scope(env); 9 | 10 | Napi::Function func = DefineClass(env, "RegionCoverer", { 11 | StaticMethod("getCovering", &RegionCoverer::GetCoveringCellUnion), 12 | StaticMethod("getCoveringIds", &RegionCoverer::GetCoveringIds), 13 | StaticMethod("getCoveringTokens", &RegionCoverer::GetCoveringTokens), 14 | 15 | StaticMethod("getRadiusCovering", &RegionCoverer::GetRadiusCoveringCellUnion), 16 | StaticMethod("getRadiusCoveringIds", &RegionCoverer::GetRadiusCoveringIds), 17 | StaticMethod("getRadiusCoveringTokens", &RegionCoverer::GetRadiusCoveringTokens), 18 | }); 19 | 20 | constructor = Napi::Persistent(func); 21 | constructor.SuppressDestruct(); 22 | 23 | exports.Set("RegionCoverer", func); 24 | return exports; 25 | } 26 | 27 | RegionCoverer::RegionCoverer(const Napi::CallbackInfo& info) : Napi::ObjectWrap(info) { 28 | Napi::Env env = info.Env(); 29 | Napi::HandleScope scope(env); 30 | } 31 | 32 | Napi::Value RegionCoverer::GetCoveringIds(const Napi::CallbackInfo &info) { 33 | Napi::Env env = info.Env(); 34 | 35 | int length = info.Length(); 36 | string badArgs = "(latlngs: s2.LatLng[], options: { min?: number, max?: number, max_cells?: number }) expected."; 37 | 38 | if (length <= 1 || !info[0].IsArray() || !info[1].IsObject()) { 39 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 40 | return env.Null(); 41 | } 42 | 43 | Napi::Array llArray = info[0].As(); 44 | uint32_t arrayLength = llArray.Length(); 45 | if (arrayLength <= 0) { 46 | Napi::TypeError::New(env, "(latlngs: s2.LatLng[]) was empty.").ThrowAsJavaScriptException(); 47 | return env.Null(); 48 | } 49 | 50 | Napi::Object optionsObject = info[1].As(); 51 | S2RegionCoverer::Options options; 52 | GetS2Options(optionsObject, options); 53 | 54 | std::vector vertices; 55 | for (uint32_t i = 0; i < arrayLength; i++) { 56 | Napi::Value obj = llArray[i]; 57 | if (obj.IsObject()) { 58 | LatLng* ll = LatLng::Unwrap(obj.As()); 59 | S2Point point = ll->Get().Normalized().ToPoint().Normalize(); 60 | vertices.push_back(point); 61 | } else { 62 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 63 | return env.Null(); 64 | } 65 | } 66 | 67 | S2Error loopError; 68 | S2Error buildError; 69 | S2Error outputError; 70 | 71 | S2CellUnion covering = GetCovering( 72 | env, 73 | vertices, 74 | options, 75 | loopError, 76 | buildError, 77 | outputError 78 | ); 79 | 80 | if (!loopError.ok()) { 81 | Napi::Error::New( 82 | env, 83 | StringPrintf("Loop is invalid: %d %s", loopError.code(), loopError.text().c_str()) 84 | ).ThrowAsJavaScriptException(); 85 | return env.Null(); 86 | } 87 | 88 | if (!buildError.ok()) { 89 | Napi::Error::New( 90 | env, 91 | StringPrintf("Build failed: %d %s", buildError.code(), buildError.text().c_str()) 92 | ).ThrowAsJavaScriptException(); 93 | return env.Null(); 94 | } 95 | 96 | if (!outputError.ok()) { 97 | Napi::Error::New( 98 | env, 99 | StringPrintf("Output is invalid: %d %s", outputError.code(), outputError.text().c_str()) 100 | ).ThrowAsJavaScriptException(); 101 | return env.Null(); 102 | } 103 | 104 | if (!covering.IsValid()) { 105 | Napi::Error::New(env, "Covering is invalid").ThrowAsJavaScriptException(); 106 | return env.Null(); 107 | } 108 | 109 | uint32_t size = covering.size(); 110 | Napi::BigUint64Array returnedIds = Napi::BigUint64Array::New(env, size); 111 | 112 | for (uint32_t i = 0; i < size; i++) { 113 | returnedIds[i] = covering[i].id(); 114 | } 115 | 116 | return returnedIds; 117 | } 118 | 119 | Napi::Value RegionCoverer::GetCoveringTokens(const Napi::CallbackInfo &info) { 120 | Napi::Env env = info.Env(); 121 | 122 | int length = info.Length(); 123 | string badArgs = "(latlngs: s2.LatLng[], options: { min?: number, max?: number, max_cells?: number }) expected."; 124 | 125 | if (length <= 1 || !info[0].IsArray() || !info[1].IsObject()) { 126 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 127 | return env.Null(); 128 | } 129 | 130 | Napi::Array llArray = info[0].As(); 131 | uint32_t arrayLength = llArray.Length(); 132 | if (arrayLength <= 0) { 133 | Napi::TypeError::New(env, "(latlngs: s2.LatLng[]) was empty.").ThrowAsJavaScriptException(); 134 | return env.Null(); 135 | } 136 | 137 | Napi::Object optionsObject = info[1].As(); 138 | S2RegionCoverer::Options options; 139 | GetS2Options(optionsObject, options); 140 | 141 | std::vector vertices; 142 | for (uint32_t i = 0; i < arrayLength; i++) { 143 | Napi::Value obj = llArray[i]; 144 | if (obj.IsObject()) { 145 | LatLng* ll = LatLng::Unwrap(obj.As()); 146 | S2Point point = ll->Get().Normalized().ToPoint().Normalize(); 147 | vertices.push_back(point); 148 | } else { 149 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 150 | return env.Null(); 151 | } 152 | } 153 | 154 | S2Error loopError; 155 | S2Error buildError; 156 | S2Error outputError; 157 | 158 | S2CellUnion covering = GetCovering( 159 | env, 160 | vertices, 161 | options, 162 | loopError, 163 | buildError, 164 | outputError 165 | ); 166 | 167 | if (!loopError.ok()) { 168 | Napi::Error::New( 169 | env, 170 | StringPrintf("Loop is invalid: %d %s", loopError.code(), loopError.text().c_str()) 171 | ).ThrowAsJavaScriptException(); 172 | return env.Null(); 173 | } 174 | 175 | if (!buildError.ok()) { 176 | Napi::Error::New( 177 | env, 178 | StringPrintf("Build failed: %d %s", buildError.code(), buildError.text().c_str()) 179 | ).ThrowAsJavaScriptException(); 180 | return env.Null(); 181 | } 182 | 183 | if (!outputError.ok()) { 184 | Napi::Error::New( 185 | env, 186 | StringPrintf("Output is invalid: %d %s", outputError.code(), outputError.text().c_str()) 187 | ).ThrowAsJavaScriptException(); 188 | return env.Null(); 189 | } 190 | 191 | if (!covering.IsValid()) { 192 | Napi::Error::New(env, "Covering is invalid").ThrowAsJavaScriptException(); 193 | return env.Null(); 194 | } 195 | 196 | uint32_t size = covering.size(); 197 | Napi::Array returnedIds = Napi::Array::New(env, size); 198 | 199 | for (uint32_t i = 0; i < size; i++) { 200 | returnedIds[i] = Napi::String::New(env, covering[i].ToToken()); 201 | } 202 | 203 | return returnedIds; 204 | } 205 | 206 | Napi::Value RegionCoverer::GetCoveringCellUnion(const Napi::CallbackInfo &info) { 207 | Napi::Env env = info.Env(); 208 | 209 | int length = info.Length(); 210 | string badArgs = "(latlngs: s2.LatLng[], options: { min?: number, max?: number, max_cells?: number }) expected."; 211 | 212 | if (length <= 1 || !info[0].IsArray() || !info[1].IsObject()) { 213 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 214 | return env.Null(); 215 | } 216 | 217 | Napi::Array llArray = info[0].As(); 218 | uint32_t arrayLength = llArray.Length(); 219 | if (arrayLength <= 0) { 220 | Napi::TypeError::New(env, "(latlngs: s2.LatLng[]) was empty.").ThrowAsJavaScriptException(); 221 | return env.Null(); 222 | } 223 | 224 | Napi::Object optionsObject = info[1].As(); 225 | S2RegionCoverer::Options options; 226 | GetS2Options(optionsObject, options); 227 | 228 | std::vector vertices; 229 | for (uint32_t i = 0; i < arrayLength; i++) { 230 | Napi::Value obj = llArray[i]; 231 | if (obj.IsObject()) { 232 | LatLng* ll = LatLng::Unwrap(obj.As()); 233 | S2Point point = ll->Get().Normalized().ToPoint().Normalize(); 234 | vertices.push_back(point); 235 | } else { 236 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 237 | return env.Null(); 238 | } 239 | } 240 | 241 | S2Error loopError; 242 | S2Error buildError; 243 | S2Error outputError; 244 | 245 | S2CellUnion covering = GetCovering( 246 | env, 247 | vertices, 248 | options, 249 | loopError, 250 | buildError, 251 | outputError 252 | ); 253 | 254 | if (!loopError.ok()) { 255 | Napi::Error::New( 256 | env, 257 | StringPrintf("Loop is invalid: %d %s", loopError.code(), loopError.text().c_str()) 258 | ).ThrowAsJavaScriptException(); 259 | return env.Null(); 260 | } 261 | 262 | if (!buildError.ok()) { 263 | Napi::Error::New( 264 | env, 265 | StringPrintf("Build failed: %d %s", buildError.code(), buildError.text().c_str()) 266 | ).ThrowAsJavaScriptException(); 267 | return env.Null(); 268 | } 269 | 270 | if (!outputError.ok()) { 271 | Napi::Error::New( 272 | env, 273 | StringPrintf("Output is invalid: %d %s", outputError.code(), outputError.text().c_str()) 274 | ).ThrowAsJavaScriptException(); 275 | return env.Null(); 276 | } 277 | 278 | if (!covering.IsValid()) { 279 | Napi::Error::New(env, "Covering is invalid").ThrowAsJavaScriptException(); 280 | return env.Null(); 281 | } 282 | 283 | return CellUnion::constructor.New({ 284 | Napi::External::New(env, &covering) 285 | }); 286 | } 287 | 288 | Napi::Value RegionCoverer::GetRadiusCoveringTokens(const Napi::CallbackInfo &info) { 289 | Napi::Env env = info.Env(); 290 | 291 | int length = info.Length(); 292 | string badArgs = "(latlng: s2.LatLng, radiusM: number, options: { min?: number, max?: number, max_cells?: number }) expected."; 293 | 294 | if ( 295 | length < 3 296 | || !info[0].IsObject() 297 | || !info[1].IsNumber() 298 | || !info[2].IsObject() 299 | ) { 300 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 301 | return env.Null(); 302 | } 303 | 304 | Napi::Object optionsObject = info[2].As(); 305 | S2RegionCoverer::Options options; 306 | GetS2Options(optionsObject, options); 307 | 308 | LatLng* ll = LatLng::Unwrap(info[0].As()); 309 | double radiusM = info[1].As().DoubleValue(); 310 | 311 | S2CellUnion covering = GetRadiusCovering(ll, radiusM, options); 312 | 313 | uint32_t size = covering.size(); 314 | Napi::Array returnedIds = Napi::Array::New(env, size); 315 | 316 | for (uint32_t i = 0; i < size; i++) { 317 | returnedIds[i] = Napi::String::New(env, covering[i].ToToken()); 318 | } 319 | 320 | return returnedIds; 321 | } 322 | 323 | Napi::Value RegionCoverer::GetRadiusCoveringIds(const Napi::CallbackInfo &info) { 324 | Napi::Env env = info.Env(); 325 | 326 | int length = info.Length(); 327 | string badArgs = "(latlng: s2.LatLng, radiusM: number, options: { min?: number, max?: number, max_cells?: number }) expected."; 328 | 329 | if ( 330 | length < 3 331 | || !info[0].IsObject() 332 | || !info[1].IsNumber() 333 | || !info[2].IsObject() 334 | ) { 335 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 336 | return env.Null(); 337 | } 338 | 339 | Napi::Object optionsObject = info[2].As(); 340 | S2RegionCoverer::Options options; 341 | GetS2Options(optionsObject, options); 342 | 343 | LatLng* ll = LatLng::Unwrap(info[0].As()); 344 | double radiusM = info[1].As().DoubleValue(); 345 | 346 | S2CellUnion covering = GetRadiusCovering(ll, radiusM, options); 347 | 348 | uint32_t size = covering.size(); 349 | Napi::BigUint64Array returnedIds = Napi::BigUint64Array::New(env, size); 350 | 351 | for (uint32_t i = 0; i < size; i++) { 352 | returnedIds[i] = covering[i].id(); 353 | } 354 | 355 | return returnedIds; 356 | } 357 | 358 | Napi::Value RegionCoverer::GetRadiusCoveringCellUnion(const Napi::CallbackInfo &info) { 359 | Napi::Env env = info.Env(); 360 | 361 | int length = info.Length(); 362 | string badArgs = "(latlng: s2.LatLng, radiusM: number, options: { min?: number, max?: number, max_cells?: number }) expected."; 363 | 364 | if ( 365 | length < 3 366 | || !info[0].IsObject() 367 | || !info[1].IsNumber() 368 | || !info[2].IsObject() 369 | ) { 370 | Napi::TypeError::New(env, badArgs).ThrowAsJavaScriptException(); 371 | return env.Null(); 372 | } 373 | 374 | Napi::Object optionsObject = info[2].As(); 375 | S2RegionCoverer::Options options; 376 | GetS2Options(optionsObject, options); 377 | 378 | LatLng* ll = LatLng::Unwrap(info[0].As()); 379 | double radiusM = info[1].As().DoubleValue(); 380 | 381 | S2CellUnion covering = GetRadiusCovering(ll, radiusM, options); 382 | return CellUnion::constructor.New({ 383 | Napi::External::New(env, &covering) 384 | }); 385 | } 386 | 387 | void RegionCoverer::GetS2Options( 388 | Napi::Object optionsObject, 389 | S2RegionCoverer::Options &options 390 | ) { 391 | Napi::Value minLevelRaw = optionsObject["min"]; 392 | Napi::Value maxLevelRaw = optionsObject["max"]; 393 | Napi::Value maxCellsRaw = optionsObject["max_cells"]; 394 | 395 | if (minLevelRaw.IsNumber()) { 396 | options.set_min_level(minLevelRaw.As().Uint32Value()); 397 | } 398 | if (maxLevelRaw.IsNumber()) { 399 | options.set_max_level(maxLevelRaw.As().Uint32Value()); 400 | } 401 | if (maxCellsRaw.IsNumber()) { 402 | options.set_max_cells(maxCellsRaw.As().Uint32Value()); 403 | } 404 | } 405 | 406 | S2CellUnion RegionCoverer::GetCovering( 407 | Napi::Env env, 408 | std::vector &vertices, 409 | S2RegionCoverer::Options &coverOptions, 410 | S2Error &loopError, 411 | S2Error &buildError, 412 | S2Error &outputError 413 | ) { 414 | S2Loop loop(vertices, S2Debug::ALLOW); 415 | loop.Normalize(); 416 | 417 | bool loopOk = !loop.FindValidationError(&loopError); 418 | if (!loopOk) { return S2CellUnion(); } 419 | 420 | S2Builder::Options builderOptions; 421 | S2Builder builder(builderOptions); 422 | 423 | S2Polygon output; 424 | builder.StartLayer(std::make_unique(&output)); 425 | builder.AddLoop(loop); 426 | 427 | bool buildOk = builder.Build(&buildError); 428 | if (!buildOk) { return S2CellUnion(); } 429 | 430 | bool outputOk = !output.FindValidationError(&outputError); 431 | if (!outputOk) { return S2CellUnion(); } 432 | 433 | S2RegionCoverer coverer(coverOptions); 434 | return coverer.GetCovering(output); 435 | } 436 | 437 | S2CellUnion RegionCoverer::GetRadiusCovering( 438 | LatLng* ll, 439 | double radiusM, 440 | S2RegionCoverer::Options &coverOptions 441 | ) { 442 | S2Point point = ll->Get().Normalized().ToPoint().Normalize(); 443 | S1Angle angle = S1Angle::Radians(S2Earth::MetersToRadians(radiusM)); 444 | S2Cap s2cap(point, angle); 445 | if (!s2cap.is_valid()) { 446 | return S2CellUnion(); 447 | } 448 | S2RegionCoverer coverer(coverOptions); 449 | return coverer.GetCovering(s2cap); 450 | } 451 | --------------------------------------------------------------------------------