Merge pull request #143 from mapbox/internal-filesource

accept FileSource options object
This commit is contained in:
Mike Morris
2015-08-05 12:00:43 -04:00
15 changed files with 183 additions and 397 deletions

103
README.md
View File

@@ -27,7 +27,7 @@ Other platforms will fall back to a source compile with `npm run build`. To comp
## Rendering a map tile
```js
var map = new mbgl.Map(fileSource);
var map = new mbgl.Map({ request: function() {} });
map.load(require('./test/fixtures/style.json'));
map.render({}, function(err, image) {
if (err) throw err;
@@ -61,22 +61,20 @@ npm test
## Implementing a file source
When creating a `Map` object, you need to pass a `FileSource` object as the first parameter
When creating a `Map`, you must pass an options object (with a required `request` and optional `cancel` method) as the first parameter.
```js
var request = require('request');
var fileSource = new mbgl.FileSource();
fileSource.request = function(req) {
// TODO
};
fileSource.cancel = function(req) {
// TODO
};
var map = new mbgl.Map({
request: function(req) {
// TODO
},
cancel: function(req) {
// TODO
}
});
```
The `request()` method starts a new request to a file, while `cancel()` tells your FileSource implementation to cancel the request (if possible). The `req` parameter has two properties:
The `request()` method starts a new request to a file, while `cancel()` tells the FileSource to cancel the request (if possible). The `req` parameter has two properties:
```json
{
@@ -104,12 +102,13 @@ It has no significance for anything but serves as a hint to your implemention as
A sample implementation that reads files from disk would look like the following:
```js
fileSource.request = function(req) {
fs.readFile(path.join('base/path', req.url), function(err, data) {
req.respond(err, { data: data });
});
};
fileSource.cancel = function() {};
var map = new mbgl.Map({
request: function(req) {
fs.readFile(path.join('base/path', req.url), function(err, data) {
req.respond(err, { data: data });
});
}
});
```
This is a very barebones implementation and you'll probably want a better implementation. E.g. it passes the url verbatim to the file system, but you'd want add some logic that normalizes `http` URLs. You'll notice that once your implementation has obtained the requested file, you have to deliver it to the requestee by calling `req.respond()`, which takes either an error object or `null` and an object with several settings:
@@ -129,51 +128,51 @@ A sample implementation that uses [`request`](https://github.com/request/request
var mbgl = require('mapbox-gl-native');
var request = require('request');
var fileSource = new mbgl.FileSource();
fileSource.request = function(req) {
request({
url: req.url,
encoding: null,
gzip: true
}, function (err, res, body) {
if (req.canceled) {
return;
}
var map = new mbgl.Map({
request: function(req) {
request({
url: req.url,
encoding: null,
gzip: true
}, function (err, res, body) {
if (req.canceled) {
return;
}
if (err) {
req.respond(err);
} else if (res.statusCode == 200) {
var response = {};
if (err) {
req.respond(err);
} else if (res.statusCode == 200) {
var response = {};
if (res.headers.modified) { response.modified = new Date(res.headers.modified); }
if (res.headers.expires) { response.expires = new Date(res.headers.expires); }
if (res.headers.etag) { response.etag = res.headers.etag; }
response.data = body;
req.respond(null, response);
} else {
req.respond(new Error(JSON.parse(body).message));
}
});
};
fileSource.cancel = function(req) {
req.canceled = true;
};
if (res.headers.modified) { response.modified = new Date(res.headers.modified); }
if (res.headers.expires) { response.expires = new Date(res.headers.expires); }
if (res.headers.etag) { response.etag = res.headers.etag; }
response.data = body;
req.respond(null, response);
} else {
req.respond(new Error(JSON.parse(body).message));
}
});
},
cancel: function(req) {
req.canceled = true;
}
});
```
Note that in reality, Mapbox GL uses two types of protocols: `asset://` for files that should be loaded from some local static system, and `http://` (and `https://`), which should be loaded from the internet. However, stylesheets are free to use other protocols too, if your implementation of FileSource supports these; e.g. you could use `s3://` to indicate that files are supposed to be loaded from S3.
Note that in reality, Mapbox GL uses two types of protocols: `asset://` for files that should be loaded from some local static system, and `http://` (and `https://`), which should be loaded from the internet. However, stylesheets are free to use other protocols too, if your implementation of `request` supports these; e.g. you could use `s3://` to indicate that files are supposed to be loaded from S3.
## Mapbox API Access tokens
To use styles that rely on Mapbox vector tiles, you must pass an [API access token](https://www.mapbox.com/developers/api/#access-tokens) in your `FileSource` implementation with requests to `mapbox://` protocols.
To use styles that rely on Mapbox vector tiles, you must pass an [API access token](https://www.mapbox.com/developers/api/#access-tokens) in your `request` implementation with requests to `mapbox://` protocols.
```js
var mbgl = require('mapbox-gl-native');
var request = require('request');
var url = require('url');
var fileSource = new mbgl.FileSource();
fileSource.request = function(req) {
var options = {};
options.request = function(req) {
var opts = {
url: req.url,
encoding: null,
@@ -201,7 +200,7 @@ fileSource.request = function(req) {
});
}
var map = mbgl.Map(fileSource);
var map = new mbgl.Map(options);
var style = mapboxStyle; // includes a datasource with a reference to something like `mapbox://mapbox.mapbox-streets-v6`
map.load(style);

View File

@@ -9,18 +9,16 @@ var PNG = require('pngjs').PNG;
var base = path.join(path.dirname(process.mainModule.filename), '../test');
var fileSource = new mbgl.FileSource();
fileSource.request = function(req) {
fs.readFile(path.join(base, req.url), function(err, data) {
req.respond(err, { data: data });
});
};
var map = new mbgl.Map({
request: function(req) {
fs.readFile(path.join(base, req.url), function(err, data) {
req.respond(err, { data: data });
});
}
});
fileSource.cancel = function(req) {
};
var map = new mbgl.Map(fileSource);
map.load(require('../test/fixtures/style.json'));
map.render({}, function(err, data) {
if (err) throw err;

View File

@@ -18,7 +18,7 @@
}
],
"dependencies": {
"nan": "^1.8.4",
"nan": "^1.9.0",
"node-pre-gyp": "^0.6.7"
},
"bundledDependencies": [

View File

@@ -8,46 +8,12 @@
namespace node_mbgl {
////////////////////////////////////////////////////////////////////////////////////////////////
// Static Node Methods
v8::Persistent<v8::FunctionTemplate> NodeFileSource::constructorTemplate;
void NodeFileSource::Init(v8::Handle<v8::Object> target) {
NanScope();
v8::Local<v8::FunctionTemplate> t = NanNew<v8::FunctionTemplate>(New);
t->InstanceTemplate()->SetInternalFieldCount(1);
t->SetClassName(NanNew("FileSource"));
NanAssignPersistent(constructorTemplate, t);
target->Set(NanNew("FileSource"), t->GetFunction());
}
NAN_METHOD(NodeFileSource::New) {
NanScope();
if (!args.IsConstructCall()) {
return NanThrowTypeError("Use the new operator to create new FileSource objects");
}
auto fs = new NodeFileSource();
fs->Wrap(args.This());
NanReturnValue(args.This());
}
////////////////////////////////////////////////////////////////////////////////////////////////
// Instance
struct NodeFileSource::Action {
const enum : bool { Add, Cancel } type;
mbgl::Resource const resource;
};
NodeFileSource::NodeFileSource() :
NodeFileSource::NodeFileSource(v8::Handle<v8::Object> options_) :
queue(new Queue(uv_default_loop(), [this](Action &action) {
if (action.type == Action::Add) {
processAdd(action.resource);
@@ -56,6 +22,8 @@ NodeFileSource::NodeFileSource() :
}
}))
{
NanAssignPersistent(options, options_->ToObject());
// Make sure that the queue doesn't block the loop from exiting.
queue->unref();
}
@@ -63,6 +31,8 @@ NodeFileSource::NodeFileSource() :
NodeFileSource::~NodeFileSource() {
queue->stop();
queue = nullptr;
NanDisposePersistent(options);
}
mbgl::Request* NodeFileSource::request(const mbgl::Resource& resource, uv_loop_t* loop, Callback callback) {
@@ -119,15 +89,20 @@ void NodeFileSource::processAdd(const mbgl::Resource& resource) {
queue->ref();
}
auto handle = NanObjectWrapHandle(this);
auto requestHandle = NanNew<v8::Object>(NodeRequest::Create(handle, resource));
auto requestHandle = NanNew<v8::Object>(NodeRequest::Create(this, resource));
v8::Persistent<v8::Object> requestPersistent;
NanAssignPersistent(requestPersistent, requestHandle);
pending.emplace(resource, std::move(requestPersistent));
#if (NODE_MODULE_VERSION > NODE_0_10_MODULE_VERSION)
auto requestFunction = v8::Local<v8::Object>::New(v8::Isolate::GetCurrent(), options)->Get(NanNew("request")).As<v8::Function>();
#else
auto requestFunction = options->Get(NanNew("request")).As<v8::Function>();
#endif
v8::Local<v8::Value> argv[] = { requestHandle };
NanMakeCallback(handle, NanNew("request"), 1, argv);
NanMakeCallback(NanGetCurrentContext()->Global(), requestFunction, 1, argv);
}
void NodeFileSource::processCancel(const mbgl::Resource& resource) {
@@ -152,14 +127,20 @@ void NodeFileSource::processCancel(const mbgl::Resource& resource) {
queue->unref();
}
auto handle = NanObjectWrapHandle(this);
if (handle->Has(NanNew("cancel"))) {
#if (NODE_MODULE_VERSION > NODE_0_10_MODULE_VERSION)
auto optionsObject = v8::Local<v8::Object>::New(v8::Isolate::GetCurrent(), options);
if (optionsObject->Has(NanNew("cancel"))) {
auto cancelFunction = optionsObject->Get(NanNew("cancel")).As<v8::Function>();
#else
if (options->Has(NanNew("cancel"))) {
auto cancelFunction = options->Get(NanNew("cancel")).As<v8::Function>();
#endif
v8::Local<v8::Value> argv[] = { requestHandle };
NanMakeCallback(handle, NanNew("cancel"), 1, argv);
NanMakeCallback(NanGetCurrentContext()->Global(), cancelFunction, 1, argv);
}
// Set the request handle in the request wrapper handle to null
ObjectWrap::Unwrap<NodeRequest>(requestHandle)->cancel();
node::ObjectWrap::Unwrap<NodeRequest>(requestHandle)->cancel();
}
}

View File

@@ -19,19 +19,9 @@ namespace node_mbgl {
namespace util { template <typename T> class AsyncQueue; }
class NodeFileSource : public node::ObjectWrap, public mbgl::FileSource {
////////////////////////////////////////////////////////////////////////////////////////////////
// Static Node Methods
class NodeFileSource : public mbgl::FileSource {
public:
static void Init(v8::Handle<v8::Object> target);
static NAN_METHOD(New);
static v8::Persistent<v8::FunctionTemplate> constructorTemplate;
////////////////////////////////////////////////////////////////////////////////////////////////
// Instance
public:
NodeFileSource();
NodeFileSource(v8::Handle<v8::Object>);
~NodeFileSource();
mbgl::Request* request(const mbgl::Resource&, uv_loop_t*, Callback);
@@ -47,6 +37,8 @@ private:
void processAdd(const mbgl::Resource&);
void processCancel(const mbgl::Resource&);
v8::Persistent<v8::Object> options;
private:
#if (NODE_MODULE_VERSION > NODE_0_10_MODULE_VERSION)
std::unordered_map<mbgl::Resource, v8::Persistent<v8::Object, v8::CopyablePersistentTraits<v8::Object>>, mbgl::Resource::Hash> pending;

View File

@@ -1,5 +1,4 @@
#include "node_map.hpp"
#include "node_file_source.hpp"
#include <mbgl/platform/default/headless_display.hpp>
#include <mbgl/map/still_image.hpp>
@@ -61,23 +60,22 @@ NAN_METHOD(NodeMap::New) {
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");
if (args.Length() < 1 || !args[0]->IsObject()) {
return NanThrowTypeError("Requires an options object as first argument");
}
auto source = args[0]->ToObject();
auto options = 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 (!options->Has(NanNew("request")) || !options->Get(NanNew("request"))->IsFunction()) {
return NanThrowError("Options object must have a 'request' method");
}
if (!source->Has(NanNew("cancel")) || !source->Get(NanNew("cancel"))->IsFunction()) {
return NanThrowError("FileSource must have a cancel member function");
if (options->Has(NanNew("cancel")) && !options->Get(NanNew("cancel"))->IsFunction()) {
return NanThrowError("Options object 'cancel' property must be a function");
}
try {
auto nodeMap = new NodeMap(source);
auto nodeMap = new NodeMap(options);
nodeMap->Wrap(args.This());
} catch(std::exception &ex) {
return NanThrowError(ex.what());
@@ -309,8 +307,6 @@ void NodeMap::release() {
valid = false;
NanDisposePersistent(source);
uv_close(reinterpret_cast<uv_handle_t *>(async), [] (uv_handle_t *handle) {
delete reinterpret_cast<uv_async_t *>(handle);
});
@@ -322,14 +318,12 @@ void NodeMap::release() {
////////////////////////////////////////////////////////////////////////////////////////////////
// Instance
NodeMap::NodeMap(v8::Handle<v8::Object> source_) :
NodeMap::NodeMap(v8::Handle<v8::Object> options) :
view(sharedDisplay()),
fs(*ObjectWrap::Unwrap<NodeFileSource>(source_)),
fs(options),
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) {

View File

@@ -1,5 +1,7 @@
#pragma once
#include "node_file_source.hpp"
#include <mbgl/map/map.hpp>
#include <mbgl/platform/default/headless_view.hpp>
@@ -14,8 +16,6 @@
namespace node_mbgl {
class NodeFileSource;
class NodeMap : public node::ObjectWrap {
struct RenderOptions;
class RenderWorker;
@@ -44,15 +44,12 @@ public:
////////////////////////////////////////////////////////////////////////////////////////////////
// Instance
private:
NodeMap(v8::Handle<v8::Object> source);
NodeMap(v8::Handle<v8::Object>);
~NodeMap();
private:
// For retaining the FileSource object.
v8::Persistent<v8::Object> source;
mbgl::HeadlessView view;
NodeFileSource &fs;
NodeFileSource fs;
std::unique_ptr<mbgl::Map> map;
std::exception_ptr error;

View File

@@ -5,7 +5,6 @@
#include <nan.h>
#pragma GCC diagnostic pop
#include "node_file_source.hpp"
#include "node_map.hpp"
#include "node_log.hpp"
#include "node_request.hpp"
@@ -13,7 +12,6 @@
void RegisterModule(v8::Handle<v8::Object> exports) {
NanScope();
node_mbgl::NodeFileSource::Init(exports);
node_mbgl::NodeMap::Init(exports);
node_mbgl::NodeRequest::Init(exports);

View File

@@ -31,21 +31,22 @@ NAN_METHOD(NodeRequest::New) {
NanScope();
// Extract the pointer from the first argument
if (args.Length() < 2 || !NanHasInstance(NodeFileSource::constructorTemplate, args[0]) || !args[1]->IsExternal()) {
if (args.Length() < 2 || !args[0]->IsExternal() || !args[1]->IsExternal()) {
return NanThrowTypeError("Cannot create Request objects explicitly");
}
auto source = reinterpret_cast<NodeFileSource*>(args[0].As<v8::External>()->Value());
auto resource = reinterpret_cast<mbgl::Resource*>(args[1].As<v8::External>()->Value());
auto req = new NodeRequest(args[0]->ToObject(), *resource);
auto req = new NodeRequest(source, *resource);
req->Wrap(args.This());
NanReturnValue(args.This());
}
v8::Handle<v8::Object> NodeRequest::Create(v8::Handle<v8::Object> source, const mbgl::Resource& resource) {
v8::Handle<v8::Object> NodeRequest::Create(NodeFileSource* source, const mbgl::Resource& resource) {
NanEscapableScope();
v8::Local<v8::Value> argv[] = { NanNew<v8::Object>(source),
v8::Local<v8::Value> argv[] = { NanNew<v8::External>(const_cast<NodeFileSource*>(source)),
NanNew<v8::External>(const_cast<mbgl::Resource*>(&resource)) };
auto instance = NanNew<v8::FunctionTemplate>(constructorTemplate)->GetFunction()->NewInstance(2, argv);
@@ -61,7 +62,7 @@ NAN_METHOD(NodeRequest::Respond) {
return NanThrowError("Request has already been responded to, or was canceled.");
}
auto source = ObjectWrap::Unwrap<NodeFileSource>(NanNew<v8::Object>(nodeRequest->source));
auto source = nodeRequest->source;
auto resource = std::move(nodeRequest->resource);
if (args.Length() < 1) {
@@ -128,14 +129,11 @@ NAN_METHOD(NodeRequest::Respond) {
////////////////////////////////////////////////////////////////////////////////////////////////
// Instance
NodeRequest::NodeRequest(v8::Local<v8::Object> source_, const mbgl::Resource& resource_)
: resource(std::make_unique<mbgl::Resource>(resource_)) {
NanAssignPersistent(source, source_);
}
NodeRequest::NodeRequest(NodeFileSource* source_, const mbgl::Resource& resource_)
: source(source_),
resource(std::make_unique<mbgl::Resource>(resource_)) {}
NodeRequest::~NodeRequest() {
NanDisposePersistent(source);
}
NodeRequest::~NodeRequest() {}
void NodeRequest::cancel() {
resource.reset();

View File

@@ -11,10 +11,10 @@
#include <memory>
class NodeFileSource;
namespace node_mbgl {
class NodeFileSource;
class NodeRequest : public node::ObjectWrap {
////////////////////////////////////////////////////////////////////////////////////////////////
// Static Node Methods
@@ -23,20 +23,20 @@ public:
static NAN_METHOD(New);
static NAN_METHOD(Respond);
static v8::Handle<v8::Object> Create(v8::Handle<v8::Object> source, const mbgl::Resource& resource);
static v8::Handle<v8::Object> Create(NodeFileSource*, const mbgl::Resource&);
static v8::Persistent<v8::FunctionTemplate> constructorTemplate;
////////////////////////////////////////////////////////////////////////////////////////////////
// Instance
public:
NodeRequest(v8::Local<v8::Object> source, const mbgl::Resource& resource);
NodeRequest(NodeFileSource* source, const mbgl::Resource& resource);
~NodeRequest();
void cancel();
private:
v8::Persistent<v8::Object> source;
NodeFileSource* source;
std::unique_ptr<mbgl::Resource> resource;
};

View File

@@ -16,16 +16,16 @@ function renderTest(style, info, dir, key) {
var remaining = 10;
var start = +new Date;
var fileSource = new mbgl.FileSource();
fileSource.request = function(req) {
var options = {};
options.request = function(req) {
fs.readFile(path.join(suitePath, decodeURIComponent(req.url)), function(err, data) {
req.respond(err, { data: data });
t.error(err);
});
};
fileSource.cancel = function() {};
options.cancel = function() {};
var map = new mbgl.Map(fileSource);
var map = new mbgl.Map(options);
map.load(style);
function render() {

View File

@@ -1,21 +0,0 @@
'use strict';
/* jshint node: true */
var test = require('tape');
var mbgl = require('../..');
test('FileSource', function(t) {
t.test('constructor', function(t) {
t.test('must be called with new', function(t) {
t.throws(function() {
mbgl.FileSource();
}, /Use the new operator to create new FileSource objects/);
t.end();
});
t.end();
});
t.end();
});

View File

@@ -27,35 +27,31 @@ function filePath(name) {
}, {});
}
function setup(fileSource, callback) {
callback(new mbgl.Map(fileSource));
function setup(options, callback) {
callback(new mbgl.Map(options));
}
function getFileSource(gzip, t) {
var fileSource = new mbgl.FileSource();
function getOptions(gzip, t) {
return {
request: function(req) {
var parts = req.url.split('.');
var filetype = parts[parts.length - 1];
fileSource.request = function(req) {
var parts = req.url.split('.');
var filetype = parts[parts.length - 1];
request({
url: 'http://localhost:' + server.address().port + path.join('/', req.url),
encoding: null,
gzip: filetype === 'pbf' ? gzip : true,
headers: {
'Accept-Encoding': 'gzip'
}
}, function (err, res, body) {
t.error(err);
var response = {};
response.data = res.body;
req.respond(null, response);
});
request({
url: 'http://localhost:' + server.address().port + path.join('/', req.url),
encoding: null,
gzip: filetype === 'pbf' ? gzip : true,
headers: {
'Accept-Encoding': 'gzip'
}
}, function (err, res, body) {
t.error(err);
var response = {};
response.data = res.body;
req.respond(null, response);
});
}
};
fileSource.cancel = function(req) {};
return fileSource;
}
test('gzip', function(t) {
@@ -64,7 +60,7 @@ test('gzip', function(t) {
if (msg.severity == 'ERROR') t.error(msg);
});
setup(getFileSource(true, t), function(map) {
setup(getOptions(true, t), function(map) {
map.load(style);
map.render({}, function(err, data) {
mbgl.removeAllListeners('message');
@@ -110,7 +106,7 @@ test('gzip', function(t) {
}
});
setup(getFileSource(false, t), function(map) {
setup(getOptions(false, t), function(map) {
map.load(style);
map.render({}, function(err, data) {
map.release();

View File

@@ -20,8 +20,8 @@ function filePath(name) {
}, {});
}
function setup(fileSource, callback) {
callback(new mbgl.Map(fileSource));
function setup(options, callback) {
callback(new mbgl.Map(options));
}
test('Map', function(t) {
@@ -34,47 +34,39 @@ test('Map', function(t) {
t.end();
});
t.test('should require a FileSource object as first parameter', function(t) {
t.test('should require an options object as first parameter', function(t) {
t.throws(function() {
new mbgl.Map();
}, /Requires a FileSource as first argument/);
}, /Requires an options object as first argument/);
t.throws(function() {
new mbgl.Map('fileSource');
}, /Requires a FileSource as first argument/);
t.throws(function() {
new mbgl.Map({});
}, /Requires a FileSource as first argument/);
new mbgl.Map('options');
}, /Requires an options object as first argument/);
t.end();
});
t.test('should require the FileSource object to have request and cancel methods', function(t) {
var fileSource = new mbgl.FileSource();
t.test('should require then options object to have request and cancel methods', function(t) {
var options = {};
t.throws(function() {
new mbgl.Map(fileSource);
}, /FileSource must have a request member function/);
new mbgl.Map(options);
}, /Options object must have a 'request' method/);
fileSource.request = 'test';
options.request = 'test';
t.throws(function() {
new mbgl.Map(fileSource);
}, /FileSource must have a request member function/);
new mbgl.Map(options);
}, /Options object must have a 'request' method/);
fileSource.request = function() {};
options.request = function() {};
options.cancel = 'test';
t.throws(function() {
new mbgl.Map(fileSource);
}, /FileSource must have a cancel member function/);
new mbgl.Map(options);
}, /Options object 'cancel' property must be a function/);
fileSource.cancel = 'test';
t.throws(function() {
new mbgl.Map(fileSource);
}, /FileSource must have a cancel member function/);
fileSource.cancel = function() {};
options.cancel = function() {};
t.doesNotThrow(function() {
new mbgl.Map(fileSource);
new mbgl.Map(options);
});
t.end();
@@ -84,13 +76,13 @@ test('Map', function(t) {
});
t.test('load styles', function(t) {
var fileSource = new mbgl.FileSource();
fileSource.request = function() {};
fileSource.cancel = function() {};
var options = {};
options.request = function() {};
options.cancel = function() {};
t.test('requires a string or object as the first parameter', function(t) {
t.test('requires a map style as first argument', function(t) {
setup(fileSource, function(map) {
setup(options, function(map) {
t.throws(function() {
map.load();
}, /Requires a map style as first argument/);
@@ -102,7 +94,7 @@ test('Map', function(t) {
});
t.test('expect either an object or array at root', { timeout: 1000 }, function(t) {
setup(fileSource, function(map) {
setup(options, function(map) {
mbgl.once('message', function(msg) {
t.equal(msg.severity, 'ERROR');
t.equal(msg.class, 'ParseStyle');
@@ -121,7 +113,7 @@ test('Map', function(t) {
});
t.test('accepts an empty stylesheet string', function(t) {
setup(fileSource, function(map) {
setup(options, function(map) {
t.doesNotThrow(function() {
map.load('{}');
});
@@ -133,7 +125,7 @@ test('Map', function(t) {
});
t.test('accepts a JSON stylesheet', { timeout: 1000 }, function(t) {
setup(fileSource, function(map) {
setup(options, function(map) {
t.doesNotThrow(function() {
map.load(style);
});
@@ -145,7 +137,7 @@ test('Map', function(t) {
});
t.test('accepts a stringified stylesheet', { timeout: 1000 }, function(t) {
setup(fileSource, function(map) {
setup(options, function(map) {
t.doesNotThrow(function() {
map.load(JSON.stringify(style));
});
@@ -159,155 +151,17 @@ test('Map', function(t) {
t.end();
});
t.test('request coalesting', function(t) {
var requestList = [];
var fileSourceDoNotReply = new mbgl.FileSource();
// This file source will never reply to any
// request other than "./fixtures/tiles.tilejson" which
// will force the cancellation of the pending requests.
fileSourceDoNotReply.request = function(req) {
if (req.url != "./fixtures/tiles.tilejson") {
requestList.push(req);
return;
}
fs.readFile(path.join('test', req.url), function(err, data) {
req.respond(err, { data: data });
});
};
fileSourceDoNotReply.cancel = function() {
requestList.pop();
};
t.test('merge requests/cancels for the same resource', function(t) {
var map1 = new mbgl.Map(fileSourceDoNotReply);
var map2 = new mbgl.Map(fileSourceDoNotReply);
var map3 = new mbgl.Map(fileSourceDoNotReply);
var map4 = new mbgl.Map(fileSourceDoNotReply);
var map5 = new mbgl.Map(fileSourceDoNotReply);
map1.load(style);
map2.load(style);
map3.load(style);
map4.load(style);
map5.load(style);
var renderCallback = function(err, data) {}
map1.render({}, renderCallback);
map2.render({}, renderCallback);
map3.render({}, renderCallback);
map4.render({}, renderCallback);
map5.render({}, renderCallback);
var checkCancel = function() {
t.equal(requestList.length, 0);
t.end();
};
var checkCoalescing = function() {
// Check if all the 5 vector tile
// requests are going to merge in
// a single request.
t.equal(requestList.length, 1);
// This should cause the pending
// resources to get canceled, but
// only one cancel is emitted, because
// all map objects requested the same
// resource.
map1.release();
map2.release();
map3.release();
map4.release();
map5.release();
setTimeout(checkCancel, 500);
};
setTimeout(checkCoalescing, 500);
});
var vectorTileRequestCount = 0;
var fileSourceDelay = new mbgl.FileSource();
// This file source will add a little delay before
// replying so we make sure that all the map objects
// request the same resource will get a reply from the
// same request.
fileSourceDelay.request = function(req) {
var timeout = 0;
if (req.url != "./fixtures/tiles.tilejson") {
timeout = 500;
t.ok(++vectorTileRequestCount == 1, "Should make only one request for tiles");
}
var readFile = function() {
fs.readFile(path.join('test', req.url), function(err, data) {
req.respond(err, { data: data });
});
};
setTimeout(readFile, timeout);
};
fileSourceDelay.cancel = function() {
t.fail("Should never cancel");
};
t.test('one request for the same resource notifies multiple map objects', function(t) {
var map1 = new mbgl.Map(fileSourceDelay);
var map2 = new mbgl.Map(fileSourceDelay);
var map3 = new mbgl.Map(fileSourceDelay);
var map4 = new mbgl.Map(fileSourceDelay);
var map5 = new mbgl.Map(fileSourceDelay);
map1.load(style);
map2.load(style);
map3.load(style);
map4.load(style);
map5.load(style);
var renderFinishedCount = 0;
var renderCallback = function(err, data) {
// We make only one request for tiles, but that
// should be enough for all these map objects because
// they all want the same resource.
if (++renderFinishedCount == 5) {
map1.release();
map2.release();
map3.release();
map4.release();
map5.release();
t.end();
}
}
map1.render({}, renderCallback);
map2.render({}, renderCallback);
map3.render({}, renderCallback);
map4.render({}, renderCallback);
map5.render({}, renderCallback);
});
t.end();
});
t.test('render argument requirements', function(t) {
var fileSource = new mbgl.FileSource();
fileSource.request = function(req) {
var options = {};
options.request = function(req) {
fs.readFile(path.join('test', req.url), function(err, data) {
req.respond(err, { data: data });
});
};
fileSource.cancel = function() {};
options.cancel = function() {};
t.test('requires an object as the first parameter', function(t) {
setup(fileSource, function(map) {
setup(options, function(map) {
t.throws(function() {
map.render();
}, /First argument must be an options object/);
@@ -323,7 +177,7 @@ test('Map', function(t) {
});
t.test('requires a callback as the second parameter', function(t) {
setup(fileSource, function(map) {
setup(options, function(map) {
t.throws(function() {
map.render({});
}, /Second argument must be a callback function/);
@@ -339,7 +193,7 @@ test('Map', function(t) {
});
t.test('requires a style to be set', function(t) {
setup(fileSource, function(map) {
setup(options, function(map) {
t.throws(function() {
map.render({}, function() {});
}, /Style is not loaded/);
@@ -358,7 +212,7 @@ test('Map', function(t) {
t.ok(msg.text.match(/Failed to load/), 'error text matches');
});
setup(fileSource, function(map) {
setup(options, function(map) {
map.load(style);
map.render({ zoom: 1 }, function(err, data) {
mbgl.removeAllListeners('message');
@@ -373,7 +227,7 @@ test('Map', function(t) {
});
t.test('double release', function(t) {
setup(fileSource, function(map) {
setup(options, function(map) {
map.release();
t.throws(function() {
@@ -385,7 +239,7 @@ test('Map', function(t) {
});
t.test('returns an image', function(t) {
setup(fileSource, function(map) {
setup(options, function(map) {
map.load(style);
map.render({}, function(err, data) {
t.error(err);

View File

@@ -39,16 +39,16 @@ function renderTest(style, info, base, key) {
clearTimeout(watchdog);
});
var fileSource = new mbgl.FileSource();
fileSource.request = function(req) {
var options = {};
options.request = function(req) {
fs.readFile(path.join(suitePath, decodeURIComponent(req.url)), function(err, data) {
req.respond(err, { data: data });
t.error(err);
});
};
fileSource.cancel = function() {};
options.cancel = function() {};
var map = new mbgl.Map(fileSource);
var map = new mbgl.Map(options);
map.load(style);
map.render(info[key], function(err, data) {