#include "node_map.hpp" #include #include #include #include #if UV_VERSION_MAJOR == 0 && UV_VERSION_MINOR <= 10 #define UV_ASYNC_PARAMS(handle) uv_async_t *handle, int #else #define UV_ASYNC_PARAMS(handle) uv_async_t *handle #endif namespace node_mbgl { struct NodeMap::RenderOptions { double zoom = 0; double bearing = 0; double latitude = 0; double longitude = 0; unsigned int width = 512; unsigned int height = 512; std::vector classes; }; //////////////////////////////////////////////////////////////////////////////////////////////// // Static Node Methods Nan::Persistent NodeMap::constructor; static std::shared_ptr sharedDisplay() { static auto display = std::make_shared(); return display; } const static char* releasedMessage() { return "Map resources have already been released"; } NAN_MODULE_INIT(NodeMap::Init) { v8::Local tpl = Nan::New(New); tpl->InstanceTemplate()->SetInternalFieldCount(1); tpl->SetClassName(Nan::New("Map").ToLocalChecked()); Nan::SetPrototypeMethod(tpl, "load", Load); Nan::SetPrototypeMethod(tpl, "render", Render); Nan::SetPrototypeMethod(tpl, "release", Release); constructor.Reset(tpl->GetFunction()); Nan::Set(target, Nan::New("Map").ToLocalChecked(), tpl->GetFunction()); // Initialize display connection on module load. sharedDisplay(); } NAN_METHOD(NodeMap::New) { 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 = info[0]->ToObject(); // Check that 'request', 'cancel' and 'ratio' are defined. 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 must have a numerical 'ratio' property"); } try { auto nodeMap = new NodeMap(options); nodeMap->Wrap(info.This()); } catch(std::exception &ex) { return Nan::ThrowError(ex.what()); } info.GetReturnValue().Set(info.This()); } const std::string StringifyStyle(v8::Local styleHandle) { Nan::HandleScope scope; v8::Local JSON = Nan::Get( Nan::GetCurrentContext()->Global(), Nan::New("JSON").ToLocalChecked()).ToLocalChecked()->ToObject(); return *Nan::Utf8String(Nan::MakeCallback(JSON, "stringify", 1, &styleHandle)); } NAN_METHOD(NodeMap::Load) { auto nodeMap = Nan::ObjectWrap::Unwrap(info.Holder()); if (!nodeMap->isValid()) 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()) { style = StringifyStyle(info[0]); } 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->setStyleJSON(style, "."); } catch (const std::exception &ex) { return Nan::ThrowError(ex.what()); } nodeMap->loaded = true; info.GetReturnValue().SetUndefined(); } std::unique_ptr NodeMap::ParseOptions(v8::Local obj) { Nan::HandleScope scope; auto options = std::make_unique(); 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("center").ToLocalChecked()).FromJust()) { auto center = Nan::Get(obj, Nan::New("center").ToLocalChecked()).ToLocalChecked().As(); if (center->Length() > 0) { options->latitude = Nan::Get(center, 0).ToLocalChecked()->NumberValue(); } if (center->Length() > 1) { options->longitude = Nan::Get(center, 1).ToLocalChecked()->NumberValue(); } } if (Nan::Has(obj, Nan::New("width").ToLocalChecked()).FromJust()) { options->width = Nan::Get(obj, Nan::New("width").ToLocalChecked()).ToLocalChecked()->IntegerValue(); } if (Nan::Has(obj, Nan::New("height").ToLocalChecked()).FromJust()) { options->height = Nan::Get(obj, Nan::New("height").ToLocalChecked()).ToLocalChecked()->IntegerValue(); } if (Nan::Has(obj, Nan::New("classes").ToLocalChecked()).FromJust()) { auto classes = Nan::Get(obj, Nan::New("classes").ToLocalChecked()).ToLocalChecked()->ToObject().As(); const int length = classes->Length(); options->classes.reserve(length); for (int i = 0; i < length; i++) { options->classes.push_back(std::string { *Nan::Utf8String(Nan::Get(classes, i).ToLocalChecked()->ToString()) }); } } return options; } NAN_METHOD(NodeMap::Render) { auto nodeMap = Nan::ObjectWrap::Unwrap(info.Holder()); if (!nodeMap->isValid()) 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->isLoaded()) { return Nan::ThrowTypeError("Style is not loaded"); } if (nodeMap->callback) { return Nan::ThrowError("Map is currently rendering an image"); } auto options = ParseOptions(info[0]->ToObject()); assert(!nodeMap->callback); assert(!nodeMap->image); nodeMap->callback = std::make_unique(info[1].As()); try { nodeMap->startRender(std::move(options)); } catch (mbgl::util::Exception &ex) { return Nan::ThrowError(ex.what()); } info.GetReturnValue().SetUndefined(); } void NodeMap::startRender(std::unique_ptr options) { view.resize(options->width, options->height); map->update(mbgl::Update::Dimensions); map->setClasses(options->classes); map->setLatLngZoom(mbgl::LatLng(options->latitude, options->longitude), options->zoom); map->setBearing(options->bearing); map->renderStill([this](const std::exception_ptr eptr, std::unique_ptr result) { if (eptr) { error = std::move(eptr); uv_async_send(async); } else { assert(!image); image = std::move(result); 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::renderFinished() { 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)); // There is no render pending anymore, we the GC could now delete this object if it went out // of scope. Unref(); // Move the callback and image out of the way so that the callback can start a new render call. auto cb = std::move(callback); auto img = std::move(image); assert(cb); // These have to be empty to be prepared for the next render call. assert(!callback); assert(!image); if (error) { std::string errorMessage; try { std::rethrow_exception(error); } catch (const std::exception& ex) { errorMessage = ex.what(); } v8::Local argv[] = { Nan::Error(errorMessage.c_str()) }; // This must be empty to be prepared for the next render call. error = nullptr; assert(!error); cb->Call(1, argv); } else if (img) { v8::Local pixels = Nan::NewBuffer( reinterpret_cast(img->pixels.get()), size_t(img->width) * size_t(img->height) * sizeof(mbgl::StillImage::Pixel), // Retain the StillImage object until the buffer is deleted. [](char *, void *hint) { delete reinterpret_cast(hint); }, const_cast(img.get()) ).ToLocalChecked(); img.release(); v8::Local argv[] = { Nan::Null(), pixels }; cb->Call(2, argv); } else { v8::Local argv[] = { Nan::Error("Didn't get an image") }; cb->Call(1, argv); } } NAN_METHOD(NodeMap::Release) { auto nodeMap = Nan::ObjectWrap::Unwrap(info.Holder()); if (!nodeMap->isValid()) return Nan::ThrowError(releasedMessage()); try { nodeMap->release(); } catch (const std::exception &ex) { return Nan::ThrowError(ex.what()); } info.GetReturnValue().SetUndefined(); } void NodeMap::release() { if (!isValid()) throw mbgl::util::Exception(releasedMessage()); valid = false; uv_close(reinterpret_cast(async), [] (uv_handle_t *handle) { delete reinterpret_cast(handle); }); map.reset(nullptr); } //////////////////////////////////////////////////////////////////////////////////////////////// // Instance NodeMap::NodeMap(v8::Local options) : view(sharedDisplay(), [&] { Nan::HandleScope scope; return Nan::Get(options, Nan::New("ratio").ToLocalChecked()).ToLocalChecked()->NumberValue(); }()), fs(options), map(std::make_unique(view, fs, mbgl::MapMode::Still)), async(new uv_async_t) { async->data = this; uv_async_init(uv_default_loop(), async, [](UV_ASYNC_PARAMS(handle)) { reinterpret_cast(handle->data)->renderFinished(); }); // Make sure the async handle doesn't keep the loop alive. uv_unref(reinterpret_cast(async)); } NodeMap::~NodeMap() { if (valid) release(); } }