#include "node_map.hpp" #include "node_request.hpp" #include "node_feature.hpp" #include "node_conversion.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace node_mbgl { struct NodeMap::RenderOptions { double zoom = 0; double bearing = 0; mbgl::style::Light light; double pitch = 0; double latitude = 0; double longitude = 0; mbgl::Size size = { 512, 512 }; bool axonometric = false; double xSkew = 0; double ySkew = 1; std::vector classes; mbgl::MapDebugOptions debugOptions = mbgl::MapDebugOptions::NoDebug; }; Nan::Persistent NodeMap::constructor; Nan::Persistent NodeMap::parseError; static const char* releasedMessage() { return "Map resources have already been released"; } void NodeMapObserver::onDidFailLoadingMap(std::exception_ptr error) { std::rethrow_exception(error); } void NodeMap::Init(v8::Local target) { // Define a custom error class for parse errors auto script = Nan::New(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(Nan::RunScript(script).ToLocalChecked()).ToLocalChecked()); Nan::Set(target, Nan::New("ParseError").ToLocalChecked(), Nan::New(parseError)); v8::Local tpl = Nan::New(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", SetLayoutProperty); Nan::SetPrototypeMethod(tpl, "setPaintProperty", SetPaintProperty); Nan::SetPrototypeMethod(tpl, "setFilter", SetFilter); 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, "dumpDebugLogs", DumpDebugLogs); Nan::SetPrototypeMethod(tpl, "queryRenderedFeatures", QueryRenderedFeatures); constructor.Reset(tpl->GetFunction()); Nan::Set(target, Nan::New("Map").ToLocalChecked(), tpl->GetFunction()); } /** * 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& info) { if (!info.IsConstructCall()) { return Nan::ThrowTypeError("Use the new operator to create new Map objects"); } if (info.Length() < 1 || !info[0]->IsObject()) { return Nan::ThrowTypeError("Requires an options object as first argument"); } auto options = Nan::To(info[0]).ToLocalChecked(); // Check that 'request' is set. 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("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); 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& info) { auto nodeMap = Nan::ObjectWrap::Unwrap(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()).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 mbgl::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(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::Get(options, Nan::New("cameraMutated").ToLocalChecked()).ToLocalChecked()->BooleanValue()) { nodeMap->map->jumpTo(nodeMap->map->getStyle().getDefaultCamera()); } } } nodeMap->loaded = true; info.GetReturnValue().SetUndefined(); } void NodeMap::Loaded(const Nan::FunctionCallbackInfo& info) { auto nodeMap = Nan::ObjectWrap::Unwrap(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 obj) { Nan::HandleScope scope; NodeMap::RenderOptions options; if (Nan::Has(obj, Nan::New("zoom").ToLocalChecked()).FromJust()) { options.zoom = Nan::Get(obj, Nan::New("zoom").ToLocalChecked()).ToLocalChecked()->NumberValue(); } if (Nan::Has(obj, Nan::New("bearing").ToLocalChecked()).FromJust()) { options.bearing = Nan::Get(obj, Nan::New("bearing").ToLocalChecked()).ToLocalChecked()->NumberValue(); } if (Nan::Has(obj, Nan::New("pitch").ToLocalChecked()).FromJust()) { options.pitch = Nan::Get(obj, Nan::New("pitch").ToLocalChecked()).ToLocalChecked()->NumberValue(); } if (Nan::Has(obj, Nan::New("light").ToLocalChecked()).FromJust()) { auto lightObj = Nan::Get(obj, Nan::New("light").ToLocalChecked()).ToLocalChecked(); mbgl::style::conversion::Error conversionError; if (auto light = mbgl::style::conversion::convert(lightObj, conversionError)) { options.light = *light; } else { throw conversionError; } } if (Nan::Has(obj, Nan::New("axonometric").ToLocalChecked()).FromJust()) { options.axonometric = Nan::Get(obj, Nan::New("axonometric").ToLocalChecked()).ToLocalChecked()->BooleanValue(); } 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(); if (skew->Length() > 0) { options.xSkew = Nan::Get(skew, 0).ToLocalChecked()->NumberValue(); } if (skew->Length() > 1) { options.ySkew = Nan::Get(skew, 1).ToLocalChecked()->NumberValue(); } } } 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(); if (center->Length() > 0) { options.longitude = Nan::Get(center, 0).ToLocalChecked()->NumberValue(); } if (center->Length() > 1) { options.latitude = Nan::Get(center, 1).ToLocalChecked()->NumberValue(); } } } if (Nan::Has(obj, Nan::New("width").ToLocalChecked()).FromJust()) { options.size.width = Nan::Get(obj, Nan::New("width").ToLocalChecked()).ToLocalChecked()->IntegerValue(); } if (Nan::Has(obj, Nan::New("height").ToLocalChecked()).FromJust()) { options.size.height = Nan::Get(obj, Nan::New("height").ToLocalChecked()).ToLocalChecked()->IntegerValue(); } if (Nan::Has(obj, Nan::New("classes").ToLocalChecked()).FromJust()) { auto classes = Nan::To(Nan::Get(obj, Nan::New("classes").ToLocalChecked()).ToLocalChecked()).ToLocalChecked().As(); 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(Nan::Get(classes, i).ToLocalChecked()).ToLocalChecked()) }); } } if (Nan::Has(obj, Nan::New("debug").ToLocalChecked()).FromJust()) { auto debug = Nan::To(Nan::Get(obj, Nan::New("debug").ToLocalChecked()).ToLocalChecked()).ToLocalChecked(); if (Nan::Has(debug, Nan::New("tileBorders").ToLocalChecked()).FromJust()) { if (Nan::Get(debug, Nan::New("tileBorders").ToLocalChecked()).ToLocalChecked()->BooleanValue()) { options.debugOptions = options.debugOptions | mbgl::MapDebugOptions::TileBorders; } } if (Nan::Has(debug, Nan::New("parseStatus").ToLocalChecked()).FromJust()) { if (Nan::Get(debug, Nan::New("parseStatus").ToLocalChecked()).ToLocalChecked()->BooleanValue()) { options.debugOptions = options.debugOptions | mbgl::MapDebugOptions::ParseStatus; } } if (Nan::Has(debug, Nan::New("timestamps").ToLocalChecked()).FromJust()) { if (Nan::Get(debug, Nan::New("timestamps").ToLocalChecked()).ToLocalChecked()->BooleanValue()) { options.debugOptions = options.debugOptions | mbgl::MapDebugOptions::Timestamps; } } if (Nan::Has(debug, Nan::New("collision").ToLocalChecked()).FromJust()) { if (Nan::Get(debug, Nan::New("collision").ToLocalChecked()).ToLocalChecked()->BooleanValue()) { options.debugOptions = options.debugOptions | mbgl::MapDebugOptions::Collision; } } if (Nan::Has(debug, Nan::New("overdraw").ToLocalChecked()).FromJust()) { if (Nan::Get(debug, Nan::New("overdraw").ToLocalChecked()).ToLocalChecked()->BooleanValue()) { options.debugOptions = mbgl::MapDebugOptions::Overdraw; } } } return options; } class RenderRequest : public Nan::AsyncResource { public: RenderRequest(v8::Local callback_) : AsyncResource("mbgl:RenderRequest") { callback.Reset(callback_); } ~RenderRequest() { callback.Reset(); } Nan::Persistent 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} [options.center=[0,0]] longitude, latitude center * of the map * @param {number} [options.bearing=0] rotation * @param {Array} [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& 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() <= 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 { 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(std::move(options)); } catch (const mbgl::style::conversion::Error& err) { return Nan::ThrowTypeError(err.message.c_str()); } catch (const mbgl::util::StyleParseException& ex) { return Nan::ThrowError(ParseError(ex.what())); } catch (const mbgl::util::Exception &ex) { return Nan::ThrowError(ex.what()); } info.GetReturnValue().SetUndefined(); } void NodeMap::startRender(NodeMap::RenderOptions options) { frontend->setSize(options.size); map->setSize(options.size); mbgl::CameraOptions camera; camera.center = mbgl::LatLng { options.latitude, options.longitude }; camera.zoom = options.zoom; camera.bearing = options.bearing; camera.pitch = options.pitch; auto projectionOptions = mbgl::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 = std::move(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)); } v8::Local NodeMap::ParseError(const char* msg) { v8::Local 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(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 callback = Nan::New(request->callback); v8::Local target = Nan::New(); if (error) { v8::Local err; try { std::rethrow_exception(error); assert(false); } catch (const mbgl::util::StyleParseException& ex) { err = ParseError(ex.what()); } catch (const std::exception& ex) { err = Nan::Error(ex.what()); } v8::Local 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 pixels = Nan::NewBuffer( reinterpret_cast(img.data.get()), img.bytes(), // Retain the data until the buffer is deleted. [](char *, void * hint) { delete [] reinterpret_cast(hint); }, img.data.get() ).ToLocalChecked(); img.data.release(); v8::Local argv[] = { Nan::Null(), pixels }; request->runInAsyncScope(target, callback, 2, argv); } else { v8::Local 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& info) { auto nodeMap = Nan::ObjectWrap::Unwrap(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 mbgl::util::Exception(releasedMessage()); uv_close(reinterpret_cast(async), [] (uv_handle_t *h) { delete reinterpret_cast(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& info) { auto nodeMap = Nan::ObjectWrap::Unwrap(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(async)); uv_close(reinterpret_cast(async), [] (uv_handle_t *h) { delete reinterpret_cast(h); }); async = new uv_async_t; async->data = this; uv_async_init(uv_default_loop(), async, [](uv_async_t* h) { reinterpret_cast(h->data)->renderFinished(); }); frontend = std::make_unique(mbgl::Size{ 256, 256 }, pixelRatio, *this, threadpool); map = std::make_unique(*frontend, mapObserver, frontend->getSize(), pixelRatio, *this, threadpool, mode, mbgl::ConstrainMode::HeightOnly, mbgl::ViewportMode::Default, crossSourceCollisions); // 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& info) { using namespace mbgl::style; using namespace mbgl::style::conversion; auto nodeMap = Nan::ObjectWrap::Unwrap(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; mbgl::optional> source = convert>(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& info) { using namespace mbgl::style; using namespace mbgl::style::conversion; auto nodeMap = Nan::ObjectWrap::Unwrap(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& info) { using namespace mbgl::style; using namespace mbgl::style::conversion; auto nodeMap = Nan::ObjectWrap::Unwrap(info.Holder()); if (!nodeMap->map) return Nan::ThrowError(releasedMessage()); if (info.Length() != 1) { return Nan::ThrowTypeError("One argument required"); } Error error; mbgl::optional> layer = convert>(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& info) { using namespace mbgl::style; using namespace mbgl::style::conversion; auto nodeMap = Nan::ObjectWrap::Unwrap(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& info) { using namespace mbgl::style; using namespace mbgl::style::conversion; auto nodeMap = Nan::ObjectWrap::Unwrap(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(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::Get(optionObject, Nan::New("height").ToLocalChecked()).ToLocalChecked()->Uint32Value(); uint32_t imageWidth = Nan::Get(optionObject, Nan::New("width").ToLocalChecked()).ToLocalChecked()->Uint32Value(); if (imageWidth > 1024 || imageHeight > 1024) { return Nan::ThrowTypeError("Max height and width is 1024"); } float pixelRatio = Nan::Get(optionObject, Nan::New("pixelRatio").ToLocalChecked()).ToLocalChecked()->NumberValue(); auto imageBuffer = Nan::To(info[1]).ToLocalChecked()->ToObject(); 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 data = std::make_unique(imageLength); std::copy(imageDataBuffer, imageDataBuffer + imageLength, data.get()); mbgl::UnassociatedImage cImage({ imageWidth, imageHeight}, std::move(data)); mbgl::PremultipliedImage cPremultipliedImage = mbgl::util::premultiply(std::move(cImage)); nodeMap->map->getStyle().addImage(std::make_unique(*Nan::Utf8String(info[0]), std::move(cPremultipliedImage), pixelRatio)); } void NodeMap::RemoveImage(const Nan::FunctionCallbackInfo& info) { using namespace mbgl::style; using namespace mbgl::style::conversion; auto nodeMap = Nan::ObjectWrap::Unwrap(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& info) { using namespace mbgl::style; auto nodeMap = Nan::ObjectWrap::Unwrap(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"); } mbgl::style::Layer* layer = nodeMap->map->getStyle().getLayer(*Nan::Utf8String(info[0])); if (!layer) { return Nan::ThrowTypeError("layer not found"); } layer->setMinZoom(info[1]->NumberValue()); layer->setMaxZoom(info[2]->NumberValue()); } void NodeMap::SetLayoutProperty(const Nan::FunctionCallbackInfo& info) { using namespace mbgl::style; using namespace mbgl::style::conversion; auto nodeMap = Nan::ObjectWrap::Unwrap(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"); } mbgl::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"); } mbgl::optional error = layer->setLayoutProperty(*Nan::Utf8String(info[1]), Convertible(info[2])); if (error) { return Nan::ThrowTypeError(error->message.c_str()); } info.GetReturnValue().SetUndefined(); } void NodeMap::SetPaintProperty(const Nan::FunctionCallbackInfo& info) { using namespace mbgl::style; using namespace mbgl::style::conversion; auto nodeMap = Nan::ObjectWrap::Unwrap(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"); } mbgl::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"); } mbgl::optional error = layer->setPaintProperty(*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& info) { using namespace mbgl::style; using namespace mbgl::style::conversion; auto nodeMap = Nan::ObjectWrap::Unwrap(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"); } mbgl::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; mbgl::optional converted = convert(info[1], error); if (!converted) { Nan::ThrowTypeError(error.message.c_str()); return; } filter = std::move(*converted); } layer->setFilter(filter); } void NodeMap::SetCenter(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 center = info[0].As(); double latitude = 0; double longitude = 0; if (center->Length() > 0) { longitude = Nan::Get(center, 0).ToLocalChecked()->NumberValue(); } if (center->Length() > 1) { latitude = Nan::Get(center, 1).ToLocalChecked()->NumberValue(); } try { nodeMap->map->jumpTo(mbgl::CameraOptions().withCenter(mbgl::LatLng { latitude, longitude })); } catch (const std::exception &ex) { return Nan::ThrowError(ex.what()); } info.GetReturnValue().SetUndefined(); } void NodeMap::SetZoom(const Nan::FunctionCallbackInfo& info) { auto nodeMap = Nan::ObjectWrap::Unwrap(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(mbgl::CameraOptions().withZoom(info[0]->NumberValue())); } catch (const std::exception &ex) { return Nan::ThrowError(ex.what()); } info.GetReturnValue().SetUndefined(); } void NodeMap::SetBearing(const Nan::FunctionCallbackInfo& info) { auto nodeMap = Nan::ObjectWrap::Unwrap(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(mbgl::CameraOptions().withBearing(info[0]->NumberValue())); } catch (const std::exception &ex) { return Nan::ThrowError(ex.what()); } info.GetReturnValue().SetUndefined(); } void NodeMap::SetPitch(const Nan::FunctionCallbackInfo& info) { auto nodeMap = Nan::ObjectWrap::Unwrap(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(mbgl::CameraOptions().withPitch(info[0]->NumberValue())); } catch (const std::exception &ex) { return Nan::ThrowError(ex.what()); } info.GetReturnValue().SetUndefined(); } void NodeMap::SetLight(const Nan::FunctionCallbackInfo& info) { using namespace mbgl::style; using namespace mbgl::style::conversion; 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 object"); } try { Error conversionError; if (auto light = convert(info[0], conversionError)) { nodeMap->map->getStyle().setLight(std::make_unique(*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& info) { auto nodeMap = Nan::ObjectWrap::Unwrap(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(mbgl::ProjectionMode() .withAxonometric(info[0]->BooleanValue())); } catch (const std::exception &ex) { return Nan::ThrowError(ex.what()); } info.GetReturnValue().SetUndefined(); } void NodeMap::SetXSkew(const Nan::FunctionCallbackInfo& info) { auto nodeMap = Nan::ObjectWrap::Unwrap(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(mbgl::ProjectionMode() .withXSkew(info[0]->NumberValue())); } catch (const std::exception &ex) { return Nan::ThrowError(ex.what()); } info.GetReturnValue().SetUndefined(); } void NodeMap::SetYSkew(const Nan::FunctionCallbackInfo& info) { auto nodeMap = Nan::ObjectWrap::Unwrap(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(mbgl::ProjectionMode() .withYSkew(info[0]->NumberValue())); } catch (const std::exception &ex) { return Nan::ThrowError(ex.what()); } info.GetReturnValue().SetUndefined(); } void NodeMap::DumpDebugLogs(const Nan::FunctionCallbackInfo& info) { auto nodeMap = Nan::ObjectWrap::Unwrap(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& info) { using namespace mbgl::style; using namespace mbgl::style::conversion; 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 posOrBox = info[0].As(); if (posOrBox->Length() != 2) { return Nan::ThrowTypeError("First argument must have two components"); } mbgl::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(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(); std::vector 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; mbgl::optional converted = convert(filterOption, error); if (!converted) { return Nan::ThrowTypeError(error.message.c_str()); } queryOptions.filter = std::move(*converted); } } try { std::vector optional; if (Nan::Get(posOrBox, 0).ToLocalChecked()->IsArray()) { auto pos0 = Nan::Get(posOrBox, 0).ToLocalChecked().As(); auto pos1 = Nan::Get(posOrBox, 1).ToLocalChecked().As(); optional = nodeMap->frontend->getRenderer()->queryRenderedFeatures(mbgl::ScreenBox { { Nan::Get(pos0, 0).ToLocalChecked()->NumberValue(), Nan::Get(pos0, 1).ToLocalChecked()->NumberValue() }, { Nan::Get(pos1, 0).ToLocalChecked()->NumberValue(), Nan::Get(pos1, 1).ToLocalChecked()->NumberValue() } }, queryOptions); } else { optional = nodeMap->frontend->getRenderer()->queryRenderedFeatures(mbgl::ScreenCoordinate { Nan::Get(posOrBox, 0).ToLocalChecked()->NumberValue(), Nan::Get(posOrBox, 1).ToLocalChecked()->NumberValue() }, queryOptions); } auto array = Nan::New(); for (unsigned int i = 0; i < optional.size(); i++) { array->Set(i, toJS(optional[i])); } info.GetReturnValue().Set(array); } catch (const std::exception &ex) { return Nan::ThrowError(ex.what()); } } NodeMap::NodeMap(v8::Local options) : pixelRatio([&] { Nan::HandleScope scope; return Nan::Has(options, Nan::New("ratio").ToLocalChecked()).FromJust() ? Nan::Get(options, Nan::New("ratio").ToLocalChecked()) .ToLocalChecked() ->NumberValue() : 1.0; }()) , mode([&] { Nan::HandleScope scope; if (Nan::Has(options, Nan::New("mode").ToLocalChecked()).FromJust() && std::string(*v8::String::Utf8Value(Nan::Get(options, Nan::New("mode").ToLocalChecked()).ToLocalChecked()->ToString())) == "tile") { return mbgl::MapMode::Tile; } else { return mbgl::MapMode::Static; } }()) , crossSourceCollisions([&] { Nan::HandleScope scope; return Nan::Has(options, Nan::New("crossSourceCollisions").ToLocalChecked()).FromJust() ? Nan::Get(options, Nan::New("crossSourceCollisions").ToLocalChecked()) .ToLocalChecked() ->BooleanValue() : true; }()) , mapObserver(NodeMapObserver()) , frontend(std::make_unique(mbgl::Size { 256, 256 }, pixelRatio, *this, threadpool)) , map(std::make_unique(*frontend, mapObserver, frontend->getSize(), pixelRatio, *this, threadpool, mode, mbgl::ConstrainMode::HeightOnly, mbgl::ViewportMode::Default, crossSourceCollisions)), async(new uv_async_t) { async->data = this; uv_async_init(uv_default_loop(), async, [](uv_async_t* h) { reinterpret_cast(h->data)->renderFinished(); }); // Make sure the async handle doesn't keep the loop alive. uv_unref(reinterpret_cast(async)); } NodeMap::~NodeMap() { if (map) release(); } std::unique_ptr NodeMap::request(const mbgl::Resource& resource, mbgl::FileSource::Callback callback_) { 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. handle(); v8::Local argv[] = { Nan::New(this), Nan::New(&callback_) }; auto instance = Nan::NewInstance(Nan::New(NodeRequest::constructor), 2, argv).ToLocalChecked(); Nan::Set(instance, Nan::New("url").ToLocalChecked(), Nan::New(resource.url).ToLocalChecked()); Nan::Set(instance, Nan::New("kind").ToLocalChecked(), Nan::New(resource.kind)); auto request = Nan::ObjectWrap::Unwrap(instance); request->Execute(); return std::make_unique(request); } } // namespace node_mbgl