Files
MapLibreParticleLab/platform/node/src/node_map.cpp
Bart Louwers 2a8ebc4906 core: rename mbgl directories to mln (#4511)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-08-22 20:50:14 +02:00

1585 lines
58 KiB
C++

#include "node_map.hpp"
#include "node_request.hpp"
#include "node_feature.hpp"
#include "node_conversion.hpp"
#include <mln/renderer/renderer.hpp>
#include <mln/gfx/headless_frontend.hpp>
#include <mln/style/conversion/source.hpp>
#include <mln/style/conversion/layer.hpp>
#include <mln/style/conversion/filter.hpp>
#include <mln/style/conversion/light.hpp>
#include <mln/style/layers/background_layer.hpp>
#include <mln/style/layers/circle_layer.hpp>
#include <mln/style/layers/fill_layer.hpp>
#include <mln/style/layers/fill_extrusion_layer.hpp>
#include <mln/style/layers/heatmap_layer.hpp>
#include <mln/style/layers/hillshade_layer.hpp>
#include <mln/style/layers/color_relief_layer.hpp>
#include <mln/style/layers/line_layer.hpp>
#include <mln/style/layers/raster_layer.hpp>
#include <mln/style/layers/symbol_layer.hpp>
#include <mln/map/map.hpp>
#include <mln/map/map_observer.hpp>
#include <mln/storage/file_source_manager.hpp>
#include <mln/storage/resource_options.hpp>
#include <mln/style/image.hpp>
#include <mln/style/light.hpp>
#include <mln/style/style.hpp>
#include <mln/util/async_request.hpp>
#include <mln/util/exception.hpp>
#include <mln/util/logging.hpp>
#include <mln/util/premultiply.hpp>
namespace node_mbgl {
struct NodeMap::RenderOptions {
double zoom = 0;
double bearing = 0;
mln::style::Light light;
double pitch = 0;
double latitude = 0;
double longitude = 0;
mln::Size size = {512, 512};
bool axonometric = false;
double xSkew = 0;
double ySkew = 1;
std::vector<std::string> classes;
mln::MapDebugOptions debugOptions = mln::MapDebugOptions::NoDebug;
};
Nan::Persistent<v8::Function> NodeMap::constructor;
Nan::Persistent<v8::Object> NodeMap::parseError;
static const char* releasedMessage() {
return "Map resources have already been released";
}
void NodeMapObserver::onDidFailLoadingMap(mln::MapLoadError error, const std::string& description) {
switch (error) {
case mln::MapLoadError::StyleParseError:
Nan::ThrowError(NodeMap::ParseError(description.c_str()));
break;
default:
Nan::ThrowError(description.c_str());
}
}
void NodeMap::Init(v8::Local<v8::Object> target) {
// Define a custom error class for parse errors
auto script = Nan::New<v8::UnboundScript>(Nan::New(R"JS(
class ParseError extends Error {
constructor(...params) {
super(...params);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ParseError);
}
}
}
ParseError)JS")
.ToLocalChecked())
.ToLocalChecked();
parseError.Reset(Nan::To<v8::Object>(Nan::RunScript(script).ToLocalChecked()).ToLocalChecked());
Nan::Set(target, Nan::New("ParseError").ToLocalChecked(), Nan::New(parseError));
v8::Local<v8::FunctionTemplate> tpl = Nan::New<v8::FunctionTemplate>(New);
tpl->SetClassName(Nan::New("Map").ToLocalChecked());
tpl->InstanceTemplate()->SetInternalFieldCount(2);
Nan::SetPrototypeMethod(tpl, "load", Load);
Nan::SetPrototypeMethod(tpl, "loaded", Loaded);
Nan::SetPrototypeMethod(tpl, "render", Render);
Nan::SetPrototypeMethod(tpl, "release", Release);
Nan::SetPrototypeMethod(tpl, "cancel", Cancel);
Nan::SetPrototypeMethod(tpl, "addSource", AddSource);
Nan::SetPrototypeMethod(tpl, "removeSource", RemoveSource);
Nan::SetPrototypeMethod(tpl, "addLayer", AddLayer);
Nan::SetPrototypeMethod(tpl, "removeLayer", RemoveLayer);
Nan::SetPrototypeMethod(tpl, "addImage", AddImage);
Nan::SetPrototypeMethod(tpl, "removeImage", RemoveImage);
Nan::SetPrototypeMethod(tpl, "setLayerZoomRange", SetLayerZoomRange);
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);
Nan::SetPrototypeMethod(tpl, "setPitch", SetPitch);
Nan::SetPrototypeMethod(tpl, "setLight", SetLight);
Nan::SetPrototypeMethod(tpl, "setAxonometric", SetAxonometric);
Nan::SetPrototypeMethod(tpl, "setXSkew", SetXSkew);
Nan::SetPrototypeMethod(tpl, "setYSkew", SetYSkew);
Nan::SetPrototypeMethod(tpl, "setFeatureState", SetFeatureState);
Nan::SetPrototypeMethod(tpl, "getFeatureState", GetFeatureState);
Nan::SetPrototypeMethod(tpl, "removeFeatureState", RemoveFeatureState);
Nan::SetPrototypeMethod(tpl, "dumpDebugLogs", DumpDebugLogs);
Nan::SetPrototypeMethod(tpl, "queryRenderedFeatures", QueryRenderedFeatures);
#if defined NODE_MODULE_VERSION && NODE_MODULE_VERSION < 93
v8::Local<v8::Context> context = target->CreationContext();
#else
v8::Local<v8::Context> context = target->GetCreationContext().ToLocalChecked();
#endif
constructor.Reset(tpl->GetFunction(context).ToLocalChecked());
Nan::Set(target, Nan::New("Map").ToLocalChecked(), tpl->GetFunction(context).ToLocalChecked());
}
/**
* A request object, given to the `request` handler of a map, is an
* encapsulation of a URL and type of a resource that the map asks you to load.
*
* The `kind` property is one of
*
* "Unknown": 0,
* "Style": 1,
* "Source": 2,
* "Tile": 3,
* "Glyphs": 4,
* "SpriteImage": 5,
* "SpriteJSON": 6
*
* @typedef
* @name Request
* @property {string} url
* @property {number} kind
*/
/**
* Mapbox GL object: this object loads stylesheets and renders them into
* images.
*
* @class
* @name Map
* @param {Object} options
* @param {Function} options.request a method used to request resources
* over the internet
* @param {Function} [options.cancel]
* @param {number} options.ratio pixel ratio
* @example
* 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);
* });
*/
void NodeMap::New(const Nan::FunctionCallbackInfo<v8::Value>& info) {
if (!info.IsConstructCall()) {
return Nan::ThrowTypeError("Use the new operator to create new Map objects");
}
if (info.Length() > 0 && !info[0]->IsObject()) {
return Nan::ThrowTypeError("Requires an options object as first argument");
}
v8::Local<v8::Object> options;
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 'request' property must be a function");
}
if (Nan::Has(options, Nan::New("cancel").ToLocalChecked()).FromJust() &&
!Nan::Get(options, Nan::New("cancel").ToLocalChecked()).ToLocalChecked()->IsFunction()) {
return Nan::ThrowError("Options object 'cancel' property must be a function");
}
if (Nan::Has(options, Nan::New("ratio").ToLocalChecked()).FromJust() &&
!Nan::Get(options, Nan::New("ratio").ToLocalChecked()).ToLocalChecked()->IsNumber()) {
return Nan::ThrowError("Options object 'ratio' property must be a number");
}
info.This()->SetInternalField(1, options);
if (Nan::Has(options, Nan::New("request").ToLocalChecked()).FromJust() &&
Nan::Get(options, Nan::New("request").ToLocalChecked()).ToLocalChecked()->IsFunction()) {
mln::FileSourceManager::get()->registerFileSourceFactory(
mln::FileSourceType::ResourceLoader,
[](const mln::ResourceOptions& resourceOptions, const mln::ClientOptions&) {
return std::make_unique<node_mbgl::NodeFileSource>(
reinterpret_cast<node_mbgl::NodeMap*>(resourceOptions.platformContext()));
});
}
try {
auto nodeMap = new NodeMap(options);
nodeMap->Wrap(info.This());
} catch (std::exception& ex) {
return Nan::ThrowError(ex.what());
}
info.GetReturnValue().Set(info.This());
}
/**
* Load a stylesheet
*
* @function
* @name load
* @param {string|Object} stylesheet either an object or a JSON representation
* @param {Object} options
* @param {boolean} options.defaultStyleCamera if true, sets the default style
* camera
* @returns {undefined} loads stylesheet into map
* @throws {Error} if stylesheet is missing or invalid
* @example
* // providing an object
* map.load(require('./test/fixtures/style.json'));
*
* // providing a string
* map.load(fs.readFileSync('./test/fixtures/style.json', 'utf8'));
*/
void NodeMap::Load(const Nan::FunctionCallbackInfo<v8::Value>& info) {
v8::Local<v8::Context> context = info.GetIsolate()->GetCurrentContext();
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
// Reset the flag as this could be the second time
// we are calling this (being the previous successful).
nodeMap->loaded = false;
if (info.Length() < 1) {
return Nan::ThrowError("Requires a map style as first argument");
}
std::string style;
if (info[0]->IsObject()) {
Nan::JSON JSON;
style = *Nan::Utf8String(JSON.Stringify(info[0]->ToObject(context).ToLocalChecked()).ToLocalChecked());
} else if (info[0]->IsString()) {
style = *Nan::Utf8String(info[0]);
} else {
return Nan::ThrowTypeError("First argument must be a string or object");
}
try {
nodeMap->map->getStyle().loadJSON(style);
} catch (const mln::util::StyleParseException& ex) {
return Nan::ThrowError(ParseError(ex.what()));
} catch (const std::exception& ex) {
return Nan::ThrowError(ex.what());
}
if (info.Length() == 2) {
if (!info[1]->IsObject()) {
return Nan::ThrowTypeError("Second argument must be an options object");
}
auto options = Nan::To<v8::Object>(info[1]).ToLocalChecked();
if (Nan::Has(options, Nan::New("defaultStyleCamera").ToLocalChecked()).FromJust()) {
if (!Nan::Get(options, Nan::New("defaultStyleCamera").ToLocalChecked()).ToLocalChecked()->IsBoolean()) {
return Nan::ThrowError(
"Options object 'defaultStyleCamera' property must be a "
"boolean");
}
if (Nan::To<bool>(Nan::Get(options, Nan::New("cameraMutated").ToLocalChecked()).ToLocalChecked())
.ToChecked()) {
nodeMap->map->jumpTo(nodeMap->map->getStyle().getDefaultCamera());
}
}
}
nodeMap->loaded = true;
info.GetReturnValue().SetUndefined();
}
void NodeMap::Loaded(const Nan::FunctionCallbackInfo<v8::Value>& info) {
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
bool loaded = false;
try {
loaded = nodeMap->map->isFullyLoaded();
} catch (const std::exception& ex) {
return Nan::ThrowError(ex.what());
}
info.GetReturnValue().Set(Nan::New(loaded));
}
NodeMap::RenderOptions NodeMap::ParseOptions(v8::Local<v8::Object> obj) {
Nan::HandleScope scope;
NodeMap::RenderOptions options;
if (Nan::Has(obj, Nan::New("zoom").ToLocalChecked()).FromJust()) {
options.zoom = Nan::To<double>(Nan::Get(obj, Nan::New("zoom").ToLocalChecked()).ToLocalChecked()).ToChecked();
}
if (Nan::Has(obj, Nan::New("bearing").ToLocalChecked()).FromJust()) {
options.bearing =
Nan::To<double>(Nan::Get(obj, Nan::New("bearing").ToLocalChecked()).ToLocalChecked()).ToChecked();
}
if (Nan::Has(obj, Nan::New("pitch").ToLocalChecked()).FromJust()) {
options.pitch = Nan::To<double>(Nan::Get(obj, Nan::New("pitch").ToLocalChecked()).ToLocalChecked()).ToChecked();
}
if (Nan::Has(obj, Nan::New("light").ToLocalChecked()).FromJust()) {
auto lightObj = Nan::Get(obj, Nan::New("light").ToLocalChecked()).ToLocalChecked();
mln::style::conversion::Error conversionError;
if (auto light = mln::style::conversion::convert<mln::style::Light>(lightObj, conversionError)) {
options.light = *light;
} else {
throw std::move(conversionError);
}
}
if (Nan::Has(obj, Nan::New("axonometric").ToLocalChecked()).FromJust()) {
options.axonometric =
Nan::To<bool>(Nan::Get(obj, Nan::New("axonometric").ToLocalChecked()).ToLocalChecked()).ToChecked();
}
if (Nan::Has(obj, Nan::New("skew").ToLocalChecked()).FromJust()) {
auto skewObj = Nan::Get(obj, Nan::New("skew").ToLocalChecked()).ToLocalChecked();
if (skewObj->IsArray()) {
auto skew = skewObj.As<v8::Array>();
if (skew->Length() > 0) {
options.xSkew = Nan::To<double>(Nan::Get(skew, 0).ToLocalChecked()).ToChecked();
}
if (skew->Length() > 1) {
options.ySkew = Nan::To<double>(Nan::Get(skew, 1).ToLocalChecked()).ToChecked();
}
}
}
if (Nan::Has(obj, Nan::New("center").ToLocalChecked()).FromJust()) {
auto centerObj = Nan::Get(obj, Nan::New("center").ToLocalChecked()).ToLocalChecked();
if (centerObj->IsArray()) {
auto center = centerObj.As<v8::Array>();
if (center->Length() > 0) {
options.longitude = Nan::To<double>(Nan::Get(center, 0).ToLocalChecked()).ToChecked();
}
if (center->Length() > 1) {
options.latitude = Nan::To<double>(Nan::Get(center, 1).ToLocalChecked()).ToChecked();
}
}
}
if (Nan::Has(obj, Nan::New("width").ToLocalChecked()).FromJust()) {
options.size.width = static_cast<uint32_t>(
Nan::To<int64_t>(Nan::Get(obj, Nan::New("width").ToLocalChecked()).ToLocalChecked()).ToChecked());
}
if (Nan::Has(obj, Nan::New("height").ToLocalChecked()).FromJust()) {
options.size.height = static_cast<uint32_t>(
Nan::To<int64_t>(Nan::Get(obj, Nan::New("height").ToLocalChecked()).ToLocalChecked()).ToChecked());
}
if (Nan::Has(obj, Nan::New("classes").ToLocalChecked()).FromJust()) {
auto classes = Nan::To<v8::Object>(Nan::Get(obj, Nan::New("classes").ToLocalChecked()).ToLocalChecked())
.ToLocalChecked()
.As<v8::Array>();
const int length = classes->Length();
options.classes.reserve(length);
for (int i = 0; i < length; i++) {
options.classes.emplace_back(std::string{
*Nan::Utf8String(Nan::To<v8::String>(Nan::Get(classes, i).ToLocalChecked()).ToLocalChecked())});
}
}
if (Nan::Has(obj, Nan::New("debug").ToLocalChecked()).FromJust()) {
auto debug =
Nan::To<v8::Object>(Nan::Get(obj, Nan::New("debug").ToLocalChecked()).ToLocalChecked()).ToLocalChecked();
if (Nan::Has(debug, Nan::New("tileBorders").ToLocalChecked()).FromJust()) {
if (Nan::To<bool>(Nan::Get(debug, Nan::New("tileBorders").ToLocalChecked()).ToLocalChecked()).ToChecked()) {
options.debugOptions = options.debugOptions | mln::MapDebugOptions::TileBorders;
}
}
if (Nan::Has(debug, Nan::New("parseStatus").ToLocalChecked()).FromJust()) {
if (Nan::To<bool>(Nan::Get(debug, Nan::New("parseStatus").ToLocalChecked()).ToLocalChecked()).ToChecked()) {
options.debugOptions = options.debugOptions | mln::MapDebugOptions::ParseStatus;
}
}
if (Nan::Has(debug, Nan::New("timestamps").ToLocalChecked()).FromJust()) {
if (Nan::To<bool>(Nan::Get(debug, Nan::New("timestamps").ToLocalChecked()).ToLocalChecked()).ToChecked()) {
options.debugOptions = options.debugOptions | mln::MapDebugOptions::Timestamps;
}
}
if (Nan::Has(debug, Nan::New("collision").ToLocalChecked()).FromJust()) {
if (Nan::To<bool>(Nan::Get(debug, Nan::New("collision").ToLocalChecked()).ToLocalChecked()).ToChecked()) {
options.debugOptions = options.debugOptions | mln::MapDebugOptions::Collision;
}
}
if (Nan::Has(debug, Nan::New("overdraw").ToLocalChecked()).FromJust()) {
if (Nan::To<bool>(Nan::Get(debug, Nan::New("overdraw").ToLocalChecked()).ToLocalChecked()).ToChecked()) {
options.debugOptions = mln::MapDebugOptions::Overdraw;
}
}
}
return options;
}
class RenderRequest : public Nan::AsyncResource {
public:
explicit RenderRequest(v8::Local<v8::Function> callback_)
: AsyncResource("mbgl:RenderRequest") {
callback.Reset(callback_);
}
~RenderRequest() { callback.Reset(); }
Nan::Persistent<v8::Function> callback;
};
/**
* Render an image from the currently-loaded style
*
* @name render
* @param {Object} options
* @param {number} [options.zoom=0]
* @param {number} [options.width=512]
* @param {number} [options.height=512]
* @param {Array<number>} [options.center=[0,0]] longitude, latitude center
* of the map
* @param {number} [options.bearing=0] rotation
* @param {Array<string>} [options.classes=[]] style classes
* @param {Function} callback
* @returns {undefined} calls callback
* @throws {Error} if stylesheet is not loaded or if map is already rendering
*/
void NodeMap::Render(const Nan::FunctionCallbackInfo<v8::Value>& info) {
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
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[0]->IsFunction()) || (info.Length() > 1 && !info[1]->IsFunction())) {
return Nan::ThrowTypeError("Second argument must be a callback function");
}
if (!nodeMap->loaded) {
return Nan::ThrowTypeError("Style is not loaded");
}
if (nodeMap->req) {
return Nan::ThrowError("Map is currently processing a RenderRequest");
}
try {
if (info[0]->IsFunction()) {
assert(!nodeMap->req);
assert(!nodeMap->image.data);
nodeMap->req = std::make_unique<RenderRequest>(Nan::To<v8::Function>(info[0]).ToLocalChecked());
nodeMap->startRender();
} else {
auto options = ParseOptions(Nan::To<v8::Object>(info[0]).ToLocalChecked());
assert(!nodeMap->req);
assert(!nodeMap->image.data);
nodeMap->req = std::make_unique<RenderRequest>(Nan::To<v8::Function>(info[1]).ToLocalChecked());
nodeMap->startRender(options);
}
} catch (const mln::style::conversion::Error& err) {
return Nan::ThrowTypeError(err.message.c_str());
} catch (const mln::util::StyleParseException& ex) {
return Nan::ThrowError(ParseError(ex.what()));
} catch (const mln::util::Exception& ex) {
return Nan::ThrowError(ex.what());
}
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<uv_handle_t*>(async));
}
void NodeMap::startRender(const NodeMap::RenderOptions& options) {
frontend->setSize(options.size);
map->setSize(options.size);
mln::CameraOptions camera;
camera.center = mln::LatLng{options.latitude, options.longitude};
camera.zoom = options.zoom;
camera.bearing = options.bearing;
camera.pitch = options.pitch;
auto projectionOptions =
mln::ProjectionMode().withAxonometric(options.axonometric).withXSkew(options.xSkew).withYSkew(options.ySkew);
map->setProjectionMode(projectionOptions);
map->renderStill(camera, options.debugOptions, [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<uv_handle_t*>(async));
}
v8::Local<v8::Value> NodeMap::ParseError(const char* msg) {
v8::Local<v8::Value> argv[] = {Nan::New(msg).ToLocalChecked()};
return Nan::CallAsConstructor(Nan::New(parseError), 1, argv).ToLocalChecked();
}
void NodeMap::renderFinished() {
assert(req);
Nan::HandleScope scope;
// 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));
// Move the callback and image out of the way so that the callback can start a new render call.
auto request = std::move(req);
auto img = std::move(image);
assert(request);
// These have to be empty to be prepared for the next render call.
assert(!req);
assert(!image.data);
v8::Local<v8::Function> callback = Nan::New(request->callback);
v8::Local<v8::Object> target = Nan::New<v8::Object>();
if (error) {
v8::Local<v8::Value> err;
try {
std::rethrow_exception(error);
assert(false);
} catch (const mln::util::StyleParseException& ex) {
err = ParseError(ex.what());
} catch (const std::exception& ex) {
err = Nan::Error(ex.what());
}
v8::Local<v8::Value> argv[] = {err};
// This must be empty to be prepared for the next render call.
error = nullptr;
assert(!error);
request->runInAsyncScope(target, callback, 1, argv);
} else if (img.data) {
v8::Local<v8::Object> pixels = Nan::NewBuffer(
reinterpret_cast<char*>(img.data.get()),
img.bytes(),
// Retain the data until the buffer is deleted.
[](char*, void* hint) { delete[] reinterpret_cast<uint8_t*>(hint); },
img.data.get())
.ToLocalChecked();
if (!pixels.IsEmpty()) {
img.data.release();
}
v8::Local<v8::Value> argv[] = {Nan::Null(), pixels};
request->runInAsyncScope(target, callback, 2, argv);
} else {
v8::Local<v8::Value> argv[] = {Nan::Error("Didn't get an image")};
request->runInAsyncScope(target, callback, 1, argv);
}
// There is no render pending anymore, we the GC could now delete this
// object if it went out of scope.
Unref();
}
/**
* Clean up any resources used by a map instance.options
* @name release
* @returns {undefined}
*/
void NodeMap::Release(const Nan::FunctionCallbackInfo<v8::Value>& info) {
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
try {
nodeMap->release();
} catch (const std::exception& ex) {
return Nan::ThrowError(ex.what());
}
info.GetReturnValue().SetUndefined();
}
void NodeMap::release() {
if (!map) throw mln::util::Exception(releasedMessage());
uv_close(reinterpret_cast<uv_handle_t*>(async), [](uv_handle_t* h) { delete reinterpret_cast<uv_async_t*>(h); });
map.reset();
frontend.reset();
}
/**
* Cancel an ongoing render request. The callback will be called with
* the error set to "Canceled". Will throw if no rendering is in progress.
* @name cancel
* @returns {undefined}
*/
void NodeMap::Cancel(const Nan::FunctionCallbackInfo<v8::Value>& info) {
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
if (!nodeMap->req) return Nan::ThrowError("No render in progress");
try {
nodeMap->cancel();
} catch (const std::exception& ex) {
return Nan::ThrowError(ex.what());
}
info.GetReturnValue().SetUndefined();
}
void NodeMap::cancel() {
auto style = map->getStyle().getJSON();
// Reset map explicitly as it resets the renderer frontend
map.reset();
// Remove the existing async handle to flush any scheduled calls to renderFinished.
uv_unref(reinterpret_cast<uv_handle_t*>(async));
uv_close(reinterpret_cast<uv_handle_t*>(async), [](uv_handle_t* h) { delete reinterpret_cast<uv_async_t*>(h); });
async = new uv_async_t;
async->data = this;
uv_async_init(
uv_default_loop(), async, [](uv_async_t* h) { reinterpret_cast<NodeMap*>(h->data)->renderFinished(); });
frontend = std::make_unique<mln::HeadlessFrontend>(mln::Size{512, 512}, pixelRatio);
map = std::make_unique<mln::Map>(*frontend,
mapObserver,
mln::MapOptions()
.withSize(frontend->getSize())
.withPixelRatio(pixelRatio)
.withMapMode(mode)
.withCrossSourceCollisions(crossSourceCollisions),
mln::ResourceOptions().withPlatformContext(reinterpret_cast<void*>(this)),
mln::ClientOptions());
// FIXME: Reload the style after recreating the map. We need to find
// a better way of canceling an ongoing rendering on the core level
// without resetting the map, which is way too expensive.
map->getStyle().loadJSON(style);
error = std::make_exception_ptr(std::runtime_error("Canceled"));
renderFinished();
}
void NodeMap::AddSource(const Nan::FunctionCallbackInfo<v8::Value>& info) {
using namespace mln::style;
using namespace mln::style::conversion;
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
if (info.Length() != 2) {
return Nan::ThrowTypeError("Two argument required");
}
if (!info[0]->IsString()) {
return Nan::ThrowTypeError("First argument must be a string");
}
if (!info[1]->IsObject()) {
return Nan::ThrowTypeError("Second argument must be an object");
}
Error error;
std::optional<std::unique_ptr<Source>> source = convert<std::unique_ptr<Source>>(
info[1], error, *Nan::Utf8String(info[0]));
if (!source) {
Nan::ThrowTypeError(error.message.c_str());
return;
}
nodeMap->map->getStyle().addSource(std::move(*source));
}
void NodeMap::RemoveSource(const Nan::FunctionCallbackInfo<v8::Value>& info) {
using namespace mln::style;
using namespace mln::style::conversion;
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
if (info.Length() != 1) {
return Nan::ThrowTypeError("One argument required");
}
if (!info[0]->IsString()) {
return Nan::ThrowTypeError("First argument must be a string");
}
nodeMap->map->getStyle().removeSource(*Nan::Utf8String(info[0]));
}
void NodeMap::AddLayer(const Nan::FunctionCallbackInfo<v8::Value>& info) {
using namespace mln::style;
using namespace mln::style::conversion;
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
if (info.Length() != 1) {
return Nan::ThrowTypeError("One argument required");
}
Error error;
std::optional<std::unique_ptr<Layer>> layer = convert<std::unique_ptr<Layer>>(info[0], error);
if (!layer) {
Nan::ThrowTypeError(error.message.c_str());
return;
}
nodeMap->map->getStyle().addLayer(std::move(*layer));
}
void NodeMap::RemoveLayer(const Nan::FunctionCallbackInfo<v8::Value>& info) {
using namespace mln::style;
using namespace mln::style::conversion;
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
if (info.Length() != 1) {
return Nan::ThrowTypeError("One argument required");
}
if (!info[0]->IsString()) {
return Nan::ThrowTypeError("First argument must be a string");
}
nodeMap->map->getStyle().removeLayer(*Nan::Utf8String(info[0]));
}
void NodeMap::AddImage(const Nan::FunctionCallbackInfo<v8::Value>& info) {
using namespace mln::style;
using namespace mln::style::conversion;
v8::Local<v8::Context> context = info.GetIsolate()->GetCurrentContext();
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
if (info.Length() != 3) {
return Nan::ThrowTypeError("Three arguments required");
}
if (!info[0]->IsString()) {
return Nan::ThrowTypeError("First argument must be a string");
}
if (!info[1]->IsObject()) {
return Nan::ThrowTypeError("Second argument must be an object");
}
if (!info[2]->IsObject()) {
return Nan::ThrowTypeError("Third argument must be an object");
}
auto optionObject = Nan::To<v8::Object>(info[2]).ToLocalChecked();
if (!Nan::Get(optionObject, Nan::New("height").ToLocalChecked()).ToLocalChecked()->IsUint32()) {
return Nan::ThrowTypeError("height parameter required");
}
if (!Nan::Get(optionObject, Nan::New("width").ToLocalChecked()).ToLocalChecked()->IsUint32()) {
return Nan::ThrowTypeError("width parameter required");
}
if (!Nan::Get(optionObject, Nan::New("pixelRatio").ToLocalChecked()).ToLocalChecked()->IsNumber()) {
return Nan::ThrowTypeError("pixelRatio parameter required");
}
uint32_t imageHeight =
Nan::To<uint32_t>(Nan::Get(optionObject, Nan::New("height").ToLocalChecked()).ToLocalChecked()).ToChecked();
uint32_t imageWidth =
Nan::To<uint32_t>(Nan::Get(optionObject, Nan::New("width").ToLocalChecked()).ToLocalChecked()).ToChecked();
if (imageWidth > 1024 || imageHeight > 1024) {
return Nan::ThrowTypeError("Max height and width is 1024");
}
bool sdf = false;
if (Nan::Get(optionObject, Nan::New("sdf").ToLocalChecked()).ToLocalChecked()->IsBoolean()) {
sdf = Nan::To<bool>(Nan::Get(optionObject, Nan::New("sdf").ToLocalChecked()).ToLocalChecked()).ToChecked();
}
float pixelRatio = (float)Nan::To<double>(
Nan::Get(optionObject, Nan::New("pixelRatio").ToLocalChecked()).ToLocalChecked())
.ToChecked();
auto imageBuffer = Nan::To<v8::Object>(info[1]).ToLocalChecked()->ToObject(context).ToLocalChecked();
char* imageDataBuffer = node::Buffer::Data(imageBuffer);
size_t imageLength = node::Buffer::Length(imageBuffer);
if (imageLength != imageHeight * imageWidth * 4) {
return Nan::ThrowTypeError("Image size does not match buffer size");
}
std::unique_ptr<uint8_t[]> data = std::make_unique<uint8_t[]>(imageLength);
std::copy(imageDataBuffer, imageDataBuffer + imageLength, data.get());
mln::UnassociatedImage cImage({imageWidth, imageHeight}, std::move(data));
mln::PremultipliedImage cPremultipliedImage = mln::util::premultiply(std::move(cImage));
nodeMap->map->getStyle().addImage(std::make_unique<mln::style::Image>(
*Nan::Utf8String(info[0]), std::move(cPremultipliedImage), pixelRatio, sdf));
}
void NodeMap::RemoveImage(const Nan::FunctionCallbackInfo<v8::Value>& info) {
using namespace mln::style;
using namespace mln::style::conversion;
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
if (info.Length() != 1) {
return Nan::ThrowTypeError("One argument required");
}
if (!info[0]->IsString()) {
return Nan::ThrowTypeError("First argument must be a string");
}
nodeMap->map->getStyle().removeImage(*Nan::Utf8String(info[0]));
}
void NodeMap::SetLayerZoomRange(const Nan::FunctionCallbackInfo<v8::Value>& info) {
using namespace mln::style;
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
if (info.Length() != 3) {
return Nan::ThrowTypeError("Three arguments required");
}
if (!info[0]->IsString()) {
return Nan::ThrowTypeError("First argument must be a string");
}
if (!info[1]->IsNumber() || !info[2]->IsNumber()) {
return Nan::ThrowTypeError("Second and third arguments must be numbers");
}
mln::style::Layer* layer = nodeMap->map->getStyle().getLayer(*Nan::Utf8String(info[0]));
if (!layer) {
return Nan::ThrowTypeError("layer not found");
}
layer->setMinZoom(static_cast<float>(Nan::To<double>(info[1]).ToChecked()));
layer->setMaxZoom(static_cast<float>(Nan::To<double>(info[2]).ToChecked()));
}
void NodeMap::SetLayerProperty(const Nan::FunctionCallbackInfo<v8::Value>& info) {
using namespace mln::style;
using namespace mln::style::conversion;
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
if (info.Length() < 3) {
return Nan::ThrowTypeError("Three arguments required");
}
if (!info[0]->IsString()) {
return Nan::ThrowTypeError("First argument must be a string");
}
mln::style::Layer* layer = nodeMap->map->getStyle().getLayer(*Nan::Utf8String(info[0]));
if (!layer) {
return Nan::ThrowTypeError("layer not found");
}
if (!info[1]->IsString()) {
return Nan::ThrowTypeError("Second argument must be a string");
}
std::optional<Error> error = layer->setProperty(*Nan::Utf8String(info[1]), Convertible(info[2]));
if (error) {
return Nan::ThrowTypeError(error->message.c_str());
}
info.GetReturnValue().SetUndefined();
}
void NodeMap::SetFilter(const Nan::FunctionCallbackInfo<v8::Value>& info) {
using namespace mln::style;
using namespace mln::style::conversion;
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
if (info.Length() < 2) {
return Nan::ThrowTypeError("Two arguments required");
}
if (!info[0]->IsString()) {
return Nan::ThrowTypeError("First argument must be a string");
}
mln::style::Layer* layer = nodeMap->map->getStyle().getLayer(*Nan::Utf8String(info[0]));
if (!layer) {
return Nan::ThrowTypeError("layer not found");
}
Filter filter;
if (!info[1]->IsNull() && !info[1]->IsUndefined()) {
Error error;
std::optional<Filter> converted = convert<Filter>(info[1], error);
if (!converted) {
Nan::ThrowTypeError(error.message.c_str());
return;
}
filter = std::move(*converted);
}
layer->setFilter(filter);
}
void NodeMap::SetSize(const Nan::FunctionCallbackInfo<v8::Value>& info) {
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(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<v8::Array>();
uint32_t width = 0;
uint32_t height = 0;
if (size->Length() > 0) {
width = Nan::To<uint32_t>(Nan::Get(size, 0).ToLocalChecked()).ToChecked();
}
if (size->Length() > 1) {
height = Nan::To<uint32_t>(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<v8::Value>& info) {
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(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 center = info[0].As<v8::Array>();
double latitude = 0;
double longitude = 0;
if (center->Length() > 0) {
longitude = Nan::To<double>(Nan::Get(center, 0).ToLocalChecked()).ToChecked();
}
if (center->Length() > 1) {
latitude = Nan::To<double>(Nan::Get(center, 1).ToLocalChecked()).ToChecked();
}
try {
nodeMap->map->jumpTo(mln::CameraOptions().withCenter(mln::LatLng{latitude, longitude}));
} catch (const std::exception& ex) {
return Nan::ThrowError(ex.what());
}
info.GetReturnValue().SetUndefined();
}
void NodeMap::SetZoom(const Nan::FunctionCallbackInfo<v8::Value>& info) {
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
if (info.Length() <= 0 || !info[0]->IsNumber()) {
return Nan::ThrowTypeError("First argument must be a number");
}
try {
nodeMap->map->jumpTo(mln::CameraOptions().withZoom(Nan::To<double>(info[0]).ToChecked()));
} catch (const std::exception& ex) {
return Nan::ThrowError(ex.what());
}
info.GetReturnValue().SetUndefined();
}
void NodeMap::SetBearing(const Nan::FunctionCallbackInfo<v8::Value>& info) {
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
if (info.Length() <= 0 || !info[0]->IsNumber()) {
return Nan::ThrowTypeError("First argument must be a number");
}
try {
nodeMap->map->jumpTo(mln::CameraOptions().withBearing(Nan::To<double>(info[0]).ToChecked()));
} catch (const std::exception& ex) {
return Nan::ThrowError(ex.what());
}
info.GetReturnValue().SetUndefined();
}
void NodeMap::SetPitch(const Nan::FunctionCallbackInfo<v8::Value>& info) {
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
if (info.Length() <= 0 || !info[0]->IsNumber()) {
return Nan::ThrowTypeError("First argument must be a number");
}
try {
nodeMap->map->jumpTo(mln::CameraOptions().withPitch(Nan::To<double>(info[0]).ToChecked()));
} catch (const std::exception& ex) {
return Nan::ThrowError(ex.what());
}
info.GetReturnValue().SetUndefined();
}
void NodeMap::SetLight(const Nan::FunctionCallbackInfo<v8::Value>& info) {
using namespace mln::style;
using namespace mln::style::conversion;
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
if (info.Length() <= 0 || !info[0]->IsObject()) {
return Nan::ThrowTypeError("First argument must be an object");
}
try {
Error conversionError;
if (auto light = convert<mln::style::Light>(info[0], conversionError)) {
nodeMap->map->getStyle().setLight(std::make_unique<Light>(*light));
} else {
return Nan::ThrowTypeError(conversionError.message.c_str());
}
} catch (const std::exception& ex) {
return Nan::ThrowError(ex.what());
}
info.GetReturnValue().SetUndefined();
}
void NodeMap::SetAxonometric(const Nan::FunctionCallbackInfo<v8::Value>& info) {
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
if (info.Length() <= 0 || !info[0]->IsBoolean()) {
return Nan::ThrowTypeError("First argument must be a boolean");
}
try {
nodeMap->map->setProjectionMode(mln::ProjectionMode().withAxonometric(Nan::To<bool>(info[0]).ToChecked()));
} catch (const std::exception& ex) {
return Nan::ThrowError(ex.what());
}
info.GetReturnValue().SetUndefined();
}
void NodeMap::SetXSkew(const Nan::FunctionCallbackInfo<v8::Value>& info) {
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
if (info.Length() <= 0 || !info[0]->IsNumber()) {
return Nan::ThrowTypeError("First argument must be a number");
}
try {
nodeMap->map->setProjectionMode(mln::ProjectionMode().withXSkew(Nan::To<double>(info[0]).ToChecked()));
} catch (const std::exception& ex) {
return Nan::ThrowError(ex.what());
}
info.GetReturnValue().SetUndefined();
}
void NodeMap::SetYSkew(const Nan::FunctionCallbackInfo<v8::Value>& info) {
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
if (info.Length() <= 0 || !info[0]->IsNumber()) {
return Nan::ThrowTypeError("First argument must be a number");
}
try {
nodeMap->map->setProjectionMode(mln::ProjectionMode().withYSkew(Nan::To<double>(info[0]).ToChecked()));
} catch (const std::exception& ex) {
return Nan::ThrowError(ex.what());
}
info.GetReturnValue().SetUndefined();
}
void NodeMap::SetFeatureState(const Nan::FunctionCallbackInfo<v8::Value>& info) {
using namespace mln;
using namespace mln::style::conversion;
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
if (info.Length() < 2) {
return Nan::ThrowTypeError("Two arguments required");
}
if (!info[0]->IsObject() || !info[1]->IsObject()) {
return Nan::ThrowTypeError("Both arguments must be objects");
}
std::string sourceID;
std::string featureID;
std::optional<std::string> sourceLayerID;
auto feature = Nan::To<v8::Object>(info[0]).ToLocalChecked();
if (Nan::Has(feature, Nan::New("source").ToLocalChecked()).FromJust()) {
auto sourceOption = Nan::Get(feature, Nan::New("source").ToLocalChecked()).ToLocalChecked();
if (!sourceOption->IsString()) {
return Nan::ThrowTypeError("Requires feature.source property to be a string");
}
sourceID = *Nan::Utf8String(sourceOption);
} else {
return Nan::ThrowTypeError("SetFeatureState: Requires feature.source property");
}
if (Nan::Has(feature, Nan::New("sourceLayer").ToLocalChecked()).FromJust()) {
auto sourceLayerOption = Nan::Get(feature, Nan::New("sourceLayer").ToLocalChecked()).ToLocalChecked();
if (!sourceLayerOption->IsString()) {
return Nan::ThrowTypeError(
"SetFeatureState: Requires feature.sourceLayer property to be "
"a string");
}
sourceLayerID = {*Nan::Utf8String(sourceLayerOption)};
}
if (Nan::Has(feature, Nan::New("id").ToLocalChecked()).FromJust()) {
auto idOption = Nan::Get(feature, Nan::New("id").ToLocalChecked()).ToLocalChecked();
if (!idOption->IsString() && !(idOption->IsNumber() || idOption->IsString())) {
return Nan::ThrowTypeError("Requires feature.id property to be a string or a number");
}
featureID = *Nan::Utf8String(idOption);
} else {
return Nan::ThrowTypeError("SetFeatureState: Requires feature.id property");
}
Convertible state(info[1]);
if (!isObject(state)) {
return Nan::ThrowTypeError("Feature state must be an object");
}
std::string sourceLayer = sourceLayerID.value_or(std::string());
std::string stateKey;
Value stateValue;
bool valueParsed = false;
FeatureState newState;
const std::function<std::optional<Error>(const std::string&, const Convertible&)> convertFn =
[&](const std::string& k, const Convertible& v) -> std::optional<Error> {
std::optional<Value> value = toValue(v);
if (value) {
stateValue = std::move(*value);
valueParsed = true;
} else if (isArray(v)) {
std::vector<Value> array;
std::size_t length = arrayLength(v);
array.reserve(length);
for (size_t i = 0; i < length; ++i) {
std::optional<Value> arrayVal = toValue(arrayMember(v, i));
if (arrayVal) {
array.emplace_back(*arrayVal);
}
}
std::unordered_map<std::string, Value> result;
result[k] = std::move(array);
stateValue = std::move(result);
valueParsed = true;
return {};
} else if (isObject(v)) {
eachMember(v, convertFn);
}
if (!valueParsed) {
Nan::ThrowTypeError("Could not get feature state value");
return std::nullopt;
}
stateKey = k;
newState[stateKey] = stateValue;
return std::nullopt;
};
eachMember(state, convertFn);
try {
nodeMap->frontend->getRenderer()->setFeatureState(sourceID, sourceLayerID, featureID, newState);
} catch (const std::exception& ex) {
return Nan::ThrowError(ex.what());
}
info.GetReturnValue().SetUndefined();
}
void NodeMap::GetFeatureState(const Nan::FunctionCallbackInfo<v8::Value>& info) {
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
if (info.Length() < 1) {
return Nan::ThrowTypeError("One argument required");
}
if (!info[0]->IsObject() || !info[1]->IsObject()) {
return Nan::ThrowTypeError("Argument must be object");
}
std::string sourceID;
std::string featureID;
std::optional<std::string> sourceLayerID;
auto feature = Nan::To<v8::Object>(info[0]).ToLocalChecked();
if (Nan::Has(feature, Nan::New("source").ToLocalChecked()).FromJust()) {
auto sourceOption = Nan::Get(feature, Nan::New("source").ToLocalChecked()).ToLocalChecked();
if (!sourceOption->IsString()) {
return Nan::ThrowTypeError("Requires feature.source property to be a string");
}
sourceID = *Nan::Utf8String(sourceOption);
} else {
return Nan::ThrowTypeError("GetFeatureState: Requires feature.source property");
}
if (Nan::Has(feature, Nan::New("sourceLayer").ToLocalChecked()).FromJust()) {
auto sourceLayerOption = Nan::Get(feature, Nan::New("sourceLayer").ToLocalChecked()).ToLocalChecked();
if (!sourceLayerOption->IsString()) {
return Nan::ThrowTypeError(
"GetFeatureState: Requires feature.sourceLayer property to be "
"a string");
}
sourceLayerID = {*Nan::Utf8String(sourceLayerOption)};
}
if (Nan::Has(feature, Nan::New("id").ToLocalChecked()).FromJust()) {
auto idOption = Nan::Get(feature, Nan::New("id").ToLocalChecked()).ToLocalChecked();
if (!idOption->IsString() && !(idOption->IsNumber() || idOption->IsString())) {
return Nan::ThrowTypeError("Requires feature.id property to be a string or a number");
}
featureID = *Nan::Utf8String(idOption);
} else {
return Nan::ThrowTypeError("GetFeatureState: Requires feature.id property");
}
mln::FeatureState state;
try {
nodeMap->frontend->getRenderer()->getFeatureState(state, sourceID, sourceLayerID, featureID);
} catch (const std::exception& ex) {
return Nan::ThrowError(ex.what());
}
info.GetReturnValue().SetUndefined();
}
void NodeMap::RemoveFeatureState(const Nan::FunctionCallbackInfo<v8::Value>& info) {
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
if (info.Length() < 1) {
return Nan::ThrowTypeError("At least one argument required");
}
if (!info[0]->IsObject()) {
return Nan::ThrowTypeError("Argument 1 must be object");
}
if (info.Length() == 2 && !info[1]->IsString()) {
return Nan::ThrowTypeError("argument 2 must be string");
}
std::string sourceID;
std::optional<std::string> sourceLayerID;
std::optional<std::string> featureID;
std::optional<std::string> stateKey;
auto feature = Nan::To<v8::Object>(info[0]).ToLocalChecked();
if (Nan::Has(feature, Nan::New("source").ToLocalChecked()).FromJust()) {
auto sourceOption = Nan::Get(feature, Nan::New("source").ToLocalChecked()).ToLocalChecked();
if (!sourceOption->IsString()) {
return Nan::ThrowTypeError("Requires feature.source property to be a string");
}
sourceID = *Nan::Utf8String(sourceOption);
} else {
return Nan::ThrowTypeError("RemoveFeatureState: Requires feature.source property");
}
if (Nan::Has(feature, Nan::New("sourceLayer").ToLocalChecked()).FromJust()) {
auto sourceLayerOption = Nan::Get(feature, Nan::New("sourceLayer").ToLocalChecked()).ToLocalChecked();
if (!sourceLayerOption->IsString()) {
return Nan::ThrowTypeError(
"RemoveFeatureState: Requires feature.sourceLayer property to "
"be a string");
}
sourceLayerID = {*Nan::Utf8String(sourceLayerOption)};
}
if (Nan::Has(feature, Nan::New("id").ToLocalChecked()).FromJust()) {
auto idOption = Nan::Get(feature, Nan::New("id").ToLocalChecked()).ToLocalChecked();
if (!idOption->IsString() && !(idOption->IsNumber() || idOption->IsString())) {
return Nan::ThrowTypeError("Requires feature.id property to be a string or a number");
}
featureID = {*Nan::Utf8String(idOption)};
}
if (info.Length() == 2) {
auto keyParam = Nan::To<v8::String>(info[1]).ToLocalChecked();
if (!keyParam->IsString()) {
return Nan::ThrowTypeError(
"RemoveFeatureState: Requires feature key property to be a "
"string");
}
stateKey = {*Nan::Utf8String(keyParam)};
}
try {
nodeMap->frontend->getRenderer()->removeFeatureState(sourceID, sourceLayerID, featureID, stateKey);
} catch (const std::exception& ex) {
return Nan::ThrowError(ex.what());
}
info.GetReturnValue().SetUndefined();
}
void NodeMap::DumpDebugLogs(const Nan::FunctionCallbackInfo<v8::Value>& info) {
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(info.Holder());
if (!nodeMap->map) return Nan::ThrowError(releasedMessage());
nodeMap->map->dumpDebugLogs();
nodeMap->frontend->getRenderer()->dumpDebugLogs();
info.GetReturnValue().SetUndefined();
}
void NodeMap::QueryRenderedFeatures(const Nan::FunctionCallbackInfo<v8::Value>& info) {
using namespace mln::style;
using namespace mln::style::conversion;
auto nodeMap = Nan::ObjectWrap::Unwrap<NodeMap>(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 posOrBox = info[0].As<v8::Array>();
if (posOrBox->Length() != 2) {
return Nan::ThrowTypeError("First argument must have two components");
}
mln::RenderedQueryOptions queryOptions;
if (!info[1]->IsNull() && !info[1]->IsUndefined()) {
if (!info[1]->IsObject()) {
return Nan::ThrowTypeError("options argument must be an object");
}
auto options = Nan::To<v8::Object>(info[1]).ToLocalChecked();
// Check if layers is set. If provided, it must be an array of strings
if (Nan::Has(options, Nan::New("layers").ToLocalChecked()).FromJust()) {
auto layersOption = Nan::Get(options, Nan::New("layers").ToLocalChecked()).ToLocalChecked();
if (!layersOption->IsArray()) {
return Nan::ThrowTypeError("Requires options.layers property to be an array");
}
auto layers = layersOption.As<v8::Array>();
std::vector<std::string> layersVec;
for (uint32_t i = 0; i < layers->Length(); i++) {
layersVec.emplace_back(*Nan::Utf8String(Nan::Get(layers, i).ToLocalChecked()));
}
queryOptions.layerIDs = layersVec;
}
// Check if filter is provided. If set it must be a valid Filter object
if (Nan::Has(options, Nan::New("filter").ToLocalChecked()).FromJust()) {
auto filterOption = Nan::Get(options, Nan::New("filter").ToLocalChecked()).ToLocalChecked();
Error error;
std::optional<Filter> converted = convert<Filter>(filterOption, error);
if (!converted) {
return Nan::ThrowTypeError(error.message.c_str());
}
queryOptions.filter = std::move(*converted);
}
}
try {
std::vector<mln::Feature> optional;
if (Nan::Get(posOrBox, 0).ToLocalChecked()->IsArray()) {
auto pos0 = Nan::Get(posOrBox, 0).ToLocalChecked().As<v8::Array>();
auto pos1 = Nan::Get(posOrBox, 1).ToLocalChecked().As<v8::Array>();
optional = nodeMap->frontend->getRenderer()->queryRenderedFeatures(
mln::ScreenBox{{Nan::To<double>(Nan::Get(pos0, 0).ToLocalChecked()).ToChecked(),
Nan::To<double>(Nan::Get(pos0, 1).ToLocalChecked()).ToChecked()},
{Nan::To<double>(Nan::Get(pos1, 0).ToLocalChecked()).ToChecked(),
Nan::To<double>(Nan::Get(pos1, 1).ToLocalChecked()).ToChecked()}},
queryOptions);
} else {
optional = nodeMap->frontend->getRenderer()->queryRenderedFeatures(
mln::ScreenCoordinate{Nan::To<double>(Nan::Get(posOrBox, 0).ToLocalChecked()).ToChecked(),
Nan::To<double>(Nan::Get(posOrBox, 1).ToLocalChecked()).ToChecked()},
queryOptions);
}
auto array = Nan::New<v8::Array>();
for (std::size_t i = 0; i < optional.size(); i++) {
Nan::Set(array, static_cast<uint32_t>(i), toJS(optional[i]));
}
info.GetReturnValue().Set(array);
} catch (const std::exception& ex) {
return Nan::ThrowError(ex.what());
}
}
NodeMap::NodeMap(v8::Local<v8::Object> options)
: pixelRatio([&] {
Nan::HandleScope scope;
return Nan::Has(options, Nan::New("ratio").ToLocalChecked()).FromJust()
? static_cast<float>(
Nan::To<double>(Nan::Get(options, Nan::New("ratio").ToLocalChecked()).ToLocalChecked())
.ToChecked())
: 1.0f;
}()),
mode([&] {
Nan::HandleScope scope;
if (std::string(*v8::String::Utf8Value(
v8::Isolate::GetCurrent(), Nan::Get(options, Nan::New("mode").ToLocalChecked()).ToLocalChecked())) ==
"tile") {
return mln::MapMode::Tile;
} else {
return mln::MapMode::Static;
}
}()),
crossSourceCollisions([&] {
Nan::HandleScope scope;
return Nan::Has(options, Nan::New("crossSourceCollisions").ToLocalChecked()).FromJust()
? Nan::To<bool>(
Nan::Get(options, Nan::New("crossSourceCollisions").ToLocalChecked()).ToLocalChecked())
.ToChecked()
: true;
}()),
mapObserver(NodeMapObserver()),
frontend(std::make_unique<mln::HeadlessFrontend>(mln::Size{512, 512}, pixelRatio)),
map(std::make_unique<mln::Map>(*frontend,
mapObserver,
mln::MapOptions()
.withSize(frontend->getSize())
.withPixelRatio(pixelRatio)
.withMapMode(mode)
.withCrossSourceCollisions(crossSourceCollisions),
mln::ResourceOptions().withPlatformContext(reinterpret_cast<void*>(this)),
mln::ClientOptions())),
async(new uv_async_t) {
async->data = this;
uv_async_init(
uv_default_loop(), async, [](uv_async_t* h) { reinterpret_cast<NodeMap*>(h->data)->renderFinished(); });
// Make sure the async handle doesn't keep the loop alive.
uv_unref(reinterpret_cast<uv_handle_t*>(async));
}
NodeMap::~NodeMap() {
try {
if (map) release();
} catch (...) {
mln::Log::Error(mln::Event::General, "Error release the map object when destroying NodeMap");
}
}
std::unique_ptr<mln::AsyncRequest> NodeFileSource::request(const mln::Resource& resource,
mln::FileSource::Callback callback_) {
assert(nodeMap);
Nan::HandleScope scope;
// Because this method may be called while this NodeMap is already eligible
// for garbage collection, we need to explicitly hold onto our own handle
// here so that GC during a v8 call doesn't destroy *this while we're still
// executing code.
nodeMap->handle();
auto asyncRequest = std::make_unique<node_mbgl::NodeAsyncRequest>();
v8::Local<v8::Value> argv[] = {Nan::New<v8::External>(nodeMap),
Nan::New<v8::External>(&callback_),
Nan::New<v8::External>(asyncRequest.get()),
Nan::New(resource.url).ToLocalChecked(),
Nan::New<v8::Integer>(resource.kind)};
Nan::NewInstance(Nan::New(node_mbgl::NodeRequest::constructor), 5, argv).ToLocalChecked();
return asyncRequest;
}
bool NodeFileSource::canRequest(const mln::Resource&) const {
return true;
}
void NodeFileSource::setResourceOptions(mln::ResourceOptions options) {
this->_resourceOptions = std::move(options);
}
mln::ResourceOptions NodeFileSource::getResourceOptions() {
return this->_resourceOptions.clone();
}
void NodeFileSource::setClientOptions(mln::ClientOptions options) {
this->_clientOptions = std::move(options);
}
mln::ClientOptions NodeFileSource::getClientOptions() {
return this->_clientOptions.clone();
}
} // namespace node_mbgl