summaryrefslogtreecommitdiff
path: root/platform/node
diff options
context:
space:
mode:
Diffstat (limited to 'platform/node')
-rw-r--r--platform/node/CHANGELOG.md31
-rw-r--r--platform/node/README.md130
-rw-r--r--platform/node/bitrise.yml25
-rwxr-xr-xplatform/node/scripts/after_script.sh48
-rwxr-xr-xplatform/node/scripts/install.sh24
-rwxr-xr-xplatform/node/scripts/run.sh26
-rw-r--r--platform/node/src/node_feature.cpp168
-rw-r--r--platform/node/src/node_feature.hpp18
-rw-r--r--platform/node/src/node_log.cpp1
-rw-r--r--platform/node/src/node_map.cpp84
-rw-r--r--platform/node/src/node_map.hpp3
-rw-r--r--platform/node/src/node_mapbox_gl_native.cpp32
-rw-r--r--platform/node/src/node_mapbox_gl_native.hpp9
-rw-r--r--platform/node/src/node_request.cpp43
-rw-r--r--platform/node/src/node_request.hpp9
-rw-r--r--platform/node/test/js/map.test.js24
-rw-r--r--platform/node/test/js/require.js1
-rw-r--r--platform/node/test/query.test.js12
-rw-r--r--platform/node/test/render.test.js48
-rw-r--r--platform/node/test/suite_implementation.js58
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;
+ }
+};