diff --git a/platform/node/README.md b/platform/node/README.md index 3be203d84..f41844ba7 100644 --- a/platform/node/README.md +++ b/platform/node/README.md @@ -30,6 +30,38 @@ npm run test-suite ## Rendering a map tile +The minimal example requires only the instantiation of the `mbgl.Map` object, loading a style and calling the `map.render` method: + +```js +var mbgl = require('@maplibre/maplibre-gl-native'); +var sharp = require('sharp'); + +var map = new mbgl.Map(); + +map.load(require('./test/fixtures/style.json')); + +map.render(function(err, buffer) { + if (err) throw err; + + 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; + }); +}); +``` + +But you can customize the map providing an options object to `mbgl.Map` constructor and to `map.render` method: + ```js var fs = require('fs'); var path = require('path'); @@ -87,7 +119,7 @@ When you are finished using a map object, you can call `map.release()` to perman ## Implementing a file source -When creating a `Map`, you must pass an options object (with a required `request` method and optional 'ratio' number) as the first parameter. +When creating a `Map`, you can optionally pass an options object (with an optional `request` method and optional `ratio` number) as the first parameter. 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: ```js var map = new mbgl.Map({ @@ -98,7 +130,7 @@ var map = new mbgl.Map({ }); ``` -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: +If you omit the `request` method, the `map` object will use the default internal request handlers, which is ok for most cases. However, if you have specific needs, you can implement your own `request` handler. When a `request` method is provided, all `map` resources will be requested by calling the `request` method with two parameters, called `req` and `callback` respectively in this example. The `req` parameter has two properties: ```json { @@ -123,7 +155,18 @@ The `kind` is an enum and defined in [`mbgl.Resource`](https://github.com/maplib 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. +The `callback` parameter is a function that must be called with two parameters: an error message (if there are no errors, then you must pass `null`), and a response object: + +```js +{ + data: {data}, // required, must be a byte array, usually a Buffer object + modified: {modified}, // Date, optional + expires: {expires}, // Date, optional + etag: {etag} // string, optional +} +``` + +If there is no data to be sent to the `callback` (empty data, or `no-content` respose), then it must be called without parameters. 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. A sample implementation that reads files from disk would look like the following: diff --git a/platform/node/index.d.ts b/platform/node/index.d.ts index d28a670eb..b8e3bf9a9 100644 --- a/platform/node/index.d.ts +++ b/platform/node/index.d.ts @@ -35,7 +35,7 @@ declare module '@maplibre/maplibre-gl-native' { /** * Will be used during a `Map.render` call to request all necessary map resources (tiles, fonts...) */ - request: ( + request?: ( request: { url: string; kind: ResourceKind }, callback: (error?: Error, response?: RequestResponse) => void, ) => void; @@ -111,7 +111,7 @@ declare module '@maplibre/maplibre-gl-native' { * A `Map` instance is used to render images from map views */ class Map { - constructor(mapOptions: MapOptions); + constructor(mapOptions?: MapOptions); /** * Load a style into a map diff --git a/platform/node/index.js b/platform/node/index.js index 7ac44dd70..a29e0ee86 100644 --- a/platform/node/index.js +++ b/platform/node/index.js @@ -6,43 +6,49 @@ var mbgl = require('../../lib/node-v' + process.versions.modules + '/mbgl'); var constructor = mbgl.Map.prototype.constructor; var Map = function(options) { - if (!(options instanceof Object)) { + if (options && !(options instanceof Object)) { throw TypeError("Requires an options object as first argument"); } - if (!options.hasOwnProperty('request') || !(options.request instanceof Function)) { - throw TypeError("Options object must have a 'request' method"); + if (options && options.hasOwnProperty('request') && !(options.request instanceof Function)) { + throw TypeError("Options object 'request' property must be a function"); } - var request = options.request; + if (options && options.request) { + var request = options.request; - return new constructor(Object.assign(options, { - request: function(req) { - // Protect against `request` implementations that call the callback synchronously, - // call it multiple times, or throw exceptions. - // http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony + return new constructor(Object.assign(options, { + request: function(req) { + // Protect against `request` implementations that call the callback synchronously, + // call it multiple times, or throw exceptions. + // http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony - var responded = false; - var callback = function() { - var args = arguments; - if (!responded) { - responded = true; - process.nextTick(function() { - req.respond.apply(req, args); - }); - } else { - console.warn('request function responded multiple times; it should call the callback only once'); + var responded = false; + var callback = function() { + var args = arguments; + if (!responded) { + responded = true; + process.nextTick(function() { + req.respond.apply(req, args); + }); + } else { + console.warn('request function responded multiple times; it should call the callback only once'); + } + }; + + try { + request(req, callback); + } catch (e) { + console.warn('request function threw an exception; it should call the callback with an error instead'); + callback(e); } - }; - - try { - request(req, callback); - } catch (e) { - console.warn('request function threw an exception; it should call the callback with an error instead'); - callback(e); } - } - })); + })); + } else if (options) { + return new constructor(options); + } else { + return new constructor(); + } }; Map.prototype = mbgl.Map.prototype; diff --git a/platform/node/src/node_map.cpp b/platform/node/src/node_map.cpp index 28a5a7f24..bb5536ef0 100644 --- a/platform/node/src/node_map.cpp +++ b/platform/node/src/node_map.cpp @@ -171,17 +171,23 @@ void NodeMap::New(const Nan::FunctionCallbackInfo& info) { return Nan::ThrowTypeError("Use the new operator to create new Map objects"); } - if (info.Length() < 1 || !info[0]->IsObject()) { + if (info.Length() > 0 && !info[0]->IsObject()) { return Nan::ThrowTypeError("Requires an options object as first argument"); } - auto options = Nan::To(info[0]).ToLocalChecked(); + v8::Local options; - // Check that 'request' is set. If 'cancel' is set it must be a + if(info.Length() > 0) { + options = Nan::To(info[0]).ToLocalChecked(); + } else { + options = Nan::New(); + } + + // Check that if 'request' is set it must be a function, if 'cancel' is set it must be a // function and if 'ratio' is set it must be a number. - if (!Nan::Has(options, Nan::New("request").ToLocalChecked()).FromJust() - || !Nan::Get(options, Nan::New("request").ToLocalChecked()).ToLocalChecked()->IsFunction()) { - return Nan::ThrowError("Options object must have a 'request' method"); + if (Nan::Has(options, Nan::New("request").ToLocalChecked()).FromJust() + && !Nan::Get(options, Nan::New("request").ToLocalChecked()).ToLocalChecked()->IsFunction()) { + return Nan::ThrowError("Options object 'request' property must be a function"); } if (Nan::Has(options, Nan::New("cancel").ToLocalChecked()).FromJust() @@ -196,11 +202,14 @@ void NodeMap::New(const Nan::FunctionCallbackInfo& info) { info.This()->SetInternalField(1, options); - mbgl::FileSourceManager::get()->registerFileSourceFactory( - mbgl::FileSourceType::ResourceLoader, [](const mbgl::ResourceOptions& resourceOptions, const mbgl::ClientOptions&) { - return std::make_unique( - reinterpret_cast(resourceOptions.platformContext())); - }); + if(Nan::Has(options, Nan::New("request").ToLocalChecked()).FromJust() + && Nan::Get(options, Nan::New("request").ToLocalChecked()).ToLocalChecked()->IsFunction()) { + mbgl::FileSourceManager::get()->registerFileSourceFactory( + mbgl::FileSourceType::ResourceLoader, [](const mbgl::ResourceOptions& resourceOptions, const mbgl::ClientOptions&) { + return std::make_unique( + reinterpret_cast(resourceOptions.platformContext())); + }); + } try { auto nodeMap = new NodeMap(options); diff --git a/platform/node/test/js/map.test.js b/platform/node/test/js/map.test.js index 6f351964b..60948721d 100644 --- a/platform/node/test/js/map.test.js +++ b/platform/node/test/js/map.test.js @@ -17,10 +17,11 @@ test('Map', function(t) { t.end(); }); - t.test('must be constructed with options object', function(t) { - t.throws(function() { - new mbgl.Map(); - }, /Requires an options object as first argument/); + t.test('must be constructed with no options or with options object', function(t) { + t.doesNotThrow(function() { + var map = new mbgl.Map(); + map.release(); + }); t.throws(function() { new mbgl.Map('options'); @@ -29,17 +30,18 @@ test('Map', function(t) { t.end(); }); - t.test('requires request property', function(t) { + t.test('requires request property to be a function', function(t) { var options = {}; - t.throws(function() { - new mbgl.Map(options); - }, /Options object must have a 'request' method/); + t.doesNotThrow(function() { + var map = new mbgl.Map(options); + map.release(); + }); options.request = 'test'; t.throws(function() { new mbgl.Map(options); - }, /Options object must have a 'request' method/); + }, /Options object 'request' property must be a function/); options.request = function() {}; t.doesNotThrow(function() { @@ -51,9 +53,7 @@ test('Map', function(t) { }); t.test('optional cancel property must be a function', function(t) { - var options = { - request: function() {} - }; + var options = {}; options.cancel = 'test'; t.throws(function() { @@ -71,9 +71,7 @@ test('Map', function(t) { t.test('optional ratio property must be a number', function(t) { - var options = { - request: function() {} - }; + var options = {}; options.ratio = 'test'; t.throws(function() {