Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 5 commits into from
Mar 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 46 additions & 3 deletions platform/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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({
Expand All @@ -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
{
Expand All @@ -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:

Expand Down
4 changes: 2 additions & 2 deletions platform/node/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
66 changes: 36 additions & 30 deletions platform/node/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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');
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

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;
Expand Down
31 changes: 20 additions & 11 deletions platform/node/src/node_map.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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);
Expand Down
28 changes: 13 additions & 15 deletions platform/node/test/js/map.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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() {
Expand All @@ -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() {
Expand All @@ -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() {
Expand Down