diff options
Diffstat (limited to 'platform/node')
-rw-r--r-- | platform/node/CHANGELOG.md | 31 | ||||
-rw-r--r-- | platform/node/README.md | 130 | ||||
-rw-r--r-- | platform/node/bitrise.yml | 25 | ||||
-rwxr-xr-x | platform/node/scripts/after_script.sh | 48 | ||||
-rwxr-xr-x | platform/node/scripts/install.sh | 24 | ||||
-rwxr-xr-x | platform/node/scripts/run.sh | 26 | ||||
-rw-r--r-- | platform/node/src/node_feature.cpp | 168 | ||||
-rw-r--r-- | platform/node/src/node_feature.hpp | 18 | ||||
-rw-r--r-- | platform/node/src/node_log.cpp | 1 | ||||
-rw-r--r-- | platform/node/src/node_map.cpp | 84 | ||||
-rw-r--r-- | platform/node/src/node_map.hpp | 3 | ||||
-rw-r--r-- | platform/node/src/node_mapbox_gl_native.cpp | 32 | ||||
-rw-r--r-- | platform/node/src/node_mapbox_gl_native.hpp | 9 | ||||
-rw-r--r-- | platform/node/src/node_request.cpp | 43 | ||||
-rw-r--r-- | platform/node/src/node_request.hpp | 9 | ||||
-rw-r--r-- | platform/node/test/js/map.test.js | 24 | ||||
-rw-r--r-- | platform/node/test/js/require.js | 1 | ||||
-rw-r--r-- | platform/node/test/query.test.js | 12 | ||||
-rw-r--r-- | platform/node/test/render.test.js | 48 | ||||
-rw-r--r-- | platform/node/test/suite_implementation.js | 58 |
20 files changed, 491 insertions, 303 deletions
diff --git a/platform/node/CHANGELOG.md b/platform/node/CHANGELOG.md index 25e46b3e50..e1bff6cc28 100644 --- a/platform/node/CHANGELOG.md +++ b/platform/node/CHANGELOG.md @@ -1,3 +1,34 @@ +# 3.2.1 + +- Fixes a memory leak in raster image data ([#5269](https://github.com/mapbox/mapbox-gl-native/pull/5269)) + +# 3.2.0 + +- Switches to [earcut.hpp](https://github.com/mapbox/earcut.hpp) for tessellation ([#2444](https://github.com/mapbox/mapbox-gl-native/pull/2444)) + +# 3.1.3 + +- Fixes a leak in TexturePoolHolder ([#5141](https://github.com/mapbox/mapbox-gl-native/pull/5141)) +- Fixes a bug where a callback would be fired after an AsyncRequest had been cancelled ([#5162](https://github.com/mapbox/mapbox-gl-native/pull/5162)) + +# 3.1.2 + +- Fixes a race condition with animated transitions ([#4836](https://github.com/mapbox/mapbox-gl-native/pull/4836)) + +# 3.1.1 + +- Moves node-pre-gyp from `bundledDependencies` to `preinstall` ([#4680](https://github.com/mapbox/mapbox-gl-native/pull/4680)) + +# 3.1.0 + +- Adds debug render options ([#3840](https://github.com/mapbox/mapbox-gl-native/pull/3840)) +- Fixes circle bucket rendering on tile boundaries ([#3764](https://github.com/mapbox/mapbox-gl-native/issues/3764)) +- Fixes a segfault caused by improperly disposing the entire module in the `NodeLog` destructor ([#4639](https://github.com/mapbox/mapbox-gl-native/pull/4639)) +- Fixes an issue with vanishing GeoJSON layers at high zoom levels ([#4632](https://github.com/mapbox/mapbox-gl-native/issues/4632)) +- Fixes inheritance from EventEmitter ([#4567](https://github.com/mapbox/mapbox-gl-native/pull/4576)) +- Fixes intermittent `stencil mask overflow` error ([#962](https://github.com/mapbox/mapbox-gl-native/issues/962)) +- Drops support for Node.js v5.x prebuilt binaries due to ongoing npm3 instability ([#4370](https://github.com/mapbox/mapbox-gl-native/issues/4370)) + # 3.0.2 - Fixes a memory leak in `NodeMap::request` ([#3829](https://github.com/mapbox/mapbox-gl-native/pull/3829)) diff --git a/platform/node/README.md b/platform/node/README.md index 706fcf86cd..50f53d72eb 100644 --- a/platform/node/README.md +++ b/platform/node/README.md @@ -1,7 +1,6 @@ # node-mapbox-gl-native [![NPM](https://nodei.co/npm/mapbox-gl-native.png)](https://npmjs.org/package/mapbox-gl-native) -[![Travis](https://travis-ci.org/mapbox/mapbox-gl-native.svg?branch=master)](https://travis-ci.org/mapbox/mapbox-gl-native/builds) ## Installing @@ -10,30 +9,49 @@ Requires a modern C++ runtime that supports C++14. By default, installs binaries. On these platforms no additional dependencies are needed. - 64 bit OS X or 64 bit Linux -- Node.js v4+ +- Node.js v4.x _(note: v5+ is known to have issues)_ -Just run: +Run: ``` npm install mapbox-gl-native ``` -Other platforms will fall back to a source compile with `make node`. To compile this module, make sure all submodules are initialized with `git submodule update --init` and install the [external dependencies required to build from source](https://github.com/mapbox/mapbox-gl-native/blob/node-v2.1.0/INSTALL.md#2-installing-dependencies). +Other platforms will fall back to a source compile with `make node`; see INSTALL.md in the repository root directory for prequisites. ## Testing ``` npm test +npm run test-suite ``` ## Rendering a map tile ```js +var mbgl = require('mapbox-gl-native'); +var sharp = require('sharp'); var map = new mbgl.Map({ request: function() {} }); + map.load(require('./test/fixtures/style.json')); -map.render({}, function(err, image) { + +map.render({}, function(err, buffer) { if (err) throw err; - fs.writeFileSync('image.png', image); + + map.release(); + + var image = sharp(buffer, { + raw: { + width: 512, + height: 512, + channels: 4 + } + }); + + // Convert raw image buffer to PNG + image.toFile('image.png', function(err) { + if (err) throw err; + }); }); ``` @@ -46,11 +64,12 @@ The first argument passed to `map.render` is an options object, all keys are opt height: {height}, // number (px), defaults to 512 center: [{longitude}, {latitude}], // array of numbers (coordinates), defaults to [0,0] bearing: {bearing}, // number (in degrees, counter-clockwise from north), defaults to 0 + pitch: {pitch}, // number (in degrees, arcing towards the horizon), defaults to 0 classes: {classes} // array of strings } ``` -When you are finished using a map object, you can call `map.release()` to dispose the internal map resources manually. This is not necessary, but can be helpful to optimize resource usage (memory, file sockets) on a more granualar level than v8's garbage collector. +When you are finished using a map object, you can call `map.release()` to permanently dispose the internal map resources. This is not necessary, but can be helpful to optimize resource usage (memory, file sockets) on a more granualar level than V8's garbage collector. Calling `map.release()` will prevent a map object from being used for any further render calls, but can be safely called as soon as the `map.render()` callback returns, as the returned pixel buffer will always be retained for the scope of the callback. ## Implementing a file source @@ -65,7 +84,7 @@ var map = new mbgl.Map({ }); ``` -The `request()` method starts a new request to a file. The `ratio` sets the scale at which the map will render tiles, such as `2.0` for rendering images for high pixel density displays. The `req` parameter has two properties: +The `request()` method handles a request for a resource. The `ratio` sets the scale at which the map will render tiles, such as `2.0` for rendering images for high pixel density displays. The `req` parameter has two properties: ```json { @@ -74,7 +93,7 @@ The `request()` method starts a new request to a file. The `ratio` sets the scal } ``` -The `kind` is an enum and defined in [`mbgl.Resource`](https://github.com/mapbox/mapbox-gl-native/blob/node/include/mbgl/storage/resource.hpp): +The `kind` is an enum and defined in [`mbgl.Resource`](https://github.com/mapbox/mapbox-gl-native/blob/master/include/mbgl/storage/resource.hpp): ```json { @@ -88,7 +107,7 @@ The `kind` is an enum and defined in [`mbgl.Resource`](https://github.com/mapbox } ``` -It has no significance for anything but serves as a hint to your implemention as to what sort of resource to expect. E.g., your implementation could choose caching strategies based on the expected file type. +The `kind` enum has no significance for anything but serves as a hint to your implemention as to what sort of resource to expect. E.g., your implementation could choose caching strategies based on the expected file type. The `request` implementation should pass uncompressed data to `callback`. If you are downloading assets from a source that applies gzip transport encoding, the implementation must decompress the results before passing them on. @@ -104,7 +123,7 @@ var map = new mbgl.Map({ }); ``` -This is a very barebones implementation and you'll probably want a better implementation. E.g. it passes the url verbatim to the file system, but you'd want add some logic that normalizes `http` URLs. You'll notice that once your implementation has obtained the requested file, you have to deliver it to the requestee by calling `callback()`, which takes either an error object or `null` and an object with several settings: +This is a very barebones implementation and you'll probably want a better implementation. E.g. it passes the url verbatim to the file system, but you'd want add some logic that normalizes `http` URLs. You'll notice that once your implementation has obtained the requested file, you have to deliver it to the requestee by calling `callback()`, which takes either an error object or `null` and an object with several keys: ```js { @@ -112,10 +131,10 @@ This is a very barebones implementation and you'll probably want a better implem expires: new Date(), etag: "string", data: new Buffer() -}; +} ``` -A sample implementation that uses [`request`](https://github.com/request/request) to query data from HTTP: +A sample implementation that uses [`request`](https://github.com/request/request) to fetch data from a remote source: ```js var mbgl = require('mapbox-gl-native'); @@ -148,43 +167,12 @@ var map = new mbgl.Map({ }); ``` -Mapbox GL uses two types of protocols: `asset://` for files that should be loaded from some local static system, and `http://` (and `https://`), which should be loaded from the internet. However, stylesheets are free to use other protocols too, if your implementation of `request` supports these; e.g. you could use `s3://` to indicate that files are supposed to be loaded from S3. +Stylesheets are free to use any protocols, but your implementation of `request` must support these; e.g. you could use `s3://` to indicate that files are supposed to be loaded from S3. ## Listening for log events The module imported with `require('mapbox-gl-native')` inherits from [`EventEmitter`](https://nodejs.org/api/events.html), and the `NodeLogObserver` will push log events to this. Log messages can have [`class`](https://github.com/mapbox/mapbox-gl-native/blob/node-v2.1.0/include/mbgl/platform/event.hpp#L43-L60), [`severity`](https://github.com/mapbox/mapbox-gl-native/blob/node-v2.1.0/include/mbgl/platform/event.hpp#L17-L23), `code` ([HTTP status codes](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)), and `text` parameters. -``` -MBGL_DEFINE_ENUM_CLASS(EventClass, Event, { - { Event::General, "General" }, - { Event::Setup, "Setup" }, - { Event::Shader, "Shader" }, - { Event::ParseStyle, "ParseStyle" }, - { Event::ParseTile, "ParseTile" }, - { Event::Render, "Render" }, - { Event::Style, "Style" }, - { Event::Database, "Database" }, - { Event::HttpRequest, "HttpRequest" }, - { Event::Sprite, "Sprite" }, - { Event::Image, "Image" }, - { Event::OpenGL, "OpenGL" }, - { Event::JNI, "JNI" }, - { Event::Android, "Android" }, - { Event::Crash, "Crash" }, - { Event(-1), "Unknown" }, -}); -``` - -``` -MBGL_DEFINE_ENUM_CLASS(EventSeverityClass, EventSeverity, { - { EventSeverity::Debug, "DEBUG" }, - { EventSeverity::Info, "INFO" }, - { EventSeverity::Warning, "WARNING" }, - { EventSeverity::Error, "ERROR" }, - { EventSeverity(-1), "UNKNOWN" }, -}); -``` - ```js var mbgl = require('mapbox-gl-native'); mbgl.on('message', function(msg) { @@ -195,58 +183,6 @@ mbgl.on('message', function(msg) { }); ``` -## Mapbox API Access tokens - -To use styles that rely on Mapbox vector tiles, you must pass an [API access token](https://www.mapbox.com/developers/api/#access-tokens) in your `request` implementation with requests to `mapbox://` protocols. - -```js -var mbgl = require('mapbox-gl-native'); -var request = require('request'); -var url = require('url'); - -var map = new mbgl.Map({ - request: function(req, callback) { - var opts = { - url: req.url, - encoding: null, - gzip: true - }; - - if (url.parse(req.url).protocol === 'mapbox:') { - opts.qs = { access_token: process.env.MAPBOX_ACCESS_TOKEN}; - } - - request(opts, function (err, res, body) { - if (err) { - callback(err); - } else if (res.statusCode == 200) { - var response = {}; - - if (res.headers.modified) { response.modified = new Date(res.headers.modified); } - if (res.headers.expires) { response.expires = new Date(res.headers.expires); } - if (res.headers.etag) { response.etag = res.headers.etag; } - - response.data = body; - - callback(null, response); - } else { - callback(new Error(JSON.parse(body).message)); - } - }); - } -}); - -// includes a datasource with a reference to something like `mapbox://mapbox.mapbox-streets-v6` -var style = mapboxStyle; - -map.load(style); -map.render({}, function(err, image) { - if (err) throw err; - fs.writeFileSync('image.png', image); -}); - -``` - ## Contributing See [DEVELOPING.md](DEVELOPING.md) for instructions on building this module for development. diff --git a/platform/node/bitrise.yml b/platform/node/bitrise.yml index 134925d6e8..dde34c6c6a 100644 --- a/platform/node/bitrise.yml +++ b/platform/node/bitrise.yml @@ -22,28 +22,21 @@ workflows: else envman add --key SKIPCI --value false fi - - select-xcode-version: - title: Select Xcode version - run_if: '{{enveq "SKIPCI" "false"}}' - script: title: Run build script run_if: '{{enveq "SKIPCI" "false"}}' inputs: - content: |- #!/bin/bash - set -e - set -o pipefail - export TRAVIS_OS_NAME=osx - export TRAVIS_TAG=$BITRISE_GIT_TAG - export NODE_VERSION=4 - export CXX=clang++ - export CC=clang - source ./scripts/set_compiler.sh - ./platform/node/scripts/install.sh - if command -v ccache >/dev/null 2>&1; then ccache --zero-stats ; fi - ./platform/node/scripts/run.sh - if command -v ccache >/dev/null 2>&1; then ccache --show-stats ; fi - ./platform/node/scripts/after_script.sh + set -eu -o pipefail + brew unlink node + brew install awscli homebrew/versions/node4-lts + brew link homebrew/versions/node4-lts + gem install xcpretty --no-rdoc --no-ri + make node + make test-node || result=$? + ./platform/node/scripts/after_script.sh ${BITRISE_BUILD_NUMBER} ${BITRISE_GIT_TAG:-} + exit ${result:-0} - slack: title: Post to Slack run_if: '{{enveq "SKIPCI" "false"}}' diff --git a/platform/node/scripts/after_script.sh b/platform/node/scripts/after_script.sh index 8cbfa4d8ff..905055ad11 100755 --- a/platform/node/scripts/after_script.sh +++ b/platform/node/scripts/after_script.sh @@ -3,48 +3,20 @@ set -e set -o pipefail -# Inspect binary. -if [[ ${TRAVIS_OS_NAME} == "linux" ]]; then - ldd ./lib/mapbox-gl-native.node -else - otool -L ./lib/mapbox-gl-native.node -fi +JOB=$1 +TAG=$2 -PACKAGE_JSON_VERSION=$(node -e "console.log(require('./package.json').version)") +if [ ! -z "${AWS_ACCESS_KEY_ID}" ] && [ ! -z "${AWS_SECRET_ACCESS_KEY}" ] ; then + gzip --stdout node_modules/mapbox-gl-test-suite/render-tests/index.html | \ + aws s3 cp --acl public-read --content-encoding gzip --content-type text/html \ + - s3://mapbox/mapbox-gl-native/render-tests/$JOB/index.html -if [[ ${TRAVIS_TAG} == node-v${PACKAGE_JSON_VERSION} ]]; then - source ~/.nvm/nvm.sh - nvm use $NODE_VERSION + echo http://mapbox.s3.amazonaws.com/mapbox-gl-native/render-tests/$JOB/index.html +fi - npm install aws-sdk +PACKAGE_JSON_VERSION=$(node -e "console.log(require('./package.json').version)") +if [[ $TAG == node-v${PACKAGE_JSON_VERSION} ]]; then ./node_modules/.bin/node-pre-gyp package - - if [[ ${TRAVIS_OS_NAME} == "linux" ]]; then - ./node_modules/.bin/node-pre-gyp testpackage - fi - ./node_modules/.bin/node-pre-gyp publish info - - if [[ ${TRAVIS_OS_NAME} == "linux" ]]; then - source ./platform/linux/scripts/setup.sh - - rm -rf build - rm -rf lib - npm install --fallback-to-build=false - npm test - fi -fi - -if [[ ${TRAVIS_OS_NAME} == "linux" ]] && [ ! -z "${AWS_ACCESS_KEY_ID}" ] && [ ! -z "${AWS_SECRET_ACCESS_KEY}" ] ; then - # Install and add awscli to PATH for uploading the results - pip install --user awscli - export PATH="`python -m site --user-base`/bin:${PATH}" - - REPO_NAME=$(basename $TRAVIS_REPO_SLUG) - gzip --stdout node_modules/mapbox-gl-test-suite/render-tests/index.html | \ - aws s3 cp --acl public-read --content-encoding gzip --content-type text/html \ - - s3://mapbox/$REPO_NAME/render-tests/$TRAVIS_JOB_NUMBER/index.html - - echo http://mapbox.s3.amazonaws.com/$REPO_NAME/render-tests/$TRAVIS_JOB_NUMBER/index.html fi diff --git a/platform/node/scripts/install.sh b/platform/node/scripts/install.sh deleted file mode 100755 index b550933cd9..0000000000 --- a/platform/node/scripts/install.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -o pipefail - -git submodule update --init .mason - -export PATH="`pwd`/.mason:${PATH}" MASON_DIR="`pwd`/.mason" - -if [ ${TRAVIS_OS_NAME} == "linux" ]; then - mason install mesa 10.4.3 -fi - -if [ ! -d ~/.nvm ]; then - curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.28.0/install.sh | bash -fi - -source ~/.nvm/nvm.sh - -nvm install $NODE_VERSION -nvm alias default $NODE_VERSION - -node --version -npm --version diff --git a/platform/node/scripts/run.sh b/platform/node/scripts/run.sh deleted file mode 100755 index fdebaaeb94..0000000000 --- a/platform/node/scripts/run.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -o pipefail - -source ./platform/${TRAVIS_OS_NAME}/scripts/setup.sh - -BUILDTYPE=${BUILDTYPE:-Release} - -################################################################################ -# Build -################################################################################ - -source ~/.nvm/nvm.sh -nvm use $NODE_VERSION -npm install --build-from-source - -################################################################################ -# Test -################################################################################ - -# https://github.com/mapbox/mapbox-gl-native/issues/2150 -if [[ ${TRAVIS_OS_NAME} == "linux" ]]; then - npm test - npm run test-suite -fi diff --git a/platform/node/src/node_feature.cpp b/platform/node/src/node_feature.cpp new file mode 100644 index 0000000000..1a5e31fe97 --- /dev/null +++ b/platform/node/src/node_feature.cpp @@ -0,0 +1,168 @@ +#include "node_feature.hpp" + +namespace node_mbgl { + +using namespace mapbox::geometry; + +using Value = mbgl::Value; +using Feature = mbgl::Feature; +using Geometry = mbgl::Feature::geometry_type; +using GeometryCollection = mapbox::geometry::geometry_collection<double>; +using Properties = mbgl::Feature::property_map; + +template <class T> +struct ToType { +public: + v8::Local<v8::String> operator()(const point<T>&) { + return type("Point"); + } + + v8::Local<v8::String> operator()(const line_string<T>&) { + return type("LineString"); + } + + v8::Local<v8::String> operator()(const polygon<T>&) { + return type("Polygon"); + } + + v8::Local<v8::String> operator()(const multi_point<T>&) { + return type("MultiPoint"); + } + + v8::Local<v8::String> operator()(const multi_line_string<T>&) { + return type("MultiLineString"); + } + + v8::Local<v8::String> operator()(const multi_polygon<T>&) { + return type("MultiPolygon"); + } + + v8::Local<v8::String> operator()(const geometry_collection<T>&) { + return type("GeometryCollection"); + } + +private: + v8::Local<v8::String> type(const char* type) { + Nan::EscapableHandleScope scope; + return scope.Escape(Nan::New(type).ToLocalChecked()); + } +}; + +template <class T> +struct ToCoordinatesOrGeometries { +public: + // Handles line_string, polygon, multi_point, multi_line_string, multi_polygon, and geometry_collection. + template <class E> + v8::Local<v8::Object> operator()(const std::vector<E>& vector) { + Nan::EscapableHandleScope scope; + v8::Local<v8::Array> result = Nan::New<v8::Array>(vector.size()); + for (std::size_t i = 0; i < vector.size(); ++i) { + Nan::Set(result, i, operator()(vector[i])); + } + return scope.Escape(result); + } + + v8::Local<v8::Object> operator()(const point<T>& point) { + Nan::EscapableHandleScope scope; + v8::Local<v8::Array> result = Nan::New<v8::Array>(2); + Nan::Set(result, 0, Nan::New(point.x)); + Nan::Set(result, 1, Nan::New(point.y)); + return scope.Escape(result); + } + + v8::Local<v8::Object> operator()(const geometry<T>& geometry) { + return toJS(geometry); + } +}; + +struct ToValue { + v8::Local<v8::Value> operator()(std::nullptr_t) { + Nan::EscapableHandleScope scope; + return scope.Escape(Nan::Null()); + } + + v8::Local<v8::Value> operator()(bool t) { + Nan::EscapableHandleScope scope; + return scope.Escape(Nan::New(t)); + } + + v8::Local<v8::Value> operator()(int64_t t) { + return operator()(double(t)); + } + + v8::Local<v8::Value> operator()(uint64_t t) { + return operator()(double(t)); + } + + v8::Local<v8::Value> operator()(double t) { + Nan::EscapableHandleScope scope; + return scope.Escape(Nan::New(t)); + } + + v8::Local<v8::Value> operator()(const std::string& t) { + Nan::EscapableHandleScope scope; + return scope.Escape(Nan::New(t).ToLocalChecked()); + } + + v8::Local<v8::Value> operator()(const std::vector<mbgl::Value>& array) { + Nan::EscapableHandleScope scope; + v8::Local<v8::Array> result = Nan::New<v8::Array>(); + for (unsigned int i = 0; i < array.size(); i++) { + result->Set(i, toJS(array[i])); + } + return scope.Escape(result); + } + + v8::Local<v8::Value> operator()(const std::unordered_map<std::string, mbgl::Value>& map) { + return toJS(map); + } +}; + +v8::Local<v8::Object> toJS(const Geometry& geometry) { + Nan::EscapableHandleScope scope; + + v8::Local<v8::Object> result = Nan::New<v8::Object>(); + + Nan::Set(result, + Nan::New("type").ToLocalChecked(), + Geometry::visit(geometry, ToType<double>())); + + Nan::Set(result, + Nan::New(geometry.is<GeometryCollection>() ? "geometries" : "coordinates").ToLocalChecked(), + Geometry::visit(geometry, ToCoordinatesOrGeometries<double>())); + + return scope.Escape(result); +} + +v8::Local<v8::Value> toJS(const Value& value) { + return Value::visit(value, ToValue()); +} + +v8::Local<v8::Object> toJS(const Properties& properties) { + Nan::EscapableHandleScope scope; + + v8::Local<v8::Object> result = Nan::New<v8::Object>(); + for (const auto& property : properties) { + Nan::Set(result, Nan::New(property.first).ToLocalChecked(), toJS(property.second)); + } + + return scope.Escape(result); +} + +v8::Local<v8::Object> toJS(const Feature& feature) { + Nan::EscapableHandleScope scope; + + v8::Local<v8::Object> result = Nan::New<v8::Object>(); + + Nan::Set(result, Nan::New("type").ToLocalChecked(), Nan::New("Feature").ToLocalChecked()); + Nan::Set(result, Nan::New("geometry").ToLocalChecked(), toJS(feature.geometry)); + Nan::Set(result, Nan::New("properties").ToLocalChecked(), toJS(feature.properties)); + + if (feature.id) { + Nan::Set(result, Nan::New("id").ToLocalChecked(), Nan::New(double(*feature.id))); + } + + return scope.Escape(result); +} + +} diff --git a/platform/node/src/node_feature.hpp b/platform/node/src/node_feature.hpp new file mode 100644 index 0000000000..7973ee19d4 --- /dev/null +++ b/platform/node/src/node_feature.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include <mbgl/util/feature.hpp> + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wshadow" +#include <nan.h> +#pragma GCC diagnostic pop + +namespace node_mbgl { + +v8::Local<v8::Value> toJS(const mbgl::Value&); +v8::Local<v8::Object> toJS(const mbgl::Feature&); +v8::Local<v8::Object> toJS(const mbgl::Feature::geometry_type&); +v8::Local<v8::Object> toJS(const mbgl::Feature::property_map&); + +} diff --git a/platform/node/src/node_log.cpp b/platform/node/src/node_log.cpp index 8230bb36ce..a741109b27 100644 --- a/platform/node/src/node_log.cpp +++ b/platform/node/src/node_log.cpp @@ -52,7 +52,6 @@ NodeLogObserver::NodeLogObserver(v8::Local<v8::Object> target) NodeLogObserver::~NodeLogObserver() { queue->stop(); - module.Reset(); } bool NodeLogObserver::onRecord(mbgl::EventSeverity severity, mbgl::Event event, int64_t code, const std::string &text) { diff --git a/platform/node/src/node_map.cpp b/platform/node/src/node_map.cpp index 5eefe402d6..165e27b669 100644 --- a/platform/node/src/node_map.cpp +++ b/platform/node/src/node_map.cpp @@ -1,10 +1,9 @@ #include "node_map.hpp" #include "node_request.hpp" -#include "node_mapbox_gl_native.hpp" +#include "node_feature.hpp" #include <mbgl/platform/default/headless_display.hpp> #include <mbgl/util/exception.hpp> -#include <mbgl/util/work_request.hpp> #include <unistd.h> @@ -52,6 +51,7 @@ NAN_MODULE_INIT(NodeMap::Init) { Nan::SetPrototypeMethod(tpl, "render", Render); Nan::SetPrototypeMethod(tpl, "release", Release); Nan::SetPrototypeMethod(tpl, "dumpDebugLogs", DumpDebugLogs); + Nan::SetPrototypeMethod(tpl, "queryRenderedFeatures", QueryRenderedFeatures); constructor.Reset(tpl->GetFunction()); Nan::Set(target, Nan::New("Map").ToLocalChecked(), tpl->GetFunction()); @@ -188,7 +188,7 @@ NAN_METHOD(NodeMap::Load) { } try { - nodeMap->map->setStyleJSON(style, "."); + nodeMap->map->setStyleJSON(style); } catch (const std::exception &ex) { return Nan::ThrowError(ex.what()); } @@ -450,6 +450,56 @@ NAN_METHOD(NodeMap::DumpDebugLogs) { info.GetReturnValue().SetUndefined(); } +NAN_METHOD(NodeMap::QueryRenderedFeatures) { + auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder()); + Nan::HandleScope scope; + + if (!nodeMap->isValid()) return Nan::ThrowError(releasedMessage()); + + if (info.Length() <= 0 || !info[0]->IsArray()) { + return Nan::ThrowTypeError("First argument must be an array"); + } + + auto posOrBox = info[0].As<v8::Array>(); + if (posOrBox->Length() != 2) { + return Nan::ThrowTypeError("First argument must have two components"); + } + + try { + std::vector<mbgl::Feature> result; + + if (Nan::Get(posOrBox, 0).ToLocalChecked()->IsArray()) { + + auto pos0 = Nan::Get(posOrBox, 0).ToLocalChecked().As<v8::Array>(); + auto pos1 = Nan::Get(posOrBox, 1).ToLocalChecked().As<v8::Array>(); + + result = nodeMap->map->queryRenderedFeatures(mbgl::ScreenBox { + { + Nan::Get(pos0, 0).ToLocalChecked()->NumberValue(), + Nan::Get(pos0, 1).ToLocalChecked()->NumberValue() + }, { + Nan::Get(pos1, 0).ToLocalChecked()->NumberValue(), + Nan::Get(pos1, 1).ToLocalChecked()->NumberValue() + } + }); + + } else { + result = nodeMap->map->queryRenderedFeatures(mbgl::ScreenCoordinate { + Nan::Get(posOrBox, 0).ToLocalChecked()->NumberValue(), + Nan::Get(posOrBox, 1).ToLocalChecked()->NumberValue() + }); + } + + auto array = Nan::New<v8::Array>(); + for (unsigned int i = 0; i < result.size(); i++) { + array->Set(i, toJS(result[i])); + } + info.GetReturnValue().Set(array); + } catch (const std::exception &ex) { + return Nan::ThrowError(ex.what()); + } +} + //////////////////////////////////////////////////////////////////////////////////////////////// // Instance @@ -474,27 +524,17 @@ NodeMap::~NodeMap() { if (valid) release(); } -class NodeFileSourceRequest : public mbgl::FileRequest { -public: - std::unique_ptr<mbgl::WorkRequest> workRequest; -}; - -std::unique_ptr<mbgl::FileRequest> NodeMap::request(const mbgl::Resource& resource, Callback cb1) { - auto req = std::make_unique<NodeFileSourceRequest>(); - - // This function can be called from any thread. Make sure we're executing the - // JS implementation in the node event loop. - req->workRequest = NodeRunLoop().invokeWithCallback([this] (mbgl::Resource res, Callback cb2) { - Nan::HandleScope scope; +std::unique_ptr<mbgl::AsyncRequest> NodeMap::request(const mbgl::Resource& resource, Callback callback_) { + Nan::HandleScope scope; - auto requestHandle = NodeRequest::Create(res, cb2)->ToObject(); - auto callbackHandle = Nan::New<v8::Function>(NodeRequest::Respond, requestHandle); + auto requestHandle = NodeRequest::Create(resource, callback_)->ToObject(); + auto request = Nan::ObjectWrap::Unwrap<NodeRequest>(requestHandle); + auto callbackHandle = Nan::New<v8::Function>(NodeRequest::Respond, requestHandle); - v8::Local<v8::Value> argv[] = { requestHandle, callbackHandle }; - Nan::MakeCallback(handle()->GetInternalField(1)->ToObject(), "request", 2, argv); - }, cb1, resource); + v8::Local<v8::Value> argv[] = { requestHandle, callbackHandle }; + Nan::MakeCallback(handle()->GetInternalField(1)->ToObject(), "request", 2, argv); - return std::move(req); + return std::make_unique<NodeRequest::NodeAsyncRequest>(request); } -} +} // namespace node_mbgl diff --git a/platform/node/src/node_map.hpp b/platform/node/src/node_map.hpp index fe36ae7ed0..6e28eb541e 100644 --- a/platform/node/src/node_map.hpp +++ b/platform/node/src/node_map.hpp @@ -27,6 +27,7 @@ public: static NAN_METHOD(Render); static NAN_METHOD(Release); static NAN_METHOD(DumpDebugLogs); + static NAN_METHOD(QueryRenderedFeatures); void startRender(RenderOptions options); void renderFinished(); @@ -42,7 +43,7 @@ public: NodeMap(v8::Local<v8::Object>); ~NodeMap(); - std::unique_ptr<mbgl::FileRequest> request(const mbgl::Resource&, Callback); + std::unique_ptr<mbgl::AsyncRequest> request(const mbgl::Resource&, Callback); mbgl::HeadlessView view; std::unique_ptr<mbgl::Map> map; diff --git a/platform/node/src/node_mapbox_gl_native.cpp b/platform/node/src/node_mapbox_gl_native.cpp index e0b094d570..26c49918be 100644 --- a/platform/node/src/node_mapbox_gl_native.cpp +++ b/platform/node/src/node_mapbox_gl_native.cpp @@ -5,25 +5,18 @@ #include <nan.h> #pragma GCC diagnostic pop -#include "node_mapbox_gl_native.hpp" +#include <mbgl/util/run_loop.hpp> + #include "node_map.hpp" #include "node_log.hpp" #include "node_request.hpp" -namespace node_mbgl { - -mbgl::util::RunLoop& NodeRunLoop() { - static mbgl::util::RunLoop nodeRunLoop; - return nodeRunLoop; -} - -} - -NAN_MODULE_INIT(RegisterModule) { +void RegisterModule(v8::Local<v8::Object> target, v8::Local<v8::Object> module) { // This has the effect of: // a) Ensuring that the static local variable is initialized before any thread contention. // b) unreffing an async handle, which otherwise would keep the default loop running. - node_mbgl::NodeRunLoop().stop(); + static mbgl::util::RunLoop nodeRunLoop; + nodeRunLoop.stop(); node_mbgl::NodeMap::Init(target); node_mbgl::NodeRequest::Init(target); @@ -63,18 +56,21 @@ NAN_MODULE_INIT(RegisterModule) { Nan::New("Resource").ToLocalChecked(), resource); - // Make the exported object inherit from process.EventEmitter - v8::Local<v8::Object> process = Nan::Get( - Nan::GetCurrentContext()->Global(), - Nan::New("process").ToLocalChecked()).ToLocalChecked()->ToObject(); + // Make the exported object inherit from EventEmitter + v8::Local<v8::Function> require = Nan::Get(module, + Nan::New("require").ToLocalChecked()).ToLocalChecked().As<v8::Function>(); + + v8::Local<v8::Value> eventsString = Nan::New("events").ToLocalChecked(); + v8::Local<v8::Object> events = Nan::Call(require, module, 1, &eventsString).ToLocalChecked()->ToObject(); - v8::Local<v8::Object> EventEmitter = Nan::Get(process, + v8::Local<v8::Object> EventEmitter = Nan::Get(events, Nan::New("EventEmitter").ToLocalChecked()).ToLocalChecked()->ToObject(); Nan::SetPrototype(target, Nan::Get(EventEmitter, Nan::New("prototype").ToLocalChecked()).ToLocalChecked()); + Nan::CallAsFunction(EventEmitter, target, 0, nullptr); - mbgl::Log::setObserver(std::make_unique<node_mbgl::NodeLogObserver>(target->ToObject())); + mbgl::Log::setObserver(std::make_unique<node_mbgl::NodeLogObserver>(target)); } NODE_MODULE(mapbox_gl_native, RegisterModule) diff --git a/platform/node/src/node_mapbox_gl_native.hpp b/platform/node/src/node_mapbox_gl_native.hpp deleted file mode 100644 index b98b035fea..0000000000 --- a/platform/node/src/node_mapbox_gl_native.hpp +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include <mbgl/util/run_loop.hpp> - -namespace node_mbgl { - -mbgl::util::RunLoop& NodeRunLoop(); - -} diff --git a/platform/node/src/node_request.cpp b/platform/node/src/node_request.cpp index 50d7628a2b..fa560ed4e7 100644 --- a/platform/node/src/node_request.cpp +++ b/platform/node/src/node_request.cpp @@ -45,6 +45,14 @@ v8::Handle<v8::Object> NodeRequest::Create(const mbgl::Resource& resource, mbgl: NAN_METHOD(NodeRequest::Respond) { using Error = mbgl::Response::Error; + // Move out of the object so callback() can only be fired once. + auto request = Nan::ObjectWrap::Unwrap<NodeRequest>(info.Data().As<v8::Object>()); + auto callback = std::move(request->callback); + if (!callback) { + info.GetReturnValue().SetUndefined(); + return; + } + mbgl::Response response; if (info.Length() < 1) { @@ -79,14 +87,16 @@ NAN_METHOD(NodeRequest::Respond) { if (Nan::Has(res, Nan::New("modified").ToLocalChecked()).FromJust()) { const double modified = Nan::Get(res, Nan::New("modified").ToLocalChecked()).ToLocalChecked()->ToNumber()->Value(); if (!std::isnan(modified)) { - response.modified = mbgl::SystemClock::from_time_t(modified / 1000); + response.modified = mbgl::Timestamp{ mbgl::Seconds( + static_cast<mbgl::Seconds::rep>(modified / 1000)) }; } } if (Nan::Has(res, Nan::New("expires").ToLocalChecked()).FromJust()) { const double expires = Nan::Get(res, Nan::New("expires").ToLocalChecked()).ToLocalChecked()->ToNumber()->Value(); if (!std::isnan(expires)) { - response.expires = mbgl::SystemClock::from_time_t(expires / 1000); + response.expires = mbgl::Timestamp{ mbgl::Seconds( + static_cast<mbgl::Seconds::rep>(expires / 1000)) }; } } @@ -112,14 +122,39 @@ NAN_METHOD(NodeRequest::Respond) { } // Send the response object to the NodeFileSource object - Nan::ObjectWrap::Unwrap<NodeRequest>(info.Data().As<v8::Object>())->callback(response); + callback(response); info.GetReturnValue().SetUndefined(); } //////////////////////////////////////////////////////////////////////////////////////////////// // Instance +NodeRequest::NodeAsyncRequest::NodeAsyncRequest(NodeRequest* request_) : request(request_) { + assert(request); + // Make sure the JS object has a pointer to this so that it can remove its pointer in the + // destructor + request->asyncRequest = this; +} + +NodeRequest::NodeAsyncRequest::~NodeAsyncRequest() { + if (request) { + // Remove the callback function because the AsyncRequest was canceled and we are no longer + // interested in the result. + request->callback = {}; + request->asyncRequest = nullptr; + } +} + NodeRequest::NodeRequest(mbgl::FileSource::Callback callback_) - : callback(callback_) {} + : callback(callback_) { +} +NodeRequest::~NodeRequest() { + // When this object gets garbage collected, make sure that the AsyncRequest can no longer + // attempt to remove the callback function this object was holding (it can't be fired anymore). + if (asyncRequest) { + asyncRequest->request = nullptr; + } } + +} // namespace node_mbgl diff --git a/platform/node/src/node_request.hpp b/platform/node/src/node_request.hpp index 8e57cb30ec..2d307a3f19 100644 --- a/platform/node/src/node_request.hpp +++ b/platform/node/src/node_request.hpp @@ -12,6 +12,7 @@ namespace node_mbgl { class NodeFileSource; +class NodeRequest; class NodeRequest : public Nan::ObjectWrap { public: @@ -24,9 +25,17 @@ public: static Nan::Persistent<v8::Function> constructor; NodeRequest(mbgl::FileSource::Callback); + ~NodeRequest(); + + struct NodeAsyncRequest : public mbgl::AsyncRequest { + NodeAsyncRequest(NodeRequest*); + ~NodeAsyncRequest() override; + NodeRequest* request; + }; private: mbgl::FileSource::Callback callback; + NodeAsyncRequest* asyncRequest = nullptr; }; } diff --git a/platform/node/test/js/map.test.js b/platform/node/test/js/map.test.js index 1228900940..e8434bc774 100644 --- a/platform/node/test/js/map.test.js +++ b/platform/node/test/js/map.test.js @@ -220,6 +220,26 @@ test('Map', function(t) { t.end(); }); + t.test('returns an error delayed', function(t) { + var delay = 0; + var map = new mbgl.Map({ + request: function(req, callback) { + delay += 100; + setTimeout(function() { + callback(new Error('not found')); + }, delay); + }, + ratio: 1 + }); + map.load(style); + map.render({ zoom: 1 }, function(err, data) { + map.release(); + + t.ok(err, 'returns error'); + t.end(); + }); + }); + t.test('returns an error', function(t) { var map = new mbgl.Map(options); map.load(style); @@ -298,7 +318,9 @@ test('Map', function(t) { t.test('returning an error', function(t) { var map = new mbgl.Map({ request: function(req, callback) { - callback(new Error('request error')); + setImmediate(function () { + callback(new Error('request error')); + }); }, }); map.load(style); diff --git a/platform/node/test/js/require.js b/platform/node/test/js/require.js new file mode 100644 index 0000000000..1528002c20 --- /dev/null +++ b/platform/node/test/js/require.js @@ -0,0 +1 @@ +var mbgl = require('../../../../lib/mapbox-gl-native'); diff --git a/platform/node/test/query.test.js b/platform/node/test/query.test.js new file mode 100644 index 0000000000..1309c03467 --- /dev/null +++ b/platform/node/test/query.test.js @@ -0,0 +1,12 @@ +'use strict'; + +var suite = require('mapbox-gl-test-suite').query; +var suiteImplementation = require('./suite_implementation'); + +var tests; + +if (process.argv[1] === __filename && process.argv.length > 2) { + tests = process.argv.slice(2); +} + +suite.run('native', {tests: tests}, suiteImplementation); diff --git a/platform/node/test/render.test.js b/platform/node/test/render.test.js index 05a6b2ba68..0527a19070 100644 --- a/platform/node/test/render.test.js +++ b/platform/node/test/render.test.js @@ -1,8 +1,7 @@ 'use strict'; -var mbgl = require('../../../lib/mapbox-gl-native'); var suite = require('mapbox-gl-test-suite').render; -var request = require('request'); +var suiteImplementation = require('./suite_implementation'); var tests; @@ -10,47 +9,4 @@ if (process.argv[1] === __filename && process.argv.length > 2) { tests = process.argv.slice(2); } -mbgl.on('message', function(msg) { - console.log('%s (%s): %s', msg.severity, msg.class, msg.text); -}); - -suite.run('native', {tests: tests}, function (style, options, callback) { - var map = new mbgl.Map({ - ratio: options.pixelRatio, - request: function(req, callback) { - request(req.url, {encoding: null}, function (err, response, body) { - if (err) { - callback(err); - } else if (response.statusCode != 200) { - callback(new Error(response.statusMessage)); - } else { - callback(null, {data: body}); - } - }); - } - }); - - var timedOut = false; - var watchdog = setTimeout(function () { - timedOut = true; - map.dumpDebugLogs(); - callback(new Error('timed out after 20 seconds')); - }, 20000); - - options.center = style.center; - options.zoom = style.zoom; - options.bearing = style.bearing; - options.pitch = style.pitch; - options.debug = { - tileBorders: options.debug, - collision: options.collisionDebug - }; - - map.load(style); - map.render(options, function (err, pixels) { - map.release(); - if (timedOut) return; - clearTimeout(watchdog); - callback(err, pixels); - }); -}); +suite.run('native', {tests: tests}, suiteImplementation); diff --git a/platform/node/test/suite_implementation.js b/platform/node/test/suite_implementation.js new file mode 100644 index 0000000000..da226a68f4 --- /dev/null +++ b/platform/node/test/suite_implementation.js @@ -0,0 +1,58 @@ +'use strict'; + +var mbgl = require('../../../lib/mapbox-gl-native'); +var request = require('request'); + +mbgl.on('message', function(msg) { + console.log('%s (%s): %s', msg.severity, msg.class, msg.text); +}); + +module.exports = function (style, options, callback) { + var map = new mbgl.Map({ + ratio: options.pixelRatio, + request: function(req, callback) { + request(req.url, {encoding: null}, function (err, response, body) { + if (err) { + callback(err); + } else if (response.statusCode != 200) { + callback(new Error(response.statusMessage)); + } else { + callback(null, {data: body}); + } + }); + } + }); + + var timedOut = false; + var watchdog = setTimeout(function () { + timedOut = true; + map.dumpDebugLogs(); + callback(new Error('timed out after 20 seconds')); + }, 20000); + + options.center = style.center; + options.zoom = style.zoom; + options.bearing = style.bearing; + options.pitch = style.pitch; + options.debug = { + tileBorders: options.debug, + collision: options.collisionDebug + }; + + map.load(style); + + map.render(options, function (err, pixels) { + var results = options.queryGeometry ? + map.queryRenderedFeatures(options.queryGeometry) : + []; + map.release(); + if (timedOut) return; + clearTimeout(watchdog); + callback(err, pixels, results.map(prepareFeatures)); + }); + + function prepareFeatures(r) { + delete r.layer; + return r; + } +}; |