Make Node Map object options "request" property optional (#904)

This commit is contained in:
tdcosta100
2023-03-23 09:55:29 -03:00
committed by GitHub
parent 3e49ad80f5
commit ee449907c3
5 changed files with 115 additions and 59 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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;

View File

@@ -171,17 +171,23 @@ void NodeMap::New(const Nan::FunctionCallbackInfo<v8::Value>& 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<v8::Object>(info[0]).ToLocalChecked();
v8::Local<v8::Object> options;
// Check that 'request' is set. If 'cancel' is set it must be a
if(info.Length() > 0) {
options = Nan::To<v8::Object>(info[0]).ToLocalChecked();
} else {
options = Nan::New<v8::Object>();
}
// 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<v8::Value>& info) {
info.This()->SetInternalField(1, options);
mbgl::FileSourceManager::get()->registerFileSourceFactory(
mbgl::FileSourceType::ResourceLoader, [](const mbgl::ResourceOptions& resourceOptions, const mbgl::ClientOptions&) {
return std::make_unique<node_mbgl::NodeFileSource>(
reinterpret_cast<node_mbgl::NodeMap*>(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<node_mbgl::NodeFileSource>(
reinterpret_cast<node_mbgl::NodeMap*>(resourceOptions.platformContext()));
});
}
try {
auto nodeMap = new NodeMap(options);

View File

@@ -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() {