From 9766f6fcc26f2e81da651b3bc44cd57e56e4cfd2 Mon Sep 17 00:00:00 2001 From: tdcosta100 Date: Thu, 9 Mar 2023 04:27:14 +0000 Subject: [PATCH] Node platform improvements (#891) --- platform/node/CHANGELOG.md | 1 + platform/node/README.md | 2 + platform/node/src/node_map.cpp | 74 +++++++++++++-- platform/node/src/node_map.hpp | 2 + platform/node/test/js/map.test.js | 147 +++++++++++++++++++++++++++++- 5 files changed, 212 insertions(+), 14 deletions(-) diff --git a/platform/node/CHANGELOG.md b/platform/node/CHANGELOG.md index f6cb8442b..96bcde2b3 100644 --- a/platform/node/CHANGELOG.md +++ b/platform/node/CHANGELOG.md @@ -5,6 +5,7 @@ - *...Add new stuff here...* # 5.2.0 +* Node platform improvements (added setSize and a new render call without render options object) by @tdcosta100 in https://github.com/maplibre/maplibre-gl-native/pull/891 * Move node ci+release to self hosted Ubuntu arm64 by @acalcutt in https://github.com/maplibre/maplibre-gl-native/pull/873 * Add windows support by @tdcosta100 in https://github.com/maplibre/maplibre-gl-native/pull/707 * Add Typings for Node Platform by @KiwiKilian in https://github.com/maplibre/maplibre-gl-native/pull/766 diff --git a/platform/node/README.md b/platform/node/README.md index 55f90368c..3be203d84 100644 --- a/platform/node/README.md +++ b/platform/node/README.md @@ -173,6 +173,8 @@ var map = new mbgl.Map({ response.data = body; callback(null, response); + } else if (res.statusCode == 204) { + callback(); } else { callback(new Error(JSON.parse(body).message)); } diff --git a/platform/node/src/node_map.cpp b/platform/node/src/node_map.cpp index 77a3ef806..bb5561333 100644 --- a/platform/node/src/node_map.cpp +++ b/platform/node/src/node_map.cpp @@ -102,6 +102,7 @@ ParseError)JS").ToLocalChecked()).ToLocalChecked(); Nan::SetPrototypeMethod(tpl, "setLayoutProperty", SetLayerProperty); Nan::SetPrototypeMethod(tpl, "setPaintProperty", SetLayerProperty); Nan::SetPrototypeMethod(tpl, "setFilter", SetFilter); + Nan::SetPrototypeMethod(tpl, "setSize", SetSize); Nan::SetPrototypeMethod(tpl, "setCenter", SetCenter); Nan::SetPrototypeMethod(tpl, "setZoom", SetZoom); Nan::SetPrototypeMethod(tpl, "setBearing", SetBearing); @@ -426,11 +427,11 @@ void NodeMap::Render(const Nan::FunctionCallbackInfo& info) { auto nodeMap = Nan::ObjectWrap::Unwrap(info.Holder()); if (!nodeMap->map) return Nan::ThrowError(releasedMessage()); - if (info.Length() <= 0 || !info[0]->IsObject()) { - return Nan::ThrowTypeError("First argument must be an options object"); + if (info.Length() <= 0 || (!info[0]->IsObject() && !info[0]->IsFunction())) { + return Nan::ThrowTypeError("First argument must be an options object or a callback function"); } - if (info.Length() <= 1 || !info[1]->IsFunction()) { + if ((info.Length() <= 1 && !info[0]->IsFunction()) || (info.Length() > 1 && !info[1]->IsFunction())) { return Nan::ThrowTypeError("Second argument must be a callback function"); } @@ -443,12 +444,20 @@ void NodeMap::Render(const Nan::FunctionCallbackInfo& info) { } try { - auto options = ParseOptions(Nan::To(info[0]).ToLocalChecked()); - assert(!nodeMap->req); - assert(!nodeMap->image.data); - nodeMap->req = std::make_unique(Nan::To(info[1]).ToLocalChecked()); + if (info[0]->IsFunction()) { + assert(!nodeMap->req); + assert(!nodeMap->image.data); + nodeMap->req = std::make_unique(Nan::To(info[0]).ToLocalChecked()); - nodeMap->startRender(options); + nodeMap->startRender(); + } else { + auto options = ParseOptions(Nan::To(info[0]).ToLocalChecked()); + assert(!nodeMap->req); + assert(!nodeMap->image.data); + nodeMap->req = std::make_unique(Nan::To(info[1]).ToLocalChecked()); + + nodeMap->startRender(options); + } } catch (const mbgl::style::conversion::Error& err) { return Nan::ThrowTypeError(err.message.c_str()); } catch (const mbgl::util::StyleParseException& ex) { @@ -460,6 +469,27 @@ void NodeMap::Render(const Nan::FunctionCallbackInfo& info) { info.GetReturnValue().SetUndefined(); } +void NodeMap::startRender() { + map->renderStill([this](const std::exception_ptr& eptr) { + if (eptr) { + error = eptr; + uv_async_send(async); + } else { + assert(!image.data); + image = frontend->readStillImage(); + 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(async)); +} + void NodeMap::startRender(const NodeMap::RenderOptions& options) { frontend->setSize(options.size); map->setSize(options.size); @@ -640,7 +670,7 @@ void NodeMap::cancel() { reinterpret_cast(h->data)->renderFinished(); }); - frontend = std::make_unique(mbgl::Size{ 256, 256 }, pixelRatio); + frontend = std::make_unique(mbgl::Size{ 512, 512 }, pixelRatio); map = std::make_unique(*frontend, mapObserver, mbgl::MapOptions().withSize(frontend->getSize()) .withPixelRatio(pixelRatio) @@ -925,6 +955,30 @@ void NodeMap::SetFilter(const Nan::FunctionCallbackInfo& info) { layer->setFilter(filter); } +void NodeMap::SetSize(const Nan::FunctionCallbackInfo& info) { + auto nodeMap = Nan::ObjectWrap::Unwrap(info.Holder()); + if (!nodeMap->map) return Nan::ThrowError(releasedMessage()); + + if (info.Length() <= 0 || !info[0]->IsArray()) { + return Nan::ThrowTypeError("First argument must be an array"); + } + + auto size = info[0].As(); + uint32_t width= 0; + uint32_t height = 0; + if (size->Length() > 0) { width = Nan::To(Nan::Get(size, 0).ToLocalChecked()).ToChecked(); } + if (size->Length() > 1) { height = Nan::To(Nan::Get(size, 1).ToLocalChecked()).ToChecked(); } + + try { + nodeMap->frontend->setSize({ width, height }); + nodeMap->map->setSize({ width, height }); + } catch (const std::exception &ex) { + return Nan::ThrowError(ex.what()); + } + + info.GetReturnValue().SetUndefined(); +} + void NodeMap::SetCenter(const Nan::FunctionCallbackInfo& info) { auto nodeMap = Nan::ObjectWrap::Unwrap(info.Holder()); if (!nodeMap->map) return Nan::ThrowError(releasedMessage()); @@ -1419,7 +1473,7 @@ NodeMap::NodeMap(v8::Local options) : true; }()) , mapObserver(NodeMapObserver()) - , frontend(std::make_unique(mbgl::Size { 256, 256 }, pixelRatio)) + , frontend(std::make_unique(mbgl::Size { 512, 512 }, pixelRatio)) , map(std::make_unique(*frontend, mapObserver, mbgl::MapOptions().withSize(frontend->getSize()) .withPixelRatio(pixelRatio) diff --git a/platform/node/src/node_map.hpp b/platform/node/src/node_map.hpp index ab84a66c5..179ab4121 100644 --- a/platform/node/src/node_map.hpp +++ b/platform/node/src/node_map.hpp @@ -55,6 +55,7 @@ public: static void SetLayerZoomRange(const Nan::FunctionCallbackInfo&); static void SetLayerProperty(const Nan::FunctionCallbackInfo&); static void SetFilter(const Nan::FunctionCallbackInfo&); + static void SetSize(const Nan::FunctionCallbackInfo&); static void SetCenter(const Nan::FunctionCallbackInfo&); static void SetZoom(const Nan::FunctionCallbackInfo&); static void SetBearing(const Nan::FunctionCallbackInfo&); @@ -72,6 +73,7 @@ public: static v8::Local ParseError(const char* msg); + void startRender(); void startRender(const RenderOptions& options); void renderFinished(); diff --git a/platform/node/test/js/map.test.js b/platform/node/test/js/map.test.js index d085e32f4..6f351964b 100644 --- a/platform/node/test/js/map.test.js +++ b/platform/node/test/js/map.test.js @@ -118,6 +118,7 @@ test('Map', function(t) { 'setLayoutProperty', 'setPaintProperty', 'setFilter', + 'setSize', 'setCenter', 'setZoom', 'setBearing', @@ -415,16 +416,16 @@ test('Map', function(t) { ratio: 1 }; - t.test('requires an object as the first parameter', function(t) { + t.test('requires an object or a callback function as the first parameter', function(t) { var map = new mbgl.Map(options); t.throws(function() { map.render(); - }, /First argument must be an options object/); + }, /First argument must be an options object or a callback function/); t.throws(function() { map.render('invalid'); - }, /First argument must be an options object/); + }, /First argument must be an options object or a callback function/); map.release(); t.end(); @@ -456,6 +457,17 @@ test('Map', function(t) { t.end(); }); + t.test('requires a style to be set (without render options object)', function(t) { + var map = new mbgl.Map(options); + + t.throws(function() { + map.render(function() {}); + }, /Style is not loaded/); + + map.release(); + t.end(); + }); + t.test('returns an error delayed', function(t) { var delay = 0; var map = new mbgl.Map({ @@ -476,6 +488,27 @@ test('Map', function(t) { }); }); + t.test('returns an error delayed (without render options object)', function(t) { + var delay = 0; + var map = new mbgl.Map({ + request: function(req, callback) { + delay += 100; + setTimeout(function() { + callback(new Error('not found')); + }, delay); + }, + ratio: 1 + }); + map.load(style); + map.setZoom(1); + map.render(function(err, data) { + map.release(); + + t.ok(err, 'returns error'); + t.end(); + }); + }); + t.test('returns an error', function(t) { var map = new mbgl.Map(options); map.load(style); @@ -487,6 +520,18 @@ test('Map', function(t) { }); }); + t.test('returns an error (without render options object)', function(t) { + var map = new mbgl.Map(options); + map.load(style); + map.setZoom(1); + map.render(function(err, data) { + map.release(); + + t.ok(err, 'returns error'); + t.end(); + }); + }); + t.test('double release', function(t) { var map = new mbgl.Map(options); map.release(); @@ -511,6 +556,19 @@ test('Map', function(t) { }); }); + t.test('returns an image (without render options object)', function(t) { + var map = new mbgl.Map(options); + map.load(style); + map.render(function(err, pixels) { + t.error(err); + map.release(); + t.ok(pixels); + t.ok(pixels instanceof Buffer); + t.equal(pixels.length, 512 * 512 * 4) + t.end(); + }); + }); + t.test('can be called several times in serial', function(t) { var completed = 0; var remaining = 10; @@ -536,6 +594,31 @@ test('Map', function(t) { render(); }); + t.test('can be called several times in serial (without render options object)', function(t) { + var completed = 0; + var remaining = 10; + var start = +new Date; + + var map = new mbgl.Map(options); + map.load(style); + + function render() { + map.render(function(err, data) { + t.error(err); + + t.ok(true, 'render @ ' + ((+new Date) - start) + 'ms'); + if (++completed === remaining) { + map.release(); + t.end(); + } else { + render(); + } + }); + } + + render(); + }); + t.test('throws if called in parallel', function(t) { var map = new mbgl.Map(options); map.load(style); @@ -549,6 +632,19 @@ test('Map', function(t) { t.end(); }); + t.test('throws if called in parallel (without render options object)', function(t) { + var map = new mbgl.Map(options); + map.load(style); + + t.throws(function() { + map.render(function() {}); + map.render(function() {}); + }, /Map is currently processing a RenderRequest/); + + map.release(); + t.end(); + }); + // This can't be tested with a test-suite render test because zoom and center // are set via a different code path when included as style properties. t.test('sets zoom before center', function(t) { @@ -589,7 +685,50 @@ test('Map', function(t) { t.equal(numPixels, 0); t.end(); }); - }) + }); + + t.test('sets zoom before center (without render options object)', function(t) { + var map = new mbgl.Map(options); + map.load({ + "version": 8, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Point", + "coordinates": [ + 18.05489, + 59.32744 + ] + } + } + }, + "layers": [ + { + "id": "circle", + "type": "circle", + "source": "geojson", + "paint": { + "circle-color": "red" + } + } + ] + }); + map.setSize([400, 400]); + map.setZoom(5); + map.setCenter([18.05489, 59.32744]); + map.render(function(err, actual) { + t.error(err); + + var PNG = require('pngjs').PNG; + var pixelmatch = require('pixelmatch'); + var expected = PNG.sync.read( + fs.readFileSync(path.join(__dirname, '../fixtures/zoom-center/expected.png'))).data; + var numPixels = pixelmatch(actual, expected, undefined, 400, 400, { threshold: 0.13 }); + t.equal(numPixels, 0); + t.end(); + }); + }); }); t.test('request callback', function (t) {