summaryrefslogtreecommitdiff
path: root/platform
diff options
context:
space:
mode:
Diffstat (limited to 'platform')
-rw-r--r--platform/node/.clang-format18
-rw-r--r--platform/node/.gitignore5
-rw-r--r--platform/node/.travis.yml115
-rw-r--r--platform/node/CHANGELOG.md52
-rw-r--r--platform/node/CONTRIBUTING.md1
-rw-r--r--platform/node/LICENSE.txt27
-rw-r--r--platform/node/Makefile78
-rw-r--r--platform/node/README.md213
-rw-r--r--platform/node/binding.gyp79
-rw-r--r--platform/node/cloudformation/travis.template114
-rwxr-xr-xplatform/node/examples/load-from-fs.js37
-rw-r--r--platform/node/package.json51
-rwxr-xr-xplatform/node/scripts/deploy_results.sh15
-rwxr-xr-xplatform/node/scripts/install_node.sh13
-rwxr-xr-xplatform/node/scripts/travis_publish.sh31
-rwxr-xr-xplatform/node/scripts/travis_test.sh10
-rw-r--r--platform/node/src/node_file_source.cpp161
-rw-r--r--platform/node/src/node_file_source.hpp60
-rw-r--r--platform/node/src/node_log.cpp54
-rw-r--r--platform/node/src/node_log.hpp32
-rw-r--r--platform/node/src/node_map.cpp349
-rw-r--r--platform/node/src/node_map.hpp66
-rw-r--r--platform/node/src/node_mapbox_gl_native.cpp36
-rw-r--r--platform/node/src/node_request.cpp142
-rw-r--r--platform/node/src/node_request.hpp43
-rw-r--r--platform/node/src/util/async_queue.hpp90
-rw-r--r--platform/node/test/compare.js27
-rw-r--r--platform/node/test/expected/gzip/success.pngbin0 -> 42195 bytes
-rw-r--r--platform/node/test/expected/gzip/unhandled.pngbin0 -> 5636 bytes
-rw-r--r--platform/node/test/expected/map/image.pngbin0 -> 42195 bytes
-rw-r--r--platform/node/test/fixtures/style.json26
-rw-r--r--platform/node/test/fixtures/tiles.tilejson13
-rw-r--r--platform/node/test/fixtures/tiles/0-0-0.vector.pbfbin0 -> 9660 bytes
-rw-r--r--platform/node/test/js/consecutive.test.js83
-rw-r--r--platform/node/test/js/gzip.test.js126
-rw-r--r--platform/node/test/js/map.test.js293
-rw-r--r--platform/node/test/render.test.js160
37 files changed, 2620 insertions, 0 deletions
diff --git a/platform/node/.clang-format b/platform/node/.clang-format
new file mode 100644
index 0000000000..109b562b59
--- /dev/null
+++ b/platform/node/.clang-format
@@ -0,0 +1,18 @@
+Standard: Cpp11
+IndentWidth: 4
+AccessModifierOffset: -4
+UseTab: Never
+BinPackParameters: true
+AllowShortIfStatementsOnASingleLine: false
+AllowShortLoopsOnASingleLine: false
+AllowShortBlocksOnASingleLine: false
+AllowShortFunctionsOnASingleLine: false
+ConstructorInitializerAllOnOneLineOrOnePerLine: true
+AlwaysBreakTemplateDeclarations: true
+NamespaceIndentation: None
+PointerBindsToType: false
+SpacesInParentheses: false
+BreakBeforeBraces: Attach
+ColumnLimit: 100
+Cpp11BracedListStyle: false
+SpacesBeforeTrailingComments: 1
diff --git a/platform/node/.gitignore b/platform/node/.gitignore
new file mode 100644
index 0000000000..2f53e5d179
--- /dev/null
+++ b/platform/node/.gitignore
@@ -0,0 +1,5 @@
+build
+lib
+node_modules
+test/actual
+test/diff
diff --git a/platform/node/.travis.yml b/platform/node/.travis.yml
new file mode 100644
index 0000000000..891496e038
--- /dev/null
+++ b/platform/node/.travis.yml
@@ -0,0 +1,115 @@
+# to prevent Travis from exporting CXX after matrix env
+language: c
+
+sudo: false
+
+matrix:
+ include:
+ - os: linux
+ compiler: clang
+ env:
+ - CXX: clang++-3.5
+ - NODE_EXE: "iojs"
+ - NODE_VERSION: 2.0.1
+ addons:
+ apt:
+ sources: [ 'ubuntu-toolchain-r-test', 'llvm-toolchain-precise-3.5' ]
+ packages: [ 'gdb', 'clang-3.5', 'libstdc++-4.9-dev', 'libstdc++6', 'libllvm3.4', 'xutils-dev', 'libxxf86vm-dev', 'x11proto-xf86vidmode-dev', 'mesa-utils' ]
+ - os: linux
+ compiler: clang
+ env:
+ - CXX: clang++-3.5
+ - NODE_EXE: "node"
+ - NODE_VERSION: 0.12.0
+ addons:
+ apt:
+ sources: [ 'ubuntu-toolchain-r-test', 'llvm-toolchain-precise-3.5' ]
+ packages: [ 'gdb', 'clang-3.5', 'libstdc++-4.9-dev', 'libstdc++6', 'libllvm3.4', 'xutils-dev', 'libxxf86vm-dev', 'x11proto-xf86vidmode-dev', 'mesa-utils' ]
+ - os: linux
+ compiler: clang
+ env:
+ - CXX: clang++-3.5
+ - NODE_EXE: "node"
+ - NODE_VERSION: 0.10.36
+ addons:
+ apt:
+ sources: [ 'ubuntu-toolchain-r-test', 'llvm-toolchain-precise-3.5' ]
+ packages: [ 'gdb', 'clang-3.5', 'libstdc++-4.9-dev', 'libstdc++6', 'libllvm3.4', 'xutils-dev', 'libxxf86vm-dev', 'x11proto-xf86vidmode-dev', 'mesa-utils' ]
+ - os: linux
+ compiler: gcc
+ env:
+ - CXX: g++-4.9
+ - NODE_EXE: "iojs"
+ - NODE_VERSION: 2.0.1
+ addons:
+ apt:
+ sources: [ 'ubuntu-toolchain-r-test' ]
+ packages: [ 'gdb', 'g++-4.9', 'gcc-4.9', 'libllvm3.4', 'xutils-dev', 'libxxf86vm-dev', 'x11proto-xf86vidmode-dev', 'mesa-utils' ]
+ - os: linux
+ compiler: gcc
+ env:
+ - CXX: g++-4.9
+ - NODE_EXE: "node"
+ - NODE_VERSION: 0.12.0
+ addons:
+ apt:
+ sources: [ 'ubuntu-toolchain-r-test' ]
+ packages: [ 'gdb', 'g++-4.9', 'gcc-4.9', 'libllvm3.4', 'xutils-dev', 'libxxf86vm-dev', 'x11proto-xf86vidmode-dev', 'mesa-utils' ]
+ - os: linux
+ compiler: gcc
+ env:
+ - CXX: g++-4.9
+ - NODE_EXE: "node"
+ - NODE_VERSION: 0.10.36
+ addons:
+ apt:
+ sources: [ 'ubuntu-toolchain-r-test' ]
+ packages: [ 'gdb', 'g++-4.9', 'gcc-4.9', 'libllvm3.4', 'xutils-dev', 'libxxf86vm-dev', 'x11proto-xf86vidmode-dev', 'mesa-utils' ]
+ - os: osx
+ compiler: clang
+ env:
+ - CXX: clang++
+ - NODE_EXE: "iojs"
+ - NODE_VERSION: 2.0.1
+ - os: osx
+ compiler: clang
+ env:
+ - CXX: clang++
+ - NODE_EXE: "node"
+ - NODE_VERSION: 0.12.0
+ - os: osx
+ compiler: clang
+ env:
+ - CXX: clang++
+ - NODE_EXE: "node"
+ - NODE_VERSION: 0.10.36
+
+env:
+ global:
+ - LD_LIBRARY_PATH: "/usr/local/lib"
+ - secure: pz/HAMQpnde//JJi3f+RcW32APN6g3QyFAH41JlZwgsM5Daj9RRoXqUcNg4hEyTvlThtc5t+wQQ2ejYAjMwDu00GAzRFJ03Sm45w2fPvith9fu4crXsyPUvWUfWPC0ajTXzorN4cwFwOoMoeo9DihLwb0EC0n4T6jWdBCae3k+s=
+ - secure: jYFAOQoMZkZVyc5AFPBKhR9oDqp5CciwCxFhVEHVjdImM+8V60loKazyw+bVLIjzKLnQbKKdiDudWL9TE1ylK/XSlF3K7o4gU2vIh5WzosKnU70Sanxd6tHt/Ui5eK+bAymKHbLdGiXIZtBZE0tML6+wgJ9vhV4ZKna5dM9tps4=
+
+before_install:
+- source ./vendor/mbgl/scripts/travis_helper.sh
+- pushd ./vendor/mbgl
+- source ./scripts/${TRAVIS_OS_NAME}/install.sh
+- source ./scripts/${TRAVIS_OS_NAME}/setup.sh
+- popd
+- source ./scripts/install_node.sh
+
+install:
+- npm install --build-from-source
+
+before_script:
+- ulimit -c unlimited -S
+- ulimit -a
+
+script:
+- "./scripts/travis_test.sh"
+
+after_success:
+- "./scripts/travis_publish.sh"
+
+git:
+ submodules: true
diff --git a/platform/node/CHANGELOG.md b/platform/node/CHANGELOG.md
new file mode 100644
index 0000000000..a86be910ef
--- /dev/null
+++ b/platform/node/CHANGELOG.md
@@ -0,0 +1,52 @@
+# 1.1.3
+
+- Removes deprecated mbgl::Environment from NodeLogObserver.
+
+# 1.1.2
+
+- Check libuv version semver-ishly, fixes segfaults in Node.js 0.12.x
+ and iojs.
+- Fixes segfault, throws JavaScript error instead when attempting to
+ render without first loading a style.
+- Bumps mbgl submodule to v0.4.0
+
+# 1.1.1
+
+- Bumps mbgl submodule to v0.3.5
+
+# 1.1.0
+
+- Adds Node.js v0.12.x and io.js support.
+- Adds `map.release()` method for manual cleanup of map resources.
+- Fixes garbage collection of NodeMap objects.
+- Returns an error callback for failed NodeFileSource requests.
+- Fixes handling of corrupt NodeFileSource request data.
+- Implements request coalescing to fix NodeRequest cancellation.
+- Removes `setAccessToken` method from NodeMap, `mapbox://` URLs
+ should be handled through `NodeFileSource` now.
+- Updates build scripts and Travis CI configuration.
+- Logs Environment ID and thread name when in an Environment scope.
+- Refactors NodeLog to implement mbgl::Log::Observer.
+- Fixes uncaught exception from missing sprites.
+- Fixes Unicode glyph range end.
+
+# 1.0.3
+
+- Fixes crash during garbage collection by assigning FileSource handle
+ to a v8::Persistent in NodeMap constructor.
+
+# 1.0.2
+
+- Initialize shared display connection at module load time to avoid
+ race condition when display connection is initialized on-demand.
+
+# 1.0.1
+
+- Adapts NodeFileSource around mbgl::Environment additions.
+- Adapts to minor changes in mapbox-gl-test-suite.
+- Adds tests for gzipped vector tile handling.
+- Cleans up documentation.
+
+# 1.0.0
+
+- Initial release.
diff --git a/platform/node/CONTRIBUTING.md b/platform/node/CONTRIBUTING.md
new file mode 100644
index 0000000000..166929e33f
--- /dev/null
+++ b/platform/node/CONTRIBUTING.md
@@ -0,0 +1 @@
+Please run [`clang-format`](http://clang.llvm.org/docs/ClangFormat.html) and verify that code formatting will remain consistent before submitting a pull request.
diff --git a/platform/node/LICENSE.txt b/platform/node/LICENSE.txt
new file mode 100644
index 0000000000..c9018a1390
--- /dev/null
+++ b/platform/node/LICENSE.txt
@@ -0,0 +1,27 @@
+Copyright (c) 2014, Mapbox
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of [project] nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/platform/node/Makefile b/platform/node/Makefile
new file mode 100644
index 0000000000..74b911a142
--- /dev/null
+++ b/platform/node/Makefile
@@ -0,0 +1,78 @@
+BUILDTYPE ?= Release
+MBGL ?= vendor/mbgl
+
+DEBUG_FLAG =
+ifeq ($(BUILDTYPE), Debug)
+DEBUG_FLAG = -d
+endif
+
+ifeq ($(shell uname -s), Darwin)
+HOST ?= osx
+ifeq ($(JOBS),)
+JOBS = $(shell sysctl -n hw.ncpu)
+endif
+endif
+ifeq ($(shell uname -s), Linux)
+HOST ?= linux
+ifeq ($(JOBS),)
+JOBS = $(shell nproc)
+endif
+endif
+
+NODE_PRE_GYP = $(shell which node-pre-gyp)
+ifeq ($(NODE_PRE_GYP),)
+NODE_PRE_GYP = $(shell npm bin)/node-pre-gyp
+endif
+
+# Explicitly disable the default FileSource implementation
+ASSET = none
+HTTP = none
+CACHE = none
+
+include $(MBGL)/config/defaults.mk
+
+global: build
+
+.PHONY: build
+build: build/Makefile
+ @$(NODE_PRE_GYP) build $(DEBUG_FLAG) --clang -- -j$(JOBS)
+
+vendor/mbgl:
+ git submodule update --init
+
+GYP_SETTINGS = -Dmbgl=$(MBGL) \
+ -Dhost=$(HOST) \
+ -I$(MBGL)/config/$(HOST).gypi \
+ $(LIBS_$(HOST)) \
+ -Duv_static_libs= -Duv_ldflags=
+
+.PHONY: build/Makefile
+build/Makefile: $(MBGL)/config/$(HOST).gypi
+ @$(NODE_PRE_GYP) configure --clang -- $(GYP_SETTINGS) -f make
+
+.PHONY: build/binding.xcodeproj
+build/binding.xcodeproj: $(MBGL)/config/$(HOST).gypi
+ @$(NODE_PRE_GYP) configure --clang -- $(GYP_SETTINGS) -f xcode
+
+.PHONY: xproj
+xproj: build/binding.xcodeproj
+ open build/binding.xcodeproj
+
+$(MBGL)/config/%.gypi: $(MBGL) $(MBGL)/configure
+ make -C $(MBGL) config/$*.gypi geojsonvt
+
+.PHONY: test-suite
+test-suite:
+ -@(`npm bin`/tape test/render.test.js | `npm bin`/faucet)
+
+.PHONY: test-js
+test-js:
+ @(`npm bin`/tape test/js/**/*.test.js | `npm bin`/faucet)
+
+.PHONY: test
+test: test-js test-suite
+
+.PHONY: clean
+clean:
+ rm -rf build lib
+ rm -f $(MBGL)/config/$(HOST).gypi
diff --git a/platform/node/README.md b/platform/node/README.md
new file mode 100644
index 0000000000..1b7d4b2d6d
--- /dev/null
+++ b/platform/node/README.md
@@ -0,0 +1,213 @@
+# node-mapbox-gl-native
+
+Renders map tiles with [Mapbox GL](https://github.com/mapbox/mapbox-gl-native).
+
+
+[![NPM](https://nodei.co/npm/mapbox-gl-native.png)](https://npmjs.org/package/mapbox-gl-native)
+
+[![Build Status](https://travis-ci.org/mapbox/node-mapbox-gl-native.svg?branch=master)](https://travis-ci.org/mapbox/node-mapbox-gl-native)
+
+## Installing
+
+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 v0.10.x, Node.js v0.12.x, io.js v2.x
+
+Just run:
+
+```
+npm install mapbox-gl-native
+```
+
+Other platforms will fall back to a source compile with `npm run build`. 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#depends).
+
+## Rendering a map tile
+
+```js
+var map = new mbgl.Map({ request: function() {} });
+map.load(require('./test/fixtures/style.json'));
+map.render({}, function(err, image) {
+ if (err) throw err;
+ fs.writeFileSync('image.png', image);
+});
+```
+
+The first argument passed to `map.render` is an options object, all keys are optional:
+
+```js
+{
+ zoom: {zoom}, // number, defaults to 0
+ width: {width}, // number (px), defaults to 512
+ height: {height}, // number (px), defaults to 512
+ center: [{latitude}, {longitude}], // array of numbers (coordinates), defaults to [0,0]
+ bearing: {bearing}, // number (in degrees, counter-clockwise from north), defaults to 0
+ ratio: {ratio} // number (scale factor), defaults to 1.0,
+ classes: {classes} // array of strings, optional
+}
+```
+
+_More about classes: https://github.com/mapbox/mapbox-gl-js/blob/master/API.md#working-with-style-classes_
+
+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.
+
+## Testing
+
+```
+npm test
+```
+
+## Implementing a file source
+
+When creating a `Map`, you must pass an options object (with a required `request` and optional `cancel` method) as the first parameter.
+
+```js
+var map = new mbgl.Map({
+ request: function(req) {
+ // TODO
+ },
+ cancel: function(req) {
+ // TODO
+ }
+});
+```
+
+The `request()` method starts a new request to a file, while `cancel()` tells the FileSource to cancel the request (if possible). The `req` parameter has two properties:
+
+```json
+{
+ "url": "http://example.com",
+ "kind": 1
+}
+```
+
+The `kind` is an enum and defined in `mbgl.Resource`:
+
+```json
+{
+ "Unknown": 0,
+ "Style": 1,
+ "Source": 2,
+ "Tile": 3,
+ "Glyphs": 4,
+ "JSON": 5,
+ "Image": 6
+}
+```
+
+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.
+
+A sample implementation that reads files from disk would look like the following:
+
+```js
+var map = new mbgl.Map({
+ request: function(req) {
+ fs.readFile(path.join('base/path', req.url), function(err, data) {
+ req.respond(err, { data: data });
+ });
+ }
+});
+```
+
+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 `req.respond()`, which takes either an error object or `null` and an object with several settings:
+
+```js
+{
+ modified: new Date(),
+ expires: new Date(),
+ etag: "string",
+ data: new Buffer()
+};
+```
+
+A sample implementation that uses [`request`](https://github.com/request/request) to query data from HTTP:
+
+```js
+var mbgl = require('mapbox-gl-native');
+var request = require('request');
+
+var map = new mbgl.Map({
+ request: function(req) {
+ request({
+ url: req.url,
+ encoding: null,
+ gzip: true
+ }, function (err, res, body) {
+ if (req.canceled) {
+ return;
+ }
+
+ if (err) {
+ req.respond(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;
+ req.respond(null, response);
+ } else {
+ req.respond(new Error(JSON.parse(body).message));
+ }
+ });
+ },
+ cancel: function(req) {
+ req.canceled = true;
+ }
+});
+```
+
+Note that in reality, 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.
+
+## 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 options = {};
+options.request = function(req) {
+ 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) {
+ req.respond(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;
+ req.respond(null, response);
+ } else {
+ req.respond(new Error(JSON.parse(body).message));
+ }
+ });
+}
+
+var map = new mbgl.Map(options);
+var style = mapboxStyle; // includes a datasource with a reference to something like `mapbox://mapbox.mapbox-streets-v6`
+
+map.load(style);
+map.render({}, function(err, image) {
+ if (err) throw err;
+ fs.writeFileSync('image.png', image);
+});
+
+```
+
diff --git a/platform/node/binding.gyp b/platform/node/binding.gyp
new file mode 100644
index 0000000000..a5fb583399
--- /dev/null
+++ b/platform/node/binding.gyp
@@ -0,0 +1,79 @@
+{
+ 'targets': [
+ { 'target_name': '<(module_name)',
+ 'dependencies': [
+ './<(mbgl)/mbgl.gyp:core',
+ './<(mbgl)/mbgl.gyp:platform-<(platform_lib)',
+ './<(mbgl)/mbgl.gyp:headless-<(headless_lib)',
+ ],
+
+ 'include_dirs': [ "<!(node -e \"require('nan')\")" ],
+
+ 'sources': [
+ 'src/node_mapbox_gl_native.cpp',
+ 'src/node_log.hpp',
+ 'src/node_log.cpp',
+ 'src/node_file_source.hpp',
+ 'src/node_file_source.cpp',
+ 'src/node_map.hpp',
+ 'src/node_map.cpp',
+ 'src/node_request.hpp',
+ 'src/node_request.cpp',
+ 'src/util/async_queue.hpp',
+ ],
+
+ 'conditions': [
+ ['OS=="mac"', {
+ 'xcode_settings': {
+ 'CLANG_CXX_LIBRARY': 'libc++',
+ 'GCC_VERSION': 'com.apple.compilers.llvm.clang.1_0',
+ 'GCC_ENABLE_CPP_EXCEPTIONS': 'YES',
+ 'GCC_ENABLE_CPP_RTTI': 'YES',
+ 'OTHER_CPLUSPLUSFLAGS': [
+ '-std=c++1y',
+ '-Werror',
+ '-Wall',
+ '-Wextra',
+ '-Wshadow',
+ '-Wno-variadic-macros',
+ '-Wno-error=unused-parameter',
+ '-frtti',
+ '-fexceptions',
+ ],
+ 'GCC_WARN_PEDANTIC': 'YES',
+ 'GCC_WARN_UNINITIALIZED_AUTOS': 'YES_AGGRESSIVE',
+ 'MACOSX_DEPLOYMENT_TARGET': '10.9',
+ },
+ }, {
+ 'cflags_cc': [
+ '-std=c++14',
+ '-Werror',
+ '-Wall',
+ '-Wextra',
+ '-Wno-variadic-macros',
+ '-Wno-error=unused-parameter',
+ '-frtti',
+ '-fexceptions',
+ ],
+ 'libraries': [ '<@(glfw3_ldflags)' ],
+ }],
+ ['OS=="linux"', {
+ 'cflags_cc': [
+ '-Wno-unknown-pragmas', # We are using '#pragma mark', but it is only available on Darwin.
+ ],
+ }],
+ ],
+ },
+
+ { 'target_name': 'action_after_build',
+ 'type': 'none',
+ 'dependencies': [ '<(module_name)' ],
+ 'copies': [
+ {
+ 'files': [ '<(PRODUCT_DIR)/<(module_name).node' ],
+ 'destination': '<(module_path)'
+ }
+ ]
+ }
+ ]
+}
diff --git a/platform/node/cloudformation/travis.template b/platform/node/cloudformation/travis.template
new file mode 100644
index 0000000000..3ba9b9625e
--- /dev/null
+++ b/platform/node/cloudformation/travis.template
@@ -0,0 +1,114 @@
+
+{
+ "AWSTemplateFormatVersion": "2010-09-09",
+ "Description": "node-mapbox-gl-native travis resources",
+ "Resources": {
+ "BuildUser": {
+ "Type": "AWS::IAM::User",
+ "Properties": {
+ "Policies": [
+ {
+ "PolicyName": "build",
+ "PolicyDocument": {
+ "Statement": [
+ {
+ "Action": [
+ "s3:DeleteObject",
+ "s3:GetObject",
+ "s3:GetObjectAcl",
+ "s3:PutObject",
+ "s3:PutObjectAcl"
+ ],
+ "Resource": [
+ "arn:aws:s3:::mapbox-node-binary/mapbox-gl-native/*"
+ ],
+ "Effect": "Allow"
+ }
+ ]
+ }
+ },
+ {
+ "PolicyName": "list",
+ "PolicyDocument": {
+ "Statement": [
+ {
+ "Action": [
+ "s3:ListBucket"
+ ],
+ "Resource": [
+ "arn:aws:s3:::mapbox-node-binary"
+ ],
+ "Effect": "Allow"
+ }
+ ]
+ }
+ },
+ {
+ "PolicyName": "build-testing",
+ "PolicyDocument": {
+ "Statement": [
+ {
+ "Action": [
+ "s3:DeleteObject",
+ "s3:GetObject",
+ "s3:GetObjectAcl",
+ "s3:PutObject",
+ "s3:PutObjectAcl"
+ ],
+ "Effect": "Allow",
+ "Resource": [
+ "arn:aws:s3:::mapbox/node-mapbox-gl-native/*"
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "PolicyName": "list-testing",
+ "PolicyDocument": {
+ "Statement": [
+ {
+ "Action": [
+ "s3:ListBucket"
+ ],
+ "Effect": "Allow",
+ "Resource": [
+ "arn:aws:s3:::mapbox"
+ ],
+ "Condition": {
+ "StringLike": {
+ "s3:prefix": "node-mapbox-gl-native/*"
+ }
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ "BuildUserKey": {
+ "Type": "AWS::IAM::AccessKey",
+ "Properties": {
+ "UserName": {
+ "Ref": "BuildUser"
+ }
+ }
+ }
+ },
+ "Outputs": {
+ "AccessKeyId": {
+ "Value": {
+ "Ref": "BuildUserKey"
+ }
+ },
+ "SecretAccessKey": {
+ "Value": {
+ "Fn::GetAtt": [
+ "BuildUserKey",
+ "SecretAccessKey"
+ ]
+ }
+ }
+ }
+}
diff --git a/platform/node/examples/load-from-fs.js b/platform/node/examples/load-from-fs.js
new file mode 100755
index 0000000000..352b015573
--- /dev/null
+++ b/platform/node/examples/load-from-fs.js
@@ -0,0 +1,37 @@
+#!/usr/bin/env node
+/* jshint node: true, unused: false */
+'use strict';
+
+var mbgl = require('..');
+var fs = require('fs');
+var path = require('path');
+var PNG = require('pngjs').PNG;
+
+var base = path.join(path.dirname(process.mainModule.filename), '../test');
+
+var map = new mbgl.Map({
+ request: function(req) {
+ fs.readFile(path.join(base, req.url), function(err, data) {
+ req.respond(err, { data: data });
+ });
+ }
+});
+
+map.load(require('../test/fixtures/style.json'));
+
+map.render({}, function(err, data) {
+ if (err) throw err;
+
+ var png = new PNG({
+ width: data.width,
+ height: data.height
+ });
+
+ png.data = data.pixels;
+
+ png.pack()
+ .pipe(fs.createWriteStream('image.png'))
+ .on('finish', function() {
+ console.warn('Written image.png');
+ });
+});
diff --git a/platform/node/package.json b/platform/node/package.json
new file mode 100644
index 0000000000..7ff254dce9
--- /dev/null
+++ b/platform/node/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "mapbox-gl-native",
+ "version": "1.1.3",
+ "description": "Renders map tiles with Mapbox GL",
+ "keywords": [
+ "mapbox",
+ "gl"
+ ],
+ "main": "lib/mapbox-gl-native.node",
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/mapbox/node-mapbox-gl-native.git"
+ },
+ "licenses": [
+ {
+ "type": "BSD",
+ "url": "https://github.com/mapbox/node-mapbox-gl-native/blob/master/LICENSE.txt"
+ }
+ ],
+ "dependencies": {
+ "nan": "^1.9.0",
+ "node-pre-gyp": "^0.6.7"
+ },
+ "bundledDependencies": [
+ "node-pre-gyp"
+ ],
+ "devDependencies": {
+ "aws-sdk": "^2.1.27",
+ "faucet": "0.0.1",
+ "mapbox-gl-test-suite": "git://github.com/mapbox/mapbox-gl-test-suite#7251829de9d2804fe02cd8f7acc0e30ea278429d",
+ "mkdirp": "^0.5.1",
+ "pngjs": "^0.4.0",
+ "request": "^2.55.0",
+ "st": "^0.5.3",
+ "tape": "^4.0.0"
+ },
+ "scripts": {
+ "install": "node-pre-gyp install || make",
+ "test": "make test",
+ "rebuild": "make clean && make",
+ "build": "make"
+ },
+ "gypfile": true,
+ "binary": {
+ "module_name": "mapbox-gl-native",
+ "module_path": "./lib/",
+ "host": "https://mapbox-node-binary.s3.amazonaws.com",
+ "remote_path": "./{name}/v{version}",
+ "package_name": "{node_abi}-{platform}-{arch}.tar.gz"
+ }
+}
diff --git a/platform/node/scripts/deploy_results.sh b/platform/node/scripts/deploy_results.sh
new file mode 100755
index 0000000000..adbed4c2d8
--- /dev/null
+++ b/platform/node/scripts/deploy_results.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+
+set -e
+set -o pipefail
+
+if [ ! -z "${AWS_ACCESS_KEY_ID}" ] && [ ! -z "${AWS_SECRET_ACCESS_KEY}" ] ; then
+ # Install and add awscli to PATH for uploading the results
+ mapbox_time "install_awscli" \
+ pip install --user awscli
+ export PATH="`python -m site --user-base`/bin:${PATH}"
+
+ mapbox_time_start "deploy_results"
+ (cd ./node_modules/mapbox-gl-test-suite/ && ./bin/deploy_results.sh)
+ mapbox_time_finish
+fi
diff --git a/platform/node/scripts/install_node.sh b/platform/node/scripts/install_node.sh
new file mode 100755
index 0000000000..311b004243
--- /dev/null
+++ b/platform/node/scripts/install_node.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+set -e
+set -o pipefail
+
+# Mason exists on PATH from sourcing mbgl install script
+mapbox_time $NODE_EXE \
+mason install $NODE_EXE $NODE_VERSION
+
+export PATH="`mason prefix $NODE_EXE $NODE_VERSION`/bin":"$PATH"
+
+$NODE_EXE --version
+npm --version
diff --git a/platform/node/scripts/travis_publish.sh b/platform/node/scripts/travis_publish.sh
new file mode 100755
index 0000000000..d06a2a8a30
--- /dev/null
+++ b/platform/node/scripts/travis_publish.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+set -e
+
+# Inspect binary.
+if [[ ${TRAVIS_OS_NAME} == "linux" ]]; then
+ ldd ./lib/mapbox-gl-native.node
+else
+ otool -L ./lib/mapbox-gl-native.node
+fi
+
+COMMIT_MESSAGE=$(git show -s --format=%B $TRAVIS_COMMIT | tr -d '\n')
+
+if test "${COMMIT_MESSAGE#*'[publish binary]'}" != "$COMMIT_MESSAGE" && [[ $CXX =~ ^clang* ]]; then
+ npm install aws-sdk
+
+ ./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
+ rm -rf build
+ rm -rf lib
+ npm install --fallback-to-build=false
+ npm test
+ fi
+fi
diff --git a/platform/node/scripts/travis_test.sh b/platform/node/scripts/travis_test.sh
new file mode 100755
index 0000000000..70fff88aa4
--- /dev/null
+++ b/platform/node/scripts/travis_test.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+set -e
+set -o pipefail
+
+# Travis OS X has no GPU
+if [[ ${TRAVIS_OS_NAME} == "linux" ]]; then
+ npm test
+ ./scripts/deploy_results.sh
+fi
diff --git a/platform/node/src/node_file_source.cpp b/platform/node/src/node_file_source.cpp
new file mode 100644
index 0000000000..204e85a126
--- /dev/null
+++ b/platform/node/src/node_file_source.cpp
@@ -0,0 +1,161 @@
+#include "node_file_source.hpp"
+#include "node_request.hpp"
+#include "util/async_queue.hpp"
+
+#include <mbgl/storage/request.hpp>
+
+namespace node_mbgl {
+
+struct NodeFileSource::Action {
+ const enum : bool { Add, Cancel } type;
+ mbgl::Resource const resource;
+};
+
+NodeFileSource::NodeFileSource(v8::Handle<v8::Object> options_) :
+ queue(new Queue(uv_default_loop(), [this](Action &action) {
+ if (action.type == Action::Add) {
+ processAdd(action.resource);
+ } else if (action.type == Action::Cancel) {
+ processCancel(action.resource);
+ }
+ }))
+{
+ NanAssignPersistent(options, options_->ToObject());
+
+ // Make sure that the queue doesn't block the loop from exiting.
+ queue->unref();
+}
+
+NodeFileSource::~NodeFileSource() {
+ queue->stop();
+ queue = nullptr;
+
+ NanDisposePersistent(options);
+}
+
+mbgl::Request* NodeFileSource::request(const mbgl::Resource& resource, uv_loop_t* loop, Callback callback) {
+ auto req = new mbgl::Request(resource, loop, std::move(callback));
+
+ std::lock_guard<std::mutex> lock(observersMutex);
+
+ assert(observers.find(resource) == observers.end());
+ observers[resource] = req;
+
+ // This function can be called from any thread. Make sure we're executing the actual call in the
+ // file source loop by sending it over the queue. It will be processed in processAction().
+ queue->send(Action{ Action::Add, resource });
+
+ return req;
+}
+
+void NodeFileSource::cancel(mbgl::Request* req) {
+ req->cancel();
+
+ std::lock_guard<std::mutex> lock(observersMutex);
+
+ auto it = observers.find(req->resource);
+ if (it == observers.end()) {
+ return;
+ }
+
+ observers.erase(it);
+
+ // This function can be called from any thread. Make sure we're executing the actual call in the
+ // file source loop by sending it over the queue. It will be processed in processAction().
+ queue->send(Action{ Action::Cancel, req->resource });
+
+ req->destruct();
+}
+
+void NodeFileSource::processAdd(const mbgl::Resource& resource) {
+ NanScope();
+
+ // Make sure the loop stays alive as long as request is pending.
+ if (pending.empty()) {
+ queue->ref();
+ }
+
+ auto requestHandle = NanNew<v8::Object>(NodeRequest::Create(this, resource));
+
+ v8::Persistent<v8::Object> requestPersistent;
+ NanAssignPersistent(requestPersistent, requestHandle);
+ pending.emplace(resource, std::move(requestPersistent));
+
+#if (NODE_MODULE_VERSION > NODE_0_10_MODULE_VERSION)
+ auto requestFunction = v8::Local<v8::Object>::New(v8::Isolate::GetCurrent(), options)->Get(NanNew("request")).As<v8::Function>();
+#else
+ auto requestFunction = options->Get(NanNew("request")).As<v8::Function>();
+#endif
+
+ v8::Local<v8::Value> argv[] = { requestHandle };
+ NanMakeCallback(NanGetCurrentContext()->Global(), requestFunction, 1, argv);
+}
+
+void NodeFileSource::processCancel(const mbgl::Resource& resource) {
+ NanScope();
+
+ auto it = pending.find(resource);
+ if (it == pending.end()) {
+ // The response callback was already fired. There is no point in calling the cancelation
+ // callback because the request is already completed.
+ } else {
+#if (NODE_MODULE_VERSION > NODE_0_10_MODULE_VERSION)
+ auto requestHandle = v8::Local<v8::Object>::New(v8::Isolate::GetCurrent(), it->second);
+ it->second.Reset();
+#else
+ auto requestHandle = NanNew<v8::Object>(it->second);
+ NanDisposePersistent(it->second);
+#endif
+ pending.erase(it);
+
+ // Make sure the the loop can exit when there are no pending requests.
+ if (pending.empty()) {
+ queue->unref();
+ }
+
+#if (NODE_MODULE_VERSION > NODE_0_10_MODULE_VERSION)
+ auto optionsObject = v8::Local<v8::Object>::New(v8::Isolate::GetCurrent(), options);
+ if (optionsObject->Has(NanNew("cancel"))) {
+ auto cancelFunction = optionsObject->Get(NanNew("cancel")).As<v8::Function>();
+#else
+ if (options->Has(NanNew("cancel"))) {
+ auto cancelFunction = options->Get(NanNew("cancel")).As<v8::Function>();
+#endif
+ v8::Local<v8::Value> argv[] = { requestHandle };
+ NanMakeCallback(NanGetCurrentContext()->Global(), cancelFunction, 1, argv);
+ }
+
+ // Set the request handle in the request wrapper handle to null
+ node::ObjectWrap::Unwrap<NodeRequest>(requestHandle)->cancel();
+ }
+}
+
+void NodeFileSource::notify(const mbgl::Resource& resource, const std::shared_ptr<const mbgl::Response>& response) {
+ // First, remove the request, since it might be destructed at any point now.
+ auto it = pending.find(resource);
+ if (it != pending.end()) {
+#if (NODE_MODULE_VERSION > NODE_0_10_MODULE_VERSION)
+ it->second.Reset();
+#else
+ NanDisposePersistent(it->second);
+#endif
+ pending.erase(it);
+
+ // Make sure the the loop can exit when there are no pending requests.
+ if (pending.empty()) {
+ queue->unref();
+ }
+ }
+
+ std::lock_guard<std::mutex> lock(observersMutex);
+
+ auto observersIt = observers.find(resource);
+ if (observersIt == observers.end()) {
+ return;
+ }
+
+ observersIt->second->notify(response);
+ observers.erase(observersIt);
+}
+
+}
diff --git a/platform/node/src/node_file_source.hpp b/platform/node/src/node_file_source.hpp
new file mode 100644
index 0000000000..f412bdee16
--- /dev/null
+++ b/platform/node/src/node_file_source.hpp
@@ -0,0 +1,60 @@
+#pragma once
+
+#include <mbgl/storage/file_source.hpp>
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wshadow"
+#include <node.h>
+#include <node_version.h>
+#include <nan.h>
+#pragma GCC diagnostic pop
+
+#include <memory>
+#include <mutex>
+#include <unordered_map>
+
+namespace node_mbgl {
+
+namespace util { template <typename T> class AsyncQueue; }
+
+class NodeFileSource : public mbgl::FileSource {
+public:
+ NodeFileSource(v8::Handle<v8::Object>);
+ ~NodeFileSource();
+
+ mbgl::Request* request(const mbgl::Resource&, uv_loop_t*, Callback);
+ void cancel(mbgl::Request*);
+
+ // visiblity?
+ void notify(const mbgl::Resource&, const std::shared_ptr<const mbgl::Response>&);
+
+private:
+ struct Action;
+ using Queue = util::AsyncQueue<Action>;
+
+ void processAdd(const mbgl::Resource&);
+ void processCancel(const mbgl::Resource&);
+
+ v8::Persistent<v8::Object> options;
+
+private:
+#if (NODE_MODULE_VERSION > NODE_0_10_MODULE_VERSION)
+ std::unordered_map<mbgl::Resource, v8::Persistent<v8::Object, v8::CopyablePersistentTraits<v8::Object>>, mbgl::Resource::Hash> pending;
+#else
+ std::unordered_map<mbgl::Resource, v8::Persistent<v8::Object>, mbgl::Resource::Hash> pending;
+#endif
+
+ // The observers list will hold pointers to all the requests waiting
+ // for a particular resource. The access must be guarded by a mutex
+ // because the list is also accessed by a thread from the mbgl::Map
+ // object and from the main thread when notifying requests of
+ // completion. Concurrent access is specially needed when
+ // canceling a request to avoid a deadlock (see #129).
+ std::unordered_map<mbgl::Resource, mbgl::Request*, mbgl::Resource::Hash> observers;
+ std::mutex observersMutex;
+
+ Queue *queue = nullptr;
+};
+
+}
diff --git a/platform/node/src/node_log.cpp b/platform/node/src/node_log.cpp
new file mode 100644
index 0000000000..6375348070
--- /dev/null
+++ b/platform/node/src/node_log.cpp
@@ -0,0 +1,54 @@
+#include "node_log.hpp"
+#include "util/async_queue.hpp"
+
+namespace node_mbgl {
+
+struct NodeLogObserver::LogMessage {
+ mbgl::EventSeverity severity;
+ mbgl::Event event;
+ int64_t code;
+ std::string text;
+
+ LogMessage(mbgl::EventSeverity severity_, mbgl::Event event_, int64_t code_, std::string text_)
+ : severity(severity_),
+ event(event_),
+ code(code_),
+ text(text_) {}
+};
+
+NodeLogObserver::NodeLogObserver(v8::Handle<v8::Object> target)
+ : queue(new Queue(uv_default_loop(), [this](LogMessage &message) {
+ NanScope();
+
+ auto msg = NanNew<v8::Object>();
+ msg->Set(NanNew("class"), NanNew(mbgl::EventClass(message.event).c_str()));
+ msg->Set(NanNew("severity"), NanNew(mbgl::EventSeverityClass(message.severity).c_str()));
+ if (message.code != -1) {
+ msg->Set(NanNew("code"), NanNew<v8::Number>(message.code));
+ }
+ if (!message.text.empty()) {
+ msg->Set(NanNew("text"), NanNew(message.text));
+ }
+
+ v8::Local<v8::Value> argv[] = { NanNew("message"), msg };
+ auto handle = NanNew<v8::Object>(module);
+ auto emit = handle->Get(NanNew("emit"))->ToObject();
+ emit->CallAsFunction(handle, 2, argv);
+ })) {
+ NanScope();
+ NanAssignPersistent(module, target);
+
+ // Don't keep the event loop alive.
+ queue->unref();
+}
+
+NodeLogObserver::~NodeLogObserver() {
+ queue->stop();
+}
+
+bool NodeLogObserver::onRecord(mbgl::EventSeverity severity, mbgl::Event event, int64_t code, const std::string &text) {
+ queue->send({ severity, event, code, text });
+ return true;
+}
+
+}
diff --git a/platform/node/src/node_log.hpp b/platform/node/src/node_log.hpp
new file mode 100644
index 0000000000..5c0048d261
--- /dev/null
+++ b/platform/node/src/node_log.hpp
@@ -0,0 +1,32 @@
+#pragma once
+
+#include <mbgl/platform/log.hpp>
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wshadow"
+#include <node.h>
+#include <nan.h>
+#pragma GCC diagnostic pop
+
+namespace node_mbgl {
+
+namespace util { template <typename T> class AsyncQueue; }
+
+class NodeLogObserver : public mbgl::Log::Observer {
+public:
+ NodeLogObserver(v8::Handle<v8::Object> target);
+ ~NodeLogObserver();
+
+ // Log::Observer implementation
+ virtual bool onRecord(mbgl::EventSeverity severity, mbgl::Event event, int64_t code, const std::string &msg) override;
+
+private:
+ v8::Persistent<v8::Object> module;
+
+ struct LogMessage;
+ using Queue = util::AsyncQueue<LogMessage>;
+ Queue *queue = nullptr;
+};
+
+}
diff --git a/platform/node/src/node_map.cpp b/platform/node/src/node_map.cpp
new file mode 100644
index 0000000000..a24f0c46b8
--- /dev/null
+++ b/platform/node/src/node_map.cpp
@@ -0,0 +1,349 @@
+#include "node_map.hpp"
+
+#include <mbgl/platform/default/headless_display.hpp>
+#include <mbgl/map/still_image.hpp>
+#include <mbgl/util/exception.hpp>
+
+#include <unistd.h>
+
+namespace node_mbgl {
+
+struct NodeMap::RenderOptions {
+ double zoom = 0;
+ double bearing = 0;
+ double latitude = 0;
+ double longitude = 0;
+ unsigned int width = 512;
+ unsigned int height = 512;
+ float ratio = 1.0f;
+ std::vector<std::string> classes;
+};
+
+////////////////////////////////////////////////////////////////////////////////////////////////
+// Static Node Methods
+
+v8::Persistent<v8::FunctionTemplate> NodeMap::constructorTemplate;
+
+static std::shared_ptr<mbgl::HeadlessDisplay> sharedDisplay() {
+ static auto display = std::make_shared<mbgl::HeadlessDisplay>();
+ return display;
+}
+
+const static char* releasedMessage() {
+ return "Map resources have already been released";
+}
+
+void NodeMap::Init(v8::Handle<v8::Object> target) {
+ NanScope();
+
+ v8::Local<v8::FunctionTemplate> t = NanNew<v8::FunctionTemplate>(New);
+
+ t->InstanceTemplate()->SetInternalFieldCount(1);
+ t->SetClassName(NanNew("Map"));
+
+ NODE_SET_PROTOTYPE_METHOD(t, "load", Load);
+ NODE_SET_PROTOTYPE_METHOD(t, "render", Render);
+ NODE_SET_PROTOTYPE_METHOD(t, "release", Release);
+
+ NanAssignPersistent(constructorTemplate, t);
+
+ target->Set(NanNew("Map"), t->GetFunction());
+
+ // Initialize display connection on module load.
+ sharedDisplay();
+}
+
+NAN_METHOD(NodeMap::New) {
+ NanScope();
+
+ if (!args.IsConstructCall()) {
+ return NanThrowTypeError("Use the new operator to create new Map objects");
+ }
+
+ if (args.Length() < 1 || !args[0]->IsObject()) {
+ return NanThrowTypeError("Requires an options object as first argument");
+ }
+
+ auto options = args[0]->ToObject();
+
+ // Check that 'request', 'cancel' and 'ratio' are defined.
+ if (!options->Has(NanNew("request")) || !options->Get(NanNew("request"))->IsFunction()) {
+ return NanThrowError("Options object must have a 'request' method");
+ }
+ if (options->Has(NanNew("cancel")) && !options->Get(NanNew("cancel"))->IsFunction()) {
+ return NanThrowError("Options object 'cancel' property must be a function");
+ }
+
+ if (!options->Has(NanNew("ratio")) || !options->Get(NanNew("ratio"))->IsNumber()) {
+ return NanThrowError("Options object must have a numerical 'ratio' property");
+ }
+
+ try {
+ auto nodeMap = new NodeMap(options);
+ nodeMap->Wrap(args.This());
+ } catch(std::exception &ex) {
+ return NanThrowError(ex.what());
+ }
+
+ NanReturnValue(args.This());
+}
+
+const std::string StringifyStyle(v8::Handle<v8::Value> styleHandle) {
+ NanScope();
+
+ auto JSON = NanGetCurrentContext()->Global()->Get(NanNew("JSON"))->ToObject();
+ auto stringify = v8::Handle<v8::Function>::Cast(JSON->Get(NanNew("stringify")));
+
+ return *NanUtf8String(stringify->Call(JSON, 1, &styleHandle));
+}
+
+NAN_METHOD(NodeMap::Load) {
+ NanScope();
+
+ auto nodeMap = node::ObjectWrap::Unwrap<NodeMap>(args.Holder());
+
+ if (!nodeMap->isValid()) return NanThrowError(releasedMessage());
+
+ // Reset the flag as this could be the second time
+ // we are calling this (being the previous successful).
+ nodeMap->loaded = false;
+
+ if (args.Length() < 1) {
+ return NanThrowError("Requires a map style as first argument");
+ }
+
+ std::string style;
+
+ if (args[0]->IsObject()) {
+ style = StringifyStyle(args[0]);
+ } else if (args[0]->IsString()) {
+ style = *NanUtf8String(args[0]);
+ } else {
+ return NanThrowTypeError("First argument must be a string or object");
+ }
+
+ try {
+ nodeMap->map->setStyleJSON(style, ".");
+ } catch (const std::exception &ex) {
+ return NanThrowError(ex.what());
+ }
+
+ nodeMap->loaded = true;
+
+ NanReturnUndefined();
+}
+
+std::unique_ptr<NodeMap::RenderOptions> NodeMap::ParseOptions(v8::Local<v8::Object> obj) {
+ NanScope();
+
+ auto options = std::make_unique<RenderOptions>();
+
+ if (obj->Has(NanNew("zoom"))) { options->zoom = obj->Get(NanNew("zoom"))->NumberValue(); }
+ if (obj->Has(NanNew("bearing"))) { options->bearing = obj->Get(NanNew("bearing"))->NumberValue(); }
+ if (obj->Has(NanNew("center"))) {
+ auto center = obj->Get(NanNew("center")).As<v8::Array>();
+ if (center->Length() > 0) { options->latitude = center->Get(0)->NumberValue(); }
+ if (center->Length() > 1) { options->longitude = center->Get(1)->NumberValue(); }
+ }
+ if (obj->Has(NanNew("width"))) { options->width = obj->Get(NanNew("width"))->IntegerValue(); }
+ if (obj->Has(NanNew("height"))) { options->height = obj->Get(NanNew("height"))->IntegerValue(); }
+ if (obj->Has(NanNew("ratio"))) { options->ratio = obj->Get(NanNew("ratio"))->IntegerValue(); }
+
+ if (obj->Has(NanNew("classes"))) {
+ auto classes = obj->Get(NanNew("classes"))->ToObject().As<v8::Array>();
+ const int length = classes->Length();
+ options->classes.reserve(length);
+ for (int i = 0; i < length; i++) {
+ options->classes.push_back(std::string { *NanUtf8String(classes->Get(i)->ToString()) });
+ }
+ }
+
+ return options;
+}
+
+NAN_METHOD(NodeMap::Render) {
+ NanScope();
+
+ auto nodeMap = node::ObjectWrap::Unwrap<NodeMap>(args.Holder());
+
+ if (!nodeMap->isValid()) return NanThrowError(releasedMessage());
+
+ if (args.Length() <= 0 || !args[0]->IsObject()) {
+ return NanThrowTypeError("First argument must be an options object");
+ }
+
+ if (args.Length() <= 1 || !args[1]->IsFunction()) {
+ return NanThrowTypeError("Second argument must be a callback function");
+ }
+
+ if (!nodeMap->isLoaded()) {
+ return NanThrowTypeError("Style is not loaded");
+ }
+
+ auto options = ParseOptions(args[0]->ToObject());
+
+ assert(!nodeMap->callback);
+ assert(!nodeMap->image);
+ nodeMap->callback = std::unique_ptr<NanCallback>(new NanCallback(args[1].As<v8::Function>()));
+
+ try {
+ nodeMap->startRender(std::move(options));
+ } catch (mbgl::util::Exception &ex) {
+ return NanThrowError(ex.what());
+ }
+
+ NanReturnUndefined();
+}
+
+void NodeMap::startRender(std::unique_ptr<NodeMap::RenderOptions> options) {
+ view.resize(options->width, options->height);
+ map->update(mbgl::Update::Dimensions);
+ map->setClasses(options->classes);
+ map->setLatLngZoom(mbgl::LatLng(options->latitude, options->longitude), options->zoom);
+ map->setBearing(options->bearing);
+
+ map->renderStill([this](const std::exception_ptr eptr, std::unique_ptr<const mbgl::StillImage> result) {
+ if (eptr) {
+ error = std::move(eptr);
+ uv_async_send(async);
+ } else {
+ assert(!image);
+ image = std::move(result);
+ uv_async_send(async);
+ }
+ });
+
+ // Retain this object, otherwise it might get destructed before we are finished rendering the
+ // still image.
+ Ref();
+
+ // Similarly, we're now waiting for the async to be called, so we need to make sure that it
+ // keeps the loop alive.
+ uv_ref(reinterpret_cast<uv_handle_t *>(async));
+}
+
+void NodeMap::renderFinished() {
+ NanScope();
+
+ // We're done with this render call, so we're unrefing so that the loop could close.
+ uv_unref(reinterpret_cast<uv_handle_t *>(async));
+
+ // There is no render pending anymore, we the GC could now delete this object if it went out
+ // of scope.
+ Unref();
+
+ // Move the callback and image out of the way so that the callback can start a new render call.
+ auto cb = std::move(callback);
+ auto img = std::move(image);
+ assert(cb);
+
+ // These have to be empty to be prepared for the next render call.
+ assert(!callback);
+ assert(!image);
+
+ if (error) {
+ std::string errorMessage;
+
+ try {
+ std::rethrow_exception(error);
+ } catch (const std::exception& ex) {
+ errorMessage = ex.what();
+ }
+
+ v8::Local<v8::Value> argv[] = {
+ NanError(errorMessage.c_str())
+ };
+
+ // This must be empty to be prepared for the next render call.
+ error = nullptr;
+ assert(!error);
+
+ cb->Call(1, argv);
+ } else if (img) {
+ auto result = NanNew<v8::Object>();
+ result->Set(NanNew("width"), NanNew(img->width));
+ result->Set(NanNew("height"), NanNew(img->height));
+
+ v8::Local<v8::Object> pixels = NanNewBufferHandle(
+ reinterpret_cast<char *>(img->pixels.get()),
+ size_t(img->width) * size_t(img->height) * sizeof(mbgl::StillImage::Pixel),
+
+ // Retain the StillImage object until the buffer is deleted.
+ [](char *, void *hint) {
+ delete reinterpret_cast<const mbgl::StillImage *>(hint);
+ },
+ const_cast<mbgl::StillImage *>(img.get())
+ );
+ img.release();
+
+ result->Set(NanNew("pixels"), pixels);
+
+ v8::Local<v8::Value> argv[] = {
+ NanNull(),
+ result,
+ };
+ cb->Call(2, argv);
+ } else {
+ v8::Local<v8::Value> argv[] = {
+ NanError("Didn't get an image")
+ };
+ cb->Call(1, argv);
+ }
+}
+
+NAN_METHOD(NodeMap::Release) {
+ NanScope();
+
+ auto nodeMap = node::ObjectWrap::Unwrap<NodeMap>(args.Holder());
+
+ if (!nodeMap->isValid()) return NanThrowError(releasedMessage());
+
+ try {
+ nodeMap->release();
+ } catch (const std::exception &ex) {
+ return NanThrowError(ex.what());
+ }
+
+ NanReturnUndefined();
+}
+
+void NodeMap::release() {
+ if (!isValid()) throw mbgl::util::Exception(releasedMessage());
+
+ valid = false;
+
+ uv_close(reinterpret_cast<uv_handle_t *>(async), [] (uv_handle_t *handle) {
+ delete reinterpret_cast<uv_async_t *>(handle);
+ });
+
+ map.reset(nullptr);
+}
+
+
+////////////////////////////////////////////////////////////////////////////////////////////////
+// Instance
+
+NodeMap::NodeMap(v8::Handle<v8::Object> options) :
+ view(sharedDisplay(), options->Get(NanNew("ratio"))->NumberValue()),
+ fs(options),
+ map(std::make_unique<mbgl::Map>(view, fs, mbgl::MapMode::Still)),
+ async(new uv_async_t) {
+
+ async->data = this;
+#if UV_VERSION_MAJOR == 0 && UV_VERSION_MINOR <= 10
+ uv_async_init(uv_default_loop(), async, [](uv_async_t *as, int) {
+#else
+ uv_async_init(uv_default_loop(), async, [](uv_async_t *as) {
+#endif
+ reinterpret_cast<NodeMap *>(as->data)->renderFinished();
+ });
+
+ // Make sure the async handle doesn't keep the loop alive.
+ uv_unref(reinterpret_cast<uv_handle_t *>(async));
+}
+
+NodeMap::~NodeMap() {
+ if (valid) release();
+}
+
+}
diff --git a/platform/node/src/node_map.hpp b/platform/node/src/node_map.hpp
new file mode 100644
index 0000000000..2c87900d4d
--- /dev/null
+++ b/platform/node/src/node_map.hpp
@@ -0,0 +1,66 @@
+#pragma once
+
+#include "node_file_source.hpp"
+
+#include <mbgl/map/map.hpp>
+#include <mbgl/platform/default/headless_view.hpp>
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wshadow"
+#include <node.h>
+#include <nan.h>
+#pragma GCC diagnostic pop
+
+#include <queue>
+
+namespace node_mbgl {
+
+class NodeMap : public node::ObjectWrap {
+ struct RenderOptions;
+ class RenderWorker;
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ // Static Node Methods
+public:
+ static void Init(v8::Handle<v8::Object> target);
+ static NAN_METHOD(New);
+ static NAN_METHOD(Load);
+ static NAN_METHOD(Render);
+ static NAN_METHOD(Release);
+
+ void startRender(std::unique_ptr<NodeMap::RenderOptions> options);
+ void renderFinished();
+
+ void release();
+
+ inline bool isLoaded() { return loaded; }
+ inline bool isValid() { return valid; }
+
+ static std::unique_ptr<NodeMap::RenderOptions> ParseOptions(v8::Local<v8::Object> obj);
+
+ static v8::Persistent<v8::FunctionTemplate> constructorTemplate;
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ // Instance
+private:
+ NodeMap(v8::Handle<v8::Object>);
+ ~NodeMap();
+
+private:
+ mbgl::HeadlessView view;
+ NodeFileSource fs;
+ std::unique_ptr<mbgl::Map> map;
+
+ std::exception_ptr error;
+ std::unique_ptr<const mbgl::StillImage> image;
+ std::unique_ptr<NanCallback> callback;
+
+ // Async for delivering the notifications of render completion.
+ uv_async_t *async;
+
+ bool loaded = false;
+ bool valid = true;
+};
+
+}
diff --git a/platform/node/src/node_mapbox_gl_native.cpp b/platform/node/src/node_mapbox_gl_native.cpp
new file mode 100644
index 0000000000..9ba0abbd05
--- /dev/null
+++ b/platform/node/src/node_mapbox_gl_native.cpp
@@ -0,0 +1,36 @@
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wshadow"
+#include <node.h>
+#include <nan.h>
+#pragma GCC diagnostic pop
+
+#include "node_map.hpp"
+#include "node_log.hpp"
+#include "node_request.hpp"
+
+void RegisterModule(v8::Handle<v8::Object> exports) {
+ NanScope();
+
+ node_mbgl::NodeMap::Init(exports);
+ node_mbgl::NodeRequest::Init(exports);
+
+ // Exports Resource constants.
+ auto ConstantProperty = static_cast<v8::PropertyAttribute>(v8::ReadOnly | v8::DontDelete);
+ auto resource = NanNew<v8::Object>();
+ resource->ForceSet(NanNew("Unknown"), NanNew(mbgl::Resource::Unknown), ConstantProperty);
+ resource->ForceSet(NanNew("Tile"), NanNew(mbgl::Resource::Tile), ConstantProperty);
+ resource->ForceSet(NanNew("Glyphs"), NanNew(mbgl::Resource::Glyphs), ConstantProperty);
+ resource->ForceSet(NanNew("Image"), NanNew(mbgl::Resource::Image), ConstantProperty);
+ resource->ForceSet(NanNew("JSON"), NanNew(mbgl::Resource::JSON), ConstantProperty);
+ exports->ForceSet(NanNew("Resource"), resource, ConstantProperty);
+
+ // Make the exported object inerhit from process.EventEmitter
+ auto process = NanGetCurrentContext()->Global()->Get(NanNew("process"))->ToObject();
+ auto EventEmitter = process->Get(NanNew("EventEmitter"))->ToObject();
+ exports->SetPrototype(EventEmitter->Get(NanNew("prototype")));
+
+ mbgl::Log::setObserver(std::make_unique<node_mbgl::NodeLogObserver>(exports));
+}
+
+NODE_MODULE(mapbox_gl_native, RegisterModule)
diff --git a/platform/node/src/node_request.cpp b/platform/node/src/node_request.cpp
new file mode 100644
index 0000000000..d2cf43790b
--- /dev/null
+++ b/platform/node/src/node_request.cpp
@@ -0,0 +1,142 @@
+#include "node_request.hpp"
+#include "node_file_source.hpp"
+#include <mbgl/storage/request.hpp>
+#include <mbgl/storage/response.hpp>
+
+#include <cmath>
+
+namespace node_mbgl {
+
+////////////////////////////////////////////////////////////////////////////////////////////////
+// Static Node Methods
+
+v8::Persistent<v8::FunctionTemplate> NodeRequest::constructorTemplate;
+
+void NodeRequest::Init(v8::Handle<v8::Object> target) {
+ NanScope();
+
+ v8::Local<v8::FunctionTemplate> t = NanNew<v8::FunctionTemplate>(New);
+
+ t->InstanceTemplate()->SetInternalFieldCount(1);
+ t->SetClassName(NanNew("Request"));
+
+ NODE_SET_PROTOTYPE_METHOD(t, "respond", Respond);
+
+ NanAssignPersistent(constructorTemplate, t);
+
+ target->Set(NanNew("Request"), t->GetFunction());
+}
+
+NAN_METHOD(NodeRequest::New) {
+ NanScope();
+
+ // Extract the pointer from the first argument
+ if (args.Length() < 2 || !args[0]->IsExternal() || !args[1]->IsExternal()) {
+ return NanThrowTypeError("Cannot create Request objects explicitly");
+ }
+
+ auto source = reinterpret_cast<NodeFileSource*>(args[0].As<v8::External>()->Value());
+ auto resource = reinterpret_cast<mbgl::Resource*>(args[1].As<v8::External>()->Value());
+ auto req = new NodeRequest(source, *resource);
+ req->Wrap(args.This());
+
+ NanReturnValue(args.This());
+}
+
+v8::Handle<v8::Object> NodeRequest::Create(NodeFileSource* source, const mbgl::Resource& resource) {
+ NanEscapableScope();
+
+ v8::Local<v8::Value> argv[] = { NanNew<v8::External>(const_cast<NodeFileSource*>(source)),
+ NanNew<v8::External>(const_cast<mbgl::Resource*>(&resource)) };
+ auto instance = NanNew<v8::FunctionTemplate>(constructorTemplate)->GetFunction()->NewInstance(2, argv);
+
+ instance->ForceSet(NanNew("url"), NanNew(resource.url), v8::ReadOnly);
+ instance->ForceSet(NanNew("kind"), NanNew<v8::Integer>(int(resource.kind)), v8::ReadOnly);
+
+ return NanEscapeScope(instance);
+}
+
+NAN_METHOD(NodeRequest::Respond) {
+ auto nodeRequest = ObjectWrap::Unwrap<NodeRequest>(args.Holder());
+
+ // Request has already been responded to, or was canceled, fail silently.
+ if (!nodeRequest->resource) NanReturnUndefined();
+
+ auto source = nodeRequest->source;
+ auto resource = std::move(nodeRequest->resource);
+
+ if (args.Length() < 1) {
+ return NanThrowTypeError("First argument must be an error object");
+ } else if (args[0]->BooleanValue()) {
+ auto response = std::make_shared<mbgl::Response>();
+
+ response->status = mbgl::Response::Error;
+
+ // Store the error string.
+ const NanUtf8String message { args[0]->ToString() };
+ response->message = std::string { *message, size_t(message.length()) };
+
+ source->notify(*resource, response);
+ } else if (args.Length() < 2 || !args[1]->IsObject()) {
+ return NanThrowTypeError("Second argument must be a response object");
+ } else {
+ auto response = std::make_shared<mbgl::Response>();
+ auto res = args[1]->ToObject();
+
+ response->status = mbgl::Response::Successful;
+
+ if (res->Has(NanNew("modified"))) {
+ const double modified = res->Get(NanNew("modified"))->ToNumber()->Value();
+ if (!std::isnan(modified)) {
+ response->modified = modified / 1000; // JS timestamps are milliseconds
+ }
+ }
+
+ if (res->Has(NanNew("expires"))) {
+ const double expires = res->Get(NanNew("expires"))->ToNumber()->Value();
+ if (!std::isnan(expires)) {
+ response->expires = expires / 1000; // JS timestamps are milliseconds
+ }
+ }
+
+ if (res->Has(NanNew("etag"))) {
+ auto etagHandle = res->Get(NanNew("etag"));
+ if (etagHandle->BooleanValue()) {
+ const NanUtf8String etag { etagHandle->ToString() };
+ response->etag = std::string { *etag, size_t(etag.length()) };
+ }
+ }
+
+ if (res->Has(NanNew("data"))) {
+ auto dataHandle = res->Get(NanNew("data"));
+ if (node::Buffer::HasInstance(dataHandle)) {
+ response->data = std::string {
+ node::Buffer::Data(dataHandle),
+ node::Buffer::Length(dataHandle)
+ };
+ } else {
+ return NanThrowTypeError("Response data must be a Buffer");
+ }
+ }
+
+ // Send the response object to the NodeFileSource object
+ source->notify(*resource, response);
+ }
+
+ NanReturnUndefined();
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////
+// Instance
+
+NodeRequest::NodeRequest(NodeFileSource* source_, const mbgl::Resource& resource_)
+ : source(source_),
+ resource(std::make_unique<mbgl::Resource>(resource_)) {}
+
+NodeRequest::~NodeRequest() {}
+
+void NodeRequest::cancel() {
+ resource.reset();
+}
+
+}
diff --git a/platform/node/src/node_request.hpp b/platform/node/src/node_request.hpp
new file mode 100644
index 0000000000..a690904ef2
--- /dev/null
+++ b/platform/node/src/node_request.hpp
@@ -0,0 +1,43 @@
+#pragma once
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#pragma GCC diagnostic ignored "-Wshadow"
+#include <node.h>
+#include <nan.h>
+#pragma GCC diagnostic pop
+
+#include <mbgl/storage/resource.hpp>
+
+#include <memory>
+
+namespace node_mbgl {
+
+class NodeFileSource;
+
+class NodeRequest : public node::ObjectWrap {
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ // Static Node Methods
+public:
+ static void Init(v8::Handle<v8::Object> target);
+ static NAN_METHOD(New);
+ static NAN_METHOD(Respond);
+
+ static v8::Handle<v8::Object> Create(NodeFileSource*, const mbgl::Resource&);
+
+ static v8::Persistent<v8::FunctionTemplate> constructorTemplate;
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ // Instance
+public:
+ NodeRequest(NodeFileSource* source, const mbgl::Resource& resource);
+ ~NodeRequest();
+
+ void cancel();
+
+private:
+ NodeFileSource* source;
+ std::unique_ptr<mbgl::Resource> resource;
+};
+
+}
diff --git a/platform/node/src/util/async_queue.hpp b/platform/node/src/util/async_queue.hpp
new file mode 100644
index 0000000000..81319da8c7
--- /dev/null
+++ b/platform/node/src/util/async_queue.hpp
@@ -0,0 +1,90 @@
+#pragma once
+
+#include <uv.h>
+
+#include <thread>
+#include <mutex>
+#include <functional>
+#include <queue>
+#include <string>
+
+
+#if UV_VERSION_MAJOR == 0 && UV_VERSION_MINOR <= 10
+#define UV_ASYNC_PARAMS(handle) uv_async_t *handle, int
+#else
+#define UV_ASYNC_PARAMS(handle) uv_async_t *handle
+#endif
+
+namespace node_mbgl {
+namespace util {
+
+template <typename T>
+class AsyncQueue {
+public:
+ AsyncQueue(uv_loop_t *loop, std::function<void(T &)> fn) :
+ callback(fn) {
+ async.data = this;
+ uv_async_init(loop, &async, [](UV_ASYNC_PARAMS(handle)) {
+ auto q = reinterpret_cast<AsyncQueue *>(handle->data);
+ q->process();
+ });
+ }
+
+ void send(T &&data) {
+ {
+ std::lock_guard<std::mutex> lock(mutex);
+ queue.push(std::make_unique<T>(std::move(data)));
+ }
+ uv_async_send(&async);
+ }
+
+ void send(std::unique_ptr<T> data) {
+ {
+ std::lock_guard<std::mutex> lock(mutex);
+ queue.push(std::move(data));
+ }
+ uv_async_send(&async);
+ }
+
+ void stop() {
+ uv_close((uv_handle_t *)&async, [](uv_handle_t *handle) {
+ delete reinterpret_cast<AsyncQueue *>(handle->data);
+ });
+ }
+
+ void ref() {
+ uv_ref((uv_handle_t *)&async);
+ }
+
+ void unref() {
+ uv_unref((uv_handle_t *)&async);
+ }
+
+private:
+ ~AsyncQueue() {
+ }
+
+ void process() {
+ std::unique_ptr<T> item;
+ while (true) {
+ mutex.lock();
+ if (queue.empty()) {
+ mutex.unlock();
+ break;
+ }
+ item = std::move(queue.front());
+ queue.pop();
+ mutex.unlock();
+ callback(*item);
+ }
+ }
+
+private:
+ std::mutex mutex;
+ uv_async_t async;
+ std::queue<std::unique_ptr<T>> queue;
+ std::function<void(T &)> callback;
+};
+
+}
+}
diff --git a/platform/node/test/compare.js b/platform/node/test/compare.js
new file mode 100644
index 0000000000..3e5221de55
--- /dev/null
+++ b/platform/node/test/compare.js
@@ -0,0 +1,27 @@
+var spawn = require('child_process').spawn;
+
+module.exports = function compare(actual, expected, diff, t, callback) {
+ var compare = spawn('compare', ['-metric', 'MAE', actual, expected, diff]);
+ var error = '';
+
+ compare.stderr.on('data', function(data) {
+ error += data.toString();
+ });
+
+ compare.on('error', function(err) {
+ t.error(err);
+ });
+
+ compare.on('exit', function(code) {
+ // The compare program returns 2 on error otherwise 0 if the images are similar or 1 if they are dissimilar.
+ if (code === 2) {
+ callback(error.trim(), Infinity);
+ } else {
+ var match = error.match(/^\d+(?:\.\d+)?\s+\(([^\)]+)\)\s*$/);
+ var difference = match ? parseFloat(match[1]) : Infinity;
+ callback(match ? '' : error, difference);
+ }
+ });
+
+ compare.stdin.end();
+};
diff --git a/platform/node/test/expected/gzip/success.png b/platform/node/test/expected/gzip/success.png
new file mode 100644
index 0000000000..de41e0fe2b
--- /dev/null
+++ b/platform/node/test/expected/gzip/success.png
Binary files differ
diff --git a/platform/node/test/expected/gzip/unhandled.png b/platform/node/test/expected/gzip/unhandled.png
new file mode 100644
index 0000000000..ddb90d8b8f
--- /dev/null
+++ b/platform/node/test/expected/gzip/unhandled.png
Binary files differ
diff --git a/platform/node/test/expected/map/image.png b/platform/node/test/expected/map/image.png
new file mode 100644
index 0000000000..de41e0fe2b
--- /dev/null
+++ b/platform/node/test/expected/map/image.png
Binary files differ
diff --git a/platform/node/test/fixtures/style.json b/platform/node/test/fixtures/style.json
new file mode 100644
index 0000000000..47a1990eb7
--- /dev/null
+++ b/platform/node/test/fixtures/style.json
@@ -0,0 +1,26 @@
+{
+ "version": 7,
+ "name": "Empty",
+ "sources": {
+ "mapbox": {
+ "type": "vector",
+ "url": "./fixtures/tiles.tilejson",
+ "maxzoom": 15
+ }
+ },
+ "layers": [{
+ "id": "background",
+ "type": "background",
+ "paint": {
+ "background-color": "white"
+ }
+ }, {
+ "id": "water",
+ "type": "fill",
+ "source": "mapbox",
+ "source-layer": "water",
+ "paint": {
+ "fill-color": "blue"
+ }
+ }]
+}
diff --git a/platform/node/test/fixtures/tiles.tilejson b/platform/node/test/fixtures/tiles.tilejson
new file mode 100644
index 0000000000..320c17500e
--- /dev/null
+++ b/platform/node/test/fixtures/tiles.tilejson
@@ -0,0 +1,13 @@
+{
+ "bounds": [ -180, -85.0511, 180, 85.0511 ],
+ "center": [ 0, 0, 0 ],
+ "format": "pbf",
+ "id": "mapbox.mapbox-streets-v6-dev",
+ "maskLevel": 8,
+ "maxzoom": 15,
+ "minzoom": 0,
+ "name": "Mapbox Streets V6",
+ "scheme": "xyz",
+ "tilejson": "2.0.0",
+ "tiles": [ "./fixtures/tiles/{z}-{x}-{y}.vector.pbf" ]
+}
diff --git a/platform/node/test/fixtures/tiles/0-0-0.vector.pbf b/platform/node/test/fixtures/tiles/0-0-0.vector.pbf
new file mode 100644
index 0000000000..87628e06cf
--- /dev/null
+++ b/platform/node/test/fixtures/tiles/0-0-0.vector.pbf
Binary files differ
diff --git a/platform/node/test/js/consecutive.test.js b/platform/node/test/js/consecutive.test.js
new file mode 100644
index 0000000000..f493e2a31c
--- /dev/null
+++ b/platform/node/test/js/consecutive.test.js
@@ -0,0 +1,83 @@
+'use strict';
+
+/* jshint node:true */
+
+var test = require('tape');
+var mbgl = require('../..');
+var fs = require('fs');
+var path = require('path');
+
+var suitePath = path.dirname(require.resolve('mapbox-gl-test-suite/package.json'));
+
+
+function renderTest(style, info, dir, key) {
+ return function (t) {
+ var completed = 0;
+ var remaining = 10;
+ var start = +new Date;
+
+ var options = {};
+ options.request = function(req) {
+ fs.readFile(path.join(suitePath, decodeURIComponent(req.url)), function(err, data) {
+ req.respond(err, { data: data });
+ t.error(err);
+ });
+ };
+ options.cancel = function() {};
+ options.ratio = 1.0;
+
+ var map = new mbgl.Map(options);
+ map.load(style);
+
+ function render() {
+ map.render(info[key], function(err, image) {
+ t.error(err);
+
+ t.ok(true, 'render @ ' + ((+new Date) - start) + 'ms');
+ if (++completed === remaining) {
+ map.release();
+ t.end();
+ } else {
+ render();
+ }
+ });
+ }
+
+ render();
+ };
+}
+
+function rewriteLocalSchema(uri) {
+ return uri.replace(/^local:\/\//, '');
+}
+
+test('Consecutive', function(t) {
+ var dir = 'line-join';
+ var k = 'round';
+
+ var style = require(path.join(suitePath, 'tests', dir, 'style.json')),
+ info = require(path.join(suitePath, 'tests', dir, 'info.json'));
+
+ for (var k in style.sources) {
+ var source = style.sources[k];
+
+ if (source.tiles) {
+ source.tiles = source.tiles.map(rewriteLocalSchema);
+ }
+
+ if (source.url) {
+ source.url = rewriteLocalSchema(source.url);
+ }
+ }
+
+ if (style.sprite) style.sprite = rewriteLocalSchema(style.sprite);
+ if (style.glyphs) style.glyphs = rewriteLocalSchema(style.glyphs);
+
+ style = JSON.stringify(style);
+
+ for (k in info) {
+ t.test(dir + ' ' + k, renderTest(style, info, dir, k));
+ }
+
+ t.end();
+});
diff --git a/platform/node/test/js/gzip.test.js b/platform/node/test/js/gzip.test.js
new file mode 100644
index 0000000000..9e899604ae
--- /dev/null
+++ b/platform/node/test/js/gzip.test.js
@@ -0,0 +1,126 @@
+'use strict';
+
+/* jshint node: true */
+
+var test = require('tape').test;
+var mbgl = require('../..');
+var fs = require('fs');
+var path = require('path');
+var mkdirp = require('mkdirp');
+var http = require('http');
+var request = require('request');
+var st = require('st');
+var style = require('../../test/fixtures/style.json');
+var PNG = require('pngjs').PNG;
+var compare = require('../compare.js');
+
+var dirPath = path.join(path.dirname(require.resolve('../../package.json')), 'test');
+var server = http.createServer(st({ path: dirPath }));
+server.listen(0);
+
+function filePath(name) {
+ return ['expected', 'actual', 'diff'].reduce(function(prev, key) {
+ var dir = path.join('test', key, 'gzip');
+ mkdirp.sync(dir);
+ prev[key] = path.join(dir, name);
+ return prev;
+ }, {});
+}
+
+function setup(options, callback) {
+ callback(new mbgl.Map(options));
+}
+
+function getOptions(gzip, t) {
+ return {
+ request: function(req) {
+ var parts = req.url.split('.');
+ var filetype = parts[parts.length - 1];
+
+ request({
+ url: 'http://localhost:' + server.address().port + path.join('/', req.url),
+ encoding: null,
+ gzip: filetype === 'pbf' ? gzip : true,
+ headers: {
+ 'Accept-Encoding': 'gzip'
+ }
+ }, function (err, res, body) {
+ t.error(err);
+ var response = {};
+ response.data = res.body;
+ req.respond(null, response);
+ });
+ },
+ ratio: 1.0
+ };
+}
+
+test('gzip', function(t) {
+ t.test('success', function(t) {
+ mbgl.on('message', function(msg) {
+ if (msg.severity == 'ERROR') t.error(msg);
+ });
+
+ setup(getOptions(true, t), function(map) {
+ map.load(style);
+ map.render({}, function(err, data) {
+ mbgl.removeAllListeners('message');
+ map.release();
+
+ t.error(err);
+
+ var filename = filePath('success.png');
+
+ var png = new PNG({
+ width: data.width,
+ height: data.height
+ });
+
+ png.data = data.pixels;
+
+ if (process.env.UPDATE) {
+ png.pack()
+ .pipe(fs.createWriteStream(filename.expected))
+ .on('finish', t.end);
+ } else {
+ png.pack()
+ .pipe(fs.createWriteStream(filename.actual))
+ .on('finish', function() {
+ compare(filename.actual, filename.expected, filename.diff, t, function(err, diff) {
+ t.error(err);
+ t.ok(diff <= 0.01, 'actual matches expected');
+ t.end();
+ });
+ });
+ }
+ });
+ });
+ });
+
+ t.test('unhandled', function(t) {
+ mbgl.once('message', function(msg) {
+ if (msg.severity == 'ERROR') {
+ t.ok(msg, 'emits error');
+ t.equal(msg.class, 'Style');
+ t.equal(msg.severity, 'ERROR');
+ t.ok(msg.text.match(/pbf unknown field type exception/), 'error text matches');
+ }
+ });
+
+ setup(getOptions(false, t), function(map) {
+ map.load(style);
+ map.render({}, function(err, data) {
+ map.release();
+
+ t.ok(err, 'returns error');
+ t.ok(err.message.match(/Failed to parse/), 'error text matches');
+
+ t.end();
+ });
+ });
+ });
+
+ t.test('teardown', function(t) {
+ server.close(t.end);
+ });
+});
diff --git a/platform/node/test/js/map.test.js b/platform/node/test/js/map.test.js
new file mode 100644
index 0000000000..4586ceeed4
--- /dev/null
+++ b/platform/node/test/js/map.test.js
@@ -0,0 +1,293 @@
+'use strict';
+
+/* jshint node: true */
+
+var test = require('tape');
+var mbgl = require('../..');
+var fs = require('fs');
+var path = require('path');
+var mkdirp = require('mkdirp');
+var style = require('../fixtures/style.json');
+var PNG = require('pngjs').PNG;
+var compare = require('../compare.js');
+
+function filePath(name) {
+ return ['expected', 'actual', 'diff'].reduce(function(prev, key) {
+ var dir = path.join('test', key, 'map');
+ mkdirp.sync(dir);
+ prev[key] = path.join(dir, name);
+ return prev;
+ }, {});
+}
+
+function setup(options, callback) {
+ callback(new mbgl.Map(options));
+}
+
+test('Map', function(t) {
+ t.test('constructor', function(t) {
+ t.test('must be called with new', function(t) {
+ t.throws(function() {
+ mbgl.Map();
+ }, /Use the new operator to create new Map objects/);
+
+ t.end();
+ });
+
+ t.test('should require an options object as first parameter', function(t) {
+ t.throws(function() {
+ new mbgl.Map();
+ }, /Requires an options object as first argument/);
+
+ t.throws(function() {
+ new mbgl.Map('options');
+ }, /Requires an options object as first argument/);
+
+ t.end();
+ });
+
+ t.test('should require then options object to have request and cancel methods', function(t) {
+ var options = {};
+
+ t.throws(function() {
+ new mbgl.Map(options);
+ }, /Options object must have a 'request' method/);
+
+ options.request = 'test';
+ t.throws(function() {
+ new mbgl.Map(options);
+ }, /Options object must have a 'request' method/);
+
+ options.request = function() {};
+ options.cancel = 'test';
+ t.throws(function() {
+ new mbgl.Map(options);
+ }, /Options object 'cancel' property must be a function/);
+
+ options.cancel = function() {};
+ t.throws(function() {
+ new mbgl.Map(options);
+ }, /Options object must have a numerical 'ratio' property/);
+
+ options.ratio = 'test';
+ t.throws(function() {
+ new mbgl.Map(options);
+ }, /Options object must have a numerical 'ratio' property/);
+
+ options.ratio = 1.0;
+ t.doesNotThrow(function() {
+ new mbgl.Map(options);
+ });
+
+ t.end();
+ });
+
+ t.end();
+ });
+
+ t.test('load styles', function(t) {
+ var options = {};
+ options.request = function() {};
+ options.cancel = function() {};
+ options.ratio = 1.0;
+
+ t.test('requires a string or object as the first parameter', function(t) {
+ t.test('requires a map style as first argument', function(t) {
+ setup(options, function(map) {
+ t.throws(function() {
+ map.load();
+ }, /Requires a map style as first argument/);
+
+ map.release();
+
+ t.end();
+ });
+ });
+
+ t.test('expect either an object or array at root', { timeout: 1000 }, function(t) {
+ setup(options, function(map) {
+ mbgl.once('message', function(msg) {
+ t.equal(msg.severity, 'ERROR');
+ t.equal(msg.class, 'ParseStyle');
+ t.ok(msg.text.match(/Expect either an object or array at root/));
+
+ map.release();
+
+ t.end();
+ });
+
+ map.load('invalid');
+ });
+ });
+
+ t.end();
+ });
+
+ t.test('accepts an empty stylesheet string', function(t) {
+ setup(options, function(map) {
+ t.doesNotThrow(function() {
+ map.load('{}');
+ });
+
+ map.release();
+
+ t.end();
+ });
+ });
+
+ t.test('accepts a JSON stylesheet', { timeout: 1000 }, function(t) {
+ setup(options, function(map) {
+ t.doesNotThrow(function() {
+ map.load(style);
+ });
+
+ map.release();
+
+ t.end();
+ });
+ });
+
+ t.test('accepts a stringified stylesheet', { timeout: 1000 }, function(t) {
+ setup(options, function(map) {
+ t.doesNotThrow(function() {
+ map.load(JSON.stringify(style));
+ });
+
+ map.release();
+
+ t.end();
+ });
+ });
+
+ t.end();
+ });
+
+ t.test('render argument requirements', function(t) {
+ var options = {};
+ options.request = function(req) {
+ fs.readFile(path.join('test', req.url), function(err, data) {
+ req.respond(err, { data: data });
+ });
+ };
+ options.cancel = function() {};
+ options.ratio = 1.0;
+
+ t.test('requires an object as the first parameter', function(t) {
+ setup(options, function(map) {
+ t.throws(function() {
+ map.render();
+ }, /First argument must be an options object/);
+
+ t.throws(function() {
+ map.render('invalid');
+ }, /First argument must be an options object/);
+
+ map.release();
+
+ t.end();
+ });
+ });
+
+ t.test('requires a callback as the second parameter', function(t) {
+ setup(options, function(map) {
+ t.throws(function() {
+ map.render({});
+ }, /Second argument must be a callback function/);
+
+ t.throws(function() {
+ map.render({}, 'invalid');
+ }, /Second argument must be a callback function/);
+
+ map.release();
+
+ t.end();
+ });
+ });
+
+ t.test('requires a style to be set', function(t) {
+ setup(options, function(map) {
+ t.throws(function() {
+ map.render({}, function() {});
+ }, /Style is not loaded/);
+
+ map.release();
+
+ t.end();
+ });
+ });
+
+ t.test('returns an error', function(t) {
+ mbgl.on('message', function(msg) {
+ t.ok(msg, 'emits error');
+ t.equal(msg.class, 'Style');
+ t.equal(msg.severity, 'ERROR');
+ t.ok(msg.text.match(/Failed to load/), 'error text matches');
+ });
+
+ setup(options, function(map) {
+ map.load(style);
+ map.render({ zoom: 1 }, function(err, data) {
+ mbgl.removeAllListeners('message');
+ map.release();
+
+ t.ok(err, 'returns error');
+ t.ok(err.message.match(/Failed to load/), 'error text matches');
+
+ t.end();
+ });
+ });
+ });
+
+ t.test('double release', function(t) {
+ setup(options, function(map) {
+ map.release();
+
+ t.throws(function() {
+ map.release();
+ }, /Map resources have already been released/);
+
+ t.end();
+ });
+ });
+
+ t.test('returns an image', function(t) {
+ setup(options, function(map) {
+ map.load(style);
+ map.render({}, function(err, data) {
+ t.error(err);
+
+ map.release();
+
+ var filename = filePath('image.png');
+
+ var png = new PNG({
+ width: data.width,
+ height: data.height
+ });
+
+ png.data = data.pixels;
+
+ if (process.env.UPDATE) {
+ png.pack()
+ .pipe(fs.createWriteStream(filename.expected))
+ .on('finish', t.end);
+ } else {
+ png.pack()
+ .pipe(fs.createWriteStream(filename.actual))
+ .on('finish', function() {
+ compare(filename.actual, filename.expected, filename.diff, t, function(err, diff) {
+ t.error(err);
+ t.ok(diff <= 0.01, 'actual matches expected');
+ t.end();
+ });
+ });
+ }
+ });
+ });
+ });
+
+ t.end();
+ });
+
+ t.end();
+});
diff --git a/platform/node/test/render.test.js b/platform/node/test/render.test.js
new file mode 100644
index 0000000000..b55841fef0
--- /dev/null
+++ b/platform/node/test/render.test.js
@@ -0,0 +1,160 @@
+'use strict';
+
+/* jshint node:true */
+
+var test = require('tape');
+var mbgl = require('..');
+var fs = require('fs');
+var path = require('path');
+var mkdirp = require('mkdirp');
+var PNG = require('pngjs').PNG;
+var compare = require('./compare.js');
+var suitePath = path.dirname(require.resolve('mapbox-gl-test-suite/package.json'));
+
+function template(name) {
+ return fs.readFileSync(require.resolve('mapbox-gl-test-suite/templates/' + name + '.html.tmpl')).toString();
+}
+
+var results = '';
+var resultTemplate = template('result');
+
+function format(tmpl, kwargs) {
+ return tmpl.replace(/\{\{|\}\}|\{([^}]+)\}/g, function(match, key) {
+ if (match === '{{') return '{';
+ if (match === '}}') return '}';
+ return kwargs[key];
+ });
+}
+
+function renderTest(style, info, base, key) {
+ var dir = path.join(suitePath, 'tests', base, key);
+ mkdirp.sync(dir);
+
+ return function(t) {
+ var watchdog = setTimeout(function() {
+ t.fail('timed out after 20 seconds');
+ }, 20000);
+
+ t.once('end', function() {
+ clearTimeout(watchdog);
+ });
+
+ var options = {};
+ options.request = function(req) {
+ fs.readFile(path.join(suitePath, decodeURIComponent(req.url)), function(err, data) {
+ req.respond(err, { data: data });
+ t.error(err);
+ });
+ };
+ options.cancel = function() {};
+ options.ratio = 1.0;
+
+ var map = new mbgl.Map(options);
+ map.load(style);
+
+ map.render(info[key], function(err, data) {
+ t.error(err);
+
+ map.release();
+
+ var expected = path.join(dir, 'expected.png');
+ var actual = path.join(dir, 'actual.png');
+ var diff = path.join(dir, 'diff.png');
+
+ var png = new PNG({
+ width: data.width,
+ height: data.height
+ });
+
+ png.data = data.pixels;
+
+ if (process.env.UPDATE) {
+ png.pack()
+ .pipe(fs.createWriteStream(expected))
+ .on('finish', t.end);
+ } else {
+ png.pack()
+ .pipe(fs.createWriteStream(actual))
+ .on('finish', function() {
+ compare(actual, expected, diff, t, function(err, diff) {
+ t.error(err);
+
+ var allowed = ('diff' in info) ? info.diff : 0.001;
+ var color = diff <= allowed ? 'green' : 'red';
+
+ results += format(resultTemplate, {
+ name: base,
+ key: key,
+ color: color,
+ error: err ? '<p>' + err + '</p>' : '',
+ difference: diff,
+ zoom: info.zoom || 0,
+ center: info.center || [0, 0],
+ bearing: info.bearing || 0,
+ width: info.width || 512,
+ height: info.height || 512
+ });
+
+ t.ok(diff <= allowed, 'actual matches expected');
+ t.end();
+ });
+ });
+ }
+ });
+ };
+}
+
+function rewriteLocalSchema(url) {
+ var regex = /^local:\/\//;
+ if (url instanceof Array) {
+ return url.map(function(str) {
+ return str.replace(regex, '');
+ });
+ } else if (typeof url === "string") {
+ return url.replace(regex, '');
+ }
+}
+
+var tests;
+
+if (process.argv[1] === __filename) {
+ tests = process.argv.slice(2);
+}
+
+test('Render', function(t) {
+ fs.readdirSync(path.join(suitePath, 'tests')).forEach(function(dir) {
+ if (dir === 'index.html' || dir[0] === '.') return;
+ if (tests && tests.length && tests.indexOf(dir) < 0) return;
+
+ var style = require(path.join(suitePath, 'tests', dir, 'style.json')),
+ info = require(path.join(suitePath, 'tests', dir, 'info.json'));
+
+ for (var k in style.sources) {
+ var source = style.sources[k];
+
+ if (source.tiles) {
+ source.tiles = source.tiles.map(rewriteLocalSchema);
+ }
+
+ if (source.url) {
+ source.url = rewriteLocalSchema(source.url);
+ }
+ }
+
+ if (style.sprite) style.sprite = rewriteLocalSchema(style.sprite);
+ if (style.glyphs) style.glyphs = rewriteLocalSchema(style.glyphs);
+
+ style = JSON.stringify(style);
+
+ for (k in info) {
+ (info[k].native === false ? t.skip : t.test)(dir + ' ' + k, renderTest(style, info, dir, k));
+ }
+ });
+
+ t.test('results', function(t) {
+ var p = path.join(suitePath, 'tests', 'index.html');
+ fs.writeFileSync(p, format(template('results'), {results: results}));
+ console.warn('Results at: ' + p);
+ t.end();
+ });
+});