Files
MapLibreParticleLab/src/node_map.cpp
Thiago Marcos P. Santos 1ae7dd585d Throw when style is not loaded
mbgl::Map won't throw if the style is not set but we
can track that on NodeMap.
2015-06-22 08:48:44 +03:00

351 lines
10 KiB
C++

#include "node_map.hpp"
#include "node_file_source.hpp"
#include <mbgl/platform/default/headless_display.hpp>
#include <mbgl/map/still_image.hpp>
#include <mbgl/util/exception.hpp>
#include <unistd.h>
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;
float ratio = 1.0f;
std::vector<std::string> classes;
};
////////////////////////////////////////////////////////////////////////////////////////////////
// Static Node Methods
v8::Persistent<v8::FunctionTemplate> NodeMap::constructorTemplate;
static std::shared_ptr<mbgl::HeadlessDisplay> sharedDisplay() {
static auto display = std::make_shared<mbgl::HeadlessDisplay>();
return display;
}
const static char* releasedMessage() {
return "Map resources have already been released";
}
void NodeMap::Init(v8::Handle<v8::Object> target) {
NanScope();
v8::Local<v8::FunctionTemplate> t = NanNew<v8::FunctionTemplate>(New);
t->InstanceTemplate()->SetInternalFieldCount(1);
t->SetClassName(NanNew("Map"));
NODE_SET_PROTOTYPE_METHOD(t, "load", Load);
NODE_SET_PROTOTYPE_METHOD(t, "render", Render);
NODE_SET_PROTOTYPE_METHOD(t, "release", Release);
NanAssignPersistent(constructorTemplate, t);
target->Set(NanNew("Map"), t->GetFunction());
// Initialize display connection on module load.
sharedDisplay();
}
NAN_METHOD(NodeMap::New) {
NanScope();
if (!args.IsConstructCall()) {
return NanThrowTypeError("Use the new operator to create new Map objects");
}
if (args.Length() < 1 || !NanHasInstance(NodeFileSource::constructorTemplate, args[0])) {
return NanThrowTypeError("Requires a FileSource as first argument");
}
auto source = args[0]->ToObject();
// Check that request() and cancel() are defined.
if (!source->Has(NanNew("request")) || !source->Get(NanNew("request"))->IsFunction()) {
return NanThrowError("FileSource must have a request member function");
}
if (!source->Has(NanNew("cancel")) || !source->Get(NanNew("cancel"))->IsFunction()) {
return NanThrowError("FileSource must have a cancel member function");
}
try {
auto nodeMap = new NodeMap(source);
nodeMap->Wrap(args.This());
} catch(std::exception &ex) {
return NanThrowError(ex.what());
}
NanReturnValue(args.This());
}
const std::string StringifyStyle(v8::Handle<v8::Value> styleHandle) {
NanScope();
auto JSON = NanGetCurrentContext()->Global()->Get(NanNew("JSON"))->ToObject();
auto stringify = v8::Handle<v8::Function>::Cast(JSON->Get(NanNew("stringify")));
return *NanUtf8String(stringify->Call(JSON, 1, &styleHandle));
}
NAN_METHOD(NodeMap::Load) {
NanScope();
auto nodeMap = node::ObjectWrap::Unwrap<NodeMap>(args.Holder());
if (!nodeMap->isValid()) return NanThrowError(releasedMessage());
// Reset the flag as this could be the second time
// we are calling this (being the previous successful).
nodeMap->loaded = false;
if (args.Length() < 1) {
return NanThrowError("Requires a map style as first argument");
}
std::string style;
if (args[0]->IsObject()) {
style = StringifyStyle(args[0]);
} else if (args[0]->IsString()) {
style = *NanUtf8String(args[0]);
} else {
return NanThrowTypeError("First argument must be a string or object");
}
try {
nodeMap->map->setStyleJSON(style, ".");
} catch (const std::exception &ex) {
return NanThrowError(ex.what());
}
nodeMap->loaded = true;
NanReturnUndefined();
}
std::unique_ptr<NodeMap::RenderOptions> NodeMap::ParseOptions(v8::Local<v8::Object> obj) {
NanScope();
auto options = std::make_unique<RenderOptions>();
if (obj->Has(NanNew("zoom"))) { options->zoom = obj->Get(NanNew("zoom"))->NumberValue(); }
if (obj->Has(NanNew("bearing"))) { options->bearing = obj->Get(NanNew("bearing"))->NumberValue(); }
if (obj->Has(NanNew("center"))) {
auto center = obj->Get(NanNew("center")).As<v8::Array>();
if (center->Length() > 0) { options->latitude = center->Get(0)->NumberValue(); }
if (center->Length() > 1) { options->longitude = center->Get(1)->NumberValue(); }
}
if (obj->Has(NanNew("width"))) { options->width = obj->Get(NanNew("width"))->IntegerValue(); }
if (obj->Has(NanNew("height"))) { options->height = obj->Get(NanNew("height"))->IntegerValue(); }
if (obj->Has(NanNew("ratio"))) { options->ratio = obj->Get(NanNew("ratio"))->IntegerValue(); }
if (obj->Has(NanNew("classes"))) {
auto classes = obj->Get(NanNew("classes"))->ToObject().As<v8::Array>();
const int length = classes->Length();
options->classes.reserve(length);
for (int i = 0; i < length; i++) {
options->classes.push_back(std::string { *NanUtf8String(classes->Get(i)->ToString()) });
}
}
return options;
}
NAN_METHOD(NodeMap::Render) {
NanScope();
auto nodeMap = node::ObjectWrap::Unwrap<NodeMap>(args.Holder());
if (!nodeMap->isValid()) return NanThrowError(releasedMessage());
if (args.Length() <= 0 || !args[0]->IsObject()) {
return NanThrowTypeError("First argument must be an options object");
}
if (args.Length() <= 1 || !args[1]->IsFunction()) {
return NanThrowTypeError("Second argument must be a callback function");
}
if (!nodeMap->isLoaded()) {
return NanThrowTypeError("Style is not loaded");
}
auto options = ParseOptions(args[0]->ToObject());
assert(!nodeMap->callback);
assert(!nodeMap->image);
nodeMap->callback = std::unique_ptr<NanCallback>(new NanCallback(args[1].As<v8::Function>()));
try {
nodeMap->startRender(std::move(options));
} catch (mbgl::util::Exception &ex) {
return NanThrowError(ex.what());
}
NanReturnUndefined();
}
void NodeMap::startRender(std::unique_ptr<NodeMap::RenderOptions> options) {
map->resize(options->width, options->height, options->ratio);
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<const mbgl::StillImage> 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<uv_handle_t *>(async));
}
void NodeMap::renderFinished() {
NanScope();
// 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));
// 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<v8::Value> argv[] = {
NanError(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) {
auto result = NanNew<v8::Object>();
result->Set(NanNew("width"), NanNew(img->width));
result->Set(NanNew("height"), NanNew(img->height));
v8::Local<v8::Object> pixels = NanNewBufferHandle(
reinterpret_cast<char *>(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<const mbgl::StillImage *>(hint);
},
const_cast<mbgl::StillImage *>(img.get())
);
img.release();
result->Set(NanNew("pixels"), pixels);
v8::Local<v8::Value> argv[] = {
NanNull(),
result,
};
cb->Call(2, argv);
} else {
v8::Local<v8::Value> argv[] = {
NanError("Didn't get an image")
};
cb->Call(1, argv);
}
}
NAN_METHOD(NodeMap::Release) {
NanScope();
auto nodeMap = node::ObjectWrap::Unwrap<NodeMap>(args.Holder());
if (!nodeMap->isValid()) return NanThrowError(releasedMessage());
try {
nodeMap->release();
} catch (const std::exception &ex) {
return NanThrowError(ex.what());
}
NanReturnUndefined();
}
void NodeMap::release() {
if (!isValid()) throw mbgl::util::Exception(releasedMessage());
valid = false;
NanDisposePersistent(source);
uv_close(reinterpret_cast<uv_handle_t *>(async), [] (uv_handle_t *handle) {
delete reinterpret_cast<uv_async_t *>(handle);
});
map.reset(nullptr);
}
////////////////////////////////////////////////////////////////////////////////////////////////
// Instance
NodeMap::NodeMap(v8::Handle<v8::Object> source_) :
view(sharedDisplay()),
fs(*ObjectWrap::Unwrap<NodeFileSource>(source_)),
map(std::make_unique<mbgl::Map>(view, fs, mbgl::MapMode::Still)),
async(new uv_async_t) {
NanAssignPersistent(source, source_);
async->data = this;
#if UV_VERSION_MAJOR == 0 && UV_VERSION_MINOR <= 10
uv_async_init(uv_default_loop(), async, [](uv_async_t *as, int) {
#else
uv_async_init(uv_default_loop(), async, [](uv_async_t *as) {
#endif
reinterpret_cast<NodeMap *>(as->data)->renderFinished();
});
// Make sure the async handle doesn't keep the loop alive.
uv_unref(reinterpret_cast<uv_handle_t *>(async));
}
NodeMap::~NodeMap() {
if (valid) release();
}
}