Skip to content

Commit

Permalink
Adjustments to #21 for better compatibility, support for "*@2x.png"-s…
Browse files Browse the repository at this point in the history
…tyle routes + documentation / more tests
  • Loading branch information
brianreavis committed Sep 11, 2017
1 parent 50cb36c commit e002a71
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 11 deletions.
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ Once configured and started, tiles can be accessed via:
/:layer/:z/:x:/:y/:filename
```

### Routing Without Filenames

As of [2.1.0](https://github.com/naturalatlas/tilestrata/releases/tag/v2.1.0), if you desire a routing scheme that's closer to other tile servers (where there's no filename) like outlined in [#21](https://github.com/naturalatlas/tilestrata/pull/21), use the following format when registering routes:

```js
.route('*.png') // /layer/0/0/0.png
.route('*@2x.png') // /layer/0/0/[email protected]
```

### Integrate with [Express.js](http://expressjs.com/) / [Connect](https://github.com/senchalabs/connect)

TileStrata comes with middleware for Express that makes serving tiles from an existing application really simple, eliminating the need to call `listen` on `strata`.
Expand Down Expand Up @@ -206,7 +215,9 @@ The version of TileStrata (useful to plugins, mainly).

##### layer.route(filename, [options])

Registers a route. Returns a [TileRequestHandler](#tilerequesthandler) instance to be configured. Setting the filename to `*.{extension}` will omit the filename from the request and make the tiles available at `/{z}/{x}/{y}.{extension}` (see [#21](https://github.com/naturalatlas/tilestrata/pull/21)). The available options are:
Registers a route and returns a [TileRequestHandler](#tilerequesthandler) instance to be configured. Setting `filename` to something like `"*.ext"` or `"*@2x.ext"` will omit the filename from the request and make the tiles available at `/{z}/{x}/{y}.ext` and `/{z}/{x}/{y}@2x.ext`, respectively (see [#21](https://github.com/naturalatlas/tilestrata/pull/21)).

The available options are:

- **cacheFetchMode**: Defines how cache fetching happens when multiple caches are configured. The mode can be `"sequential"` or `"race"`. If set to `"race"`, TileStrata will fetch from all caches simultaneously and return the first that wins.

Expand All @@ -217,7 +228,9 @@ Registers a plugin, which is either a provider, cache, transform, request hook,

#### [TileRequest](#tilerequest)

A request contains these properties: `x`, `y`, `z`, `layer` (string), `filename`, `method`, `headers`, and `qs`.
A request contains these properties: `x`, `y`, `z`, `layer` (string), `filename`, `method`, `headers`, `qs`, and `hasFilename`.

If a tile request is in the filenameless format ([see here](#routing-without-filenames)), `hasFilename` will be `false`. To illustrate: if the request is to `/layer/0/0/[email protected]`, `filename` will be set to `[email protected]` (for compatibility with caches and plugins that expect a filename) and `hasFilename` will be `false`.

##### tile.clone()
Returns an identical copy of the tile request that's safe to mutate.
Expand Down
35 changes: 29 additions & 6 deletions lib/TileRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ var TileRequest = module.exports = function(x, y, z, layer, filename, headers, m
this.filename = filename;
this.headers = headers || {};
this.method = method || 'GET';
this.hasFilename = true;
this.qs = qs;
};

Expand All @@ -21,7 +22,7 @@ TileRequest.parse = function(url, headers, method) {
// query string
var qs, index_qs = url.indexOf('?');
if (index_qs > -1) {
qs = url.substring(index_qs+1);
qs = url.substring(index_qs + 1);
}

// strip off query string
Expand All @@ -37,17 +38,37 @@ TileRequest.parse = function(url, headers, method) {
var layer = parts[0];
var z = Number(parts[1]);
var x = Number(parts[2]);
var hasFilename = isNaN(Number(parts[3])) ? false : true
var y = hasFilename ? Number(parts[3]) : Number(parts[3].split('.')[0]);
var filename = hasFilename ? parts[4] : '*.' + parts[3].split('.')[1];
var hasFilename = isNaN(Number(parts[3])) ? false : true;

var y, filename;
if (hasFilename) {
y = Number(parts[3]);
filename = parts[4];
} else {
// if the request is in the format of "/0/0/0.png", we set the filename to
// "t.png" in order to have compatiblity with caches and other plugins that
// expect a proper filename to present (like from the "/0/0/0/t.png" format).
var i0 = parts[3].indexOf('.'); // /0/0/0.png
var i1 = parts[3].indexOf('@'); // /0/0/[email protected]
var splitPoint = i0;
if (i0 > -1 && i1 > -1) {
splitPoint = Math.min(i0, i1);
} else if (i1 > -1) {
splitPoint = i1;
}
y = Number(parts[3].substring(0, splitPoint));
filename = 't' + parts[3].substring(splitPoint);
}

if (!isInt(x)) return;
if (!isInt(y)) return;
if (!isInt(z)) return;
if (!filename) return;
if (!layer) return;

return new TileRequest(x, y, z, layer, filename, headers, method, qs);
var req = new TileRequest(x, y, z, layer, filename, headers, method, qs);
req.hasFilename = hasFilename;
return req;
};

TileRequest.prototype.clone = function() {
Expand All @@ -57,5 +78,7 @@ TileRequest.prototype.clone = function() {
headers[k] = this.headers[k];
}
}
return new TileRequest(this.x, this.y, this.z, this.layer, this.filename, headers, this.method, this.qs);
var req = new TileRequest(this.x, this.y, this.z, this.layer, this.filename, headers, this.method, this.qs);
req.hasFilename = this.hasFilename;
return req;
};
18 changes: 17 additions & 1 deletion lib/TileServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,15 @@ TileServer.prototype._getTileHandler = function(req) {
if (layer.options.maxZoom && req.z > layer.options.maxZoom) return;
if (!layer._isInBounds(req)) return;

var route = layer.routes[req.filename];
var routeKey = req.filename;
if (!req.hasFilename) {
// when tile requests are in the "/0/0/0.ext" format (registered as `.route("*.ext")`),
// the convention is that req.filename is "t.ext" (for compatiblity) and req.hasFilename
// is false. to get the extension from the filename, we strip off the "t".
routeKey = '*' + req.filename.substring(1);
}

var route = layer.routes[routeKey];
if (!route) return;
return route.handler;
};
Expand Down Expand Up @@ -276,7 +284,15 @@ TileServer.prototype.serve = function(req, http, callback) {
* @return {void}
*/
TileServer.prototype.getTile = function(layer, filename, x, y, z, callback) {
// in 2.1.0 we introduced the idea of filenameless tiles (e.g /0/0/0.ext).
// in these cases, the route is registered as `.route('*.ext')`. this method
// should be called using '*.ext' as the filename.
var req = new TileRequest(x, y, z, layer, filename, {}, 'GET');
if (filename.charAt(0) === '*') {
req.filename = 't' + filename.substring(1);
req.hasFilename = false;
}

this.serve(req, false, function(status, buffer, headers) {
if (status === 200) {
callback(null, buffer, headers);
Expand Down
40 changes: 38 additions & 2 deletions test/TileRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ describe('TileRequest', function() {
assert.instanceOf(result, TileRequest);
assert.equal(result.layer, 'lyr');
assert.equal(result.filename, '[email protected]');
assert.equal(result.hasFilename, true);
assert.equal(result.z, 1);
assert.equal(result.x, 2);
assert.equal(result.y, 3);
Expand All @@ -23,6 +24,7 @@ describe('TileRequest', function() {
assert.instanceOf(result, TileRequest);
assert.equal(result.layer, 'lyr');
assert.equal(result.filename, '[email protected]');
assert.equal(result.hasFilename, true);
assert.equal(result.z, 1);
assert.equal(result.x, 2);
assert.equal(result.y, 3);
Expand All @@ -35,6 +37,7 @@ describe('TileRequest', function() {
assert.instanceOf(result, TileRequest);
assert.equal(result.layer, 'lyr1');
assert.equal(result.filename, '[email protected]');
assert.equal(result.hasFilename, true);
assert.equal(result.z, 1);
assert.equal(result.x, 2);
assert.equal(result.y, 3);
Expand All @@ -46,7 +49,21 @@ describe('TileRequest', function() {
result = TileRequest.parse('lyr1/1/2/3.png', null, null);
assert.instanceOf(result, TileRequest);
assert.equal(result.layer, 'lyr1');
assert.equal(result.filename, '*.png');
assert.equal(result.filename, 't.png');
assert.equal(result.hasFilename, false);
assert.equal(result.z, 1);
assert.equal(result.x, 2);
assert.equal(result.y, 3);
assert.equal(result.method, 'GET');
assert.equal(result.qs, undefined);
assert.deepEqual(result.headers, {});

// no filename (with resolution suffix)
result = TileRequest.parse('lyr1/1/2/[email protected]', null, null);
assert.instanceOf(result, TileRequest);
assert.equal(result.layer, 'lyr1');
assert.equal(result.filename, '[email protected]');
assert.equal(result.hasFilename, false);
assert.equal(result.z, 1);
assert.equal(result.x, 2);
assert.equal(result.y, 3);
Expand Down Expand Up @@ -74,7 +91,7 @@ describe('TileRequest', function() {
});
});
describe('clone()', function() {
it('should return different, but identical, instance', function() {
it('should return different, but identical, instance (with filename)', function() {
var original = TileRequest.parse('lyr1/1/2/3/[email protected]?query=1&test=2', {'x-tilestrata-skipcache': '1'}, 'GET');

var clone = original.clone();
Expand All @@ -87,6 +104,25 @@ describe('TileRequest', function() {
assert.equal(clone.x, 2);
assert.equal(clone.y, 3);
assert.equal(clone.qs, 'query=1&test=2');
assert.equal(clone.hasFilename, true);

assert.notEqual(clone.headers, original.headers);
assert.deepEqual(clone.headers, {'x-tilestrata-skipcache': '1'});
});
it('should return different, but identical, instance (without filename)', function() {
var original = TileRequest.parse('lyr1/1/2/[email protected]?query=1&test=2', {'x-tilestrata-skipcache': '1'}, 'GET');

var clone = original.clone();
assert.instanceOf(clone, TileRequest);
assert.notEqual(clone, original);
assert.equal(clone.layer, 'lyr1');
assert.equal(clone.method, 'GET');
assert.equal(clone.filename, '[email protected]');
assert.equal(clone.z, 1);
assert.equal(clone.x, 2);
assert.equal(clone.y, 3);
assert.equal(clone.qs, 'query=1&test=2');
assert.equal(clone.hasFilename, false);

assert.notEqual(clone.headers, original.headers);
assert.deepEqual(clone.headers, {'x-tilestrata-skipcache': '1'});
Expand Down
77 changes: 77 additions & 0 deletions test/TileServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,56 @@ describe('TileServer', function() {
done();
});
});
it('should handle requests without a filename', function(done) {
var server = new TileServer();
server.layer('layer').route('*.png').use({
serve: function(_server, _req, callback) {
assert.equal(_server, server);
assert.instanceOf(_req, TileRequest);
assert.equal(_req.filename, 't.png');
assert.equal(_req.hasFilename, false);
callback(null, new Buffer('response', 'utf8'), {'X-Test': 'hello'});
}
});

var req = TileRequest.parse('/layer/1/2/3.png', {}, 'GET');
server.serve(req, false, function(status, buffer, headers) {
assert.equal(status, 200);
assert.equal(buffer.toString('utf8'), 'response');
assert.deepEqual(headers, {
'X-Powered-By': HEADER_XPOWEREDBY,
'X-Test': 'hello',
'Content-Length': 8,
'Cache-Control': HEADER_CACHECONTROL
});
done();
});
});
it('should handle requests without a filename (with resolution)', function(done) {
var server = new TileServer();
server.layer('layer').route('*@2x.png').use({
serve: function(_server, _req, callback) {
assert.equal(_server, server);
assert.instanceOf(_req, TileRequest);
assert.equal(_req.filename, '[email protected]');
assert.equal(_req.hasFilename, false);
callback(null, new Buffer('response', 'utf8'), {'X-Test': 'hello'});
}
});

var req = TileRequest.parse('/layer/1/2/[email protected]', {}, 'GET');
server.serve(req, false, function(status, buffer, headers) {
assert.equal(status, 200);
assert.equal(buffer.toString('utf8'), 'response');
assert.deepEqual(headers, {
'X-Powered-By': HEADER_XPOWEREDBY,
'X-Test': 'hello',
'Content-Length': 8,
'Cache-Control': HEADER_CACHECONTROL
});
done();
});
});
});
describe('getTile()', function() {
it('should return error if tile unavailable', function(done) {
Expand Down Expand Up @@ -499,6 +549,33 @@ describe('TileServer', function() {
done();
});
});
it('should return tile if available (no filename)', function(done) {
var _served = false;
var server = new TileServer();
server.layer('layer').route('*@2x.png').use({
serve: function(_server, _req, callback) {
_served = true;
assert.equal(_req.x, 2);
assert.equal(_req.y, 3);
assert.equal(_req.z, 1);
callback(null, new Buffer('result', 'utf8'), {'X-Test': 'hello'});
}
});

server.getTile('layer', '*@2x.png', 2, 3, 1, function(err, buffer, headers) {
assert.isTrue(_served);
assert.isNull(err);
assert.instanceOf(buffer, Buffer);
assert.equal(buffer.toString('utf8'), 'result');
assert.deepEqual(headers, {
'X-Powered-By': HEADER_XPOWEREDBY,
'X-Test': 'hello',
'Content-Length': 6,
'Cache-Control': HEADER_CACHECONTROL
});
done();
});
});
});
describe('initialize()', function() {
it('should initialize each layer provider', function(done) {
Expand Down

0 comments on commit e002a71

Please sign in to comment.