Compare commits

...

24 Commits

Author SHA1 Message Date
Petr Sloup
074c873826 Support optional bearing and pitch in center-based static requests 2016-03-11 12:10:22 +01:00
Petr Sloup
72ad669502 Add tests to .dockerignore 2016-03-11 11:58:36 +01:00
Petr Sloup
2210ea2f35 Version v0.0.2 2016-03-11 11:44:38 +01:00
Petr Sloup
a39aa0bd8a Update README 2016-03-11 11:44:33 +01:00
Petr Sloup
1c73c14d84 Update and add tests 2016-03-11 11:27:17 +01:00
Petr Sloup
8a46bd8b88 Major cleaning of paths and urls 2016-03-11 10:50:33 +01:00
Petr Sloup
d1e33d04cb Add link index.html -> styles.html 2016-03-11 10:48:42 +01:00
Petr Sloup
06b88bbbe7 Font concatenation 2016-03-11 10:06:34 +01:00
Petr Sloup
946cb2ca5f Serve fonts only if serving some styles 2016-03-11 09:52:19 +01:00
Petr Sloup
d742672238 Serve fonts 2016-03-11 09:48:35 +01:00
Petr Sloup
b98b7244f6 Correctly serve sprites 2016-03-11 09:16:28 +01:00
Petr Sloup
d4fa224d04 Basic viewer for vector tiles based on mapbox-gl-js v0.15.0 2016-03-11 08:57:06 +01:00
Petr Sloup
4c40700bac Major refactoring of the urls (#5) 2016-03-10 18:26:26 +01:00
Petr Sloup
6f644a4c03 Serve TileJSONs on /{prefix}.json 2016-03-09 19:18:59 +01:00
Petr Sloup
9efa22b52b Add more tests + better structuring 2016-03-09 17:26:34 +01:00
Petr Sloup
a0007b42f9 Do not allow dot character in request extensions 2016-03-09 17:26:34 +01:00
Petr Sloup
a495993e68 Fix behavior of area-based static maps 2016-03-09 17:26:34 +01:00
Petr Sloup
ad867f305b Add build status badge to README.md 2016-03-09 13:26:17 +01:00
Petr Sloup
d6e17c1a3a More tests for the endpoints 2016-03-09 13:22:06 +01:00
Petr Sloup
9736649244 Stronger checking of request parameters and stability improvements 2016-03-09 13:21:34 +01:00
Petr Sloup
832b2d22be Do not invoke done in test setup 2016-03-09 12:34:16 +01:00
Petr Sloup
47f6c90a98 Add .travis.yml 2016-03-09 12:26:05 +01:00
Petr Sloup
77755b548b Add first batch of tests 2016-03-09 11:26:02 +01:00
Petr Sloup
7ca7fc721f Change server behavior to allow for testing 2016-03-09 11:09:06 +01:00
20 changed files with 1383 additions and 173 deletions

View File

@@ -1,3 +1,4 @@
.git .git
node_modules node_modules
test_data test_data
test

20
.travis.yml Normal file
View File

@@ -0,0 +1,20 @@
language: node_js
node_js:
- "5"
env:
- CXX=g++-4.8
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8
before_install:
- sudo apt-get update -qq
- sudo apt-get install -qq xvfb
install:
- npm install
- wget https://github.com/klokantech/tileserver-gl/releases/download/v0.0.2/test_data.zip
- unzip -q test_data.zip -d test_data
script:
- xvfb-run --server-args="-screen 0 1024x768x24" npm test

View File

@@ -1,4 +1,5 @@
# tileserver-gl # tileserver-gl
[![Build Status](https://travis-ci.org/klokantech/tileserver-gl.svg?branch=master)](https://travis-ci.org/klokantech/tileserver-gl)
## Installation ## Installation
@@ -10,64 +11,86 @@
- `npm install` - `npm install`
- `node src/main.js` - `node src/main.js`
## Sample data
Sample data can be downloaded at https://github.com/klokantech/tileserver-gl/releases/download/v0.0.2/test_data.zip
#### Usage
- unpack somewhere and `cd` to the directory
- `docker run -it -v $(pwd):/data -p 8080:80 klokantech/tileserver-gl`
- (or `node path/to/repo/src/main.js`)
#### Data
- tiles from http://osm2vectortiles.org/
- styles modified from https://github.com/klokantech/osm2vectortiles-gl-styles
## Configuration ## Configuration
Create `config.json` file in the root directory. Create `config.json` file in the root directory.
The config file can contain definition of several paths where the tiles will be served. The config file can contain definition of several paths where the tiles will be served.
Every path needs to have `root` specified. All other paths (in the config (`style`) **and** in the style (`sprites`, `glyphs`, `sources`, ...)) are relative to this root.
For raster endpoints specify `style`, for serving raw `pbf` vector tiles specify `mbtiles` property.
Alternative `domains` can be specified (array or comma-separated string). These will be used to generate tile urls for `index.json`.
Every path can also have `options` object and its content is directly copied into served `index.json`.
The `options.format` can be used to modify the extension in tile urls inside `index.json`, but the server will still serve all the supported formats.
### Example configuration file ### Example configuration file
Example styles can be downloaded from https://github.com/klokantech/osm2vectortiles-gl-styles.
Rendered vector tiles can be found at http://osm2vectortiles.org/downloads/.
```json ```json
{ {
"/basic": { "options": {
"root": "test_data", "paths": {
"style": "styles/basic-v8.json", "root": "",
"fonts": "glyphs",
"sprites": "sprites",
"styles": "styles",
"mbtiles": ""
},
"domains": [ "domains": [
"localhost:8080", "localhost:8080",
"127.0.0.1:8080" "127.0.0.1:8080"
], ]
"options": { },
"type": "overlay", "styles": {
"bounds": [5.8559113, 45.717995, 10.5922941, 47.9084648] "test": {
"style": "basic-v8.json",
"tilejson": {
"type": "overlay",
"bounds": [8.44806, 47.32023, 8.62537, 47.43468]
}
},
"hybrid": {
"style": "satellite-hybrid-v8.json",
"raster": false,
"tilejson": {
"format": "webp",
"center": [8.536715, 47.377455, 6]
}
},
"streets": {
"style": "streets-v8.json",
"vector": false,
"tilejson": {
"center": [8.536715, 47.377455, 6]
}
} }
}, },
"/hybrid": { "vector": {
"root": "test_data", "zurich-vector": {
"style": "styles/satellite-hybrid-v8.json", "mbtiles": "zurich.mbtiles"
"options": {
"format": "webp"
} }
},
"/switzerland-vector": {
"root": "test_data",
"mbtiles": "switzerland.mbtiles"
} }
} }
``` ```
**Note**: To specify local mbtiles as source of the vector tiles inside the style, use urls with `mbtiles` protocol with path relative to the `root`. (For example `mbtiles://switzerland.mbtiles`) **Note**: To specify local mbtiles as source of the vector tiles inside the style, use urls with `mbtiles` protocol with path relative to the `cwd + options.paths.root + options.paths.mbtiles`. (For example `mbtiles://switzerland.mbtiles`)
## Available URLs ## Available URLs
- If you visit the server on the configured port (default 8080) you should see your maps appearing in the browser. - If you visit the server on the configured port (default 8080) you should see your maps appearing in the browser.
- The tiles itself are served at `/{basename}/{z}/{x}/{y}[@2x].{format}` - Style is served at `/styles/{id}.json` (+ array at `/styles.json`)
- Sprites at `/styles/{id}/sprite[@2x].{format}`
- Fonts at `/fonts/{fontstack}/{start}-{end}.pbf`
- Rasterized tiles are at `/raster/{id}/{z}/{x}/{y}[@2x].{format}`
- The optional `@2x` (or `@3x`) part can be used to render HiDPI (retina) tiles - The optional `@2x` (or `@3x`) part can be used to render HiDPI (retina) tiles
- Static images (only for raster tiles) are rendered at: - Available formats: `png`, `jpg` (`jpeg`), `webp`
- `/{basename}/static/{lon},{lat},{zoom}/{width}x{height}[@2x].{format}` (center-based) - TileJSON at `/raster/{id}.json`
- `/{basename}/static/{minx},{miny},{maxx},{maxy}/{zoom}[@2x].{format}` (area-based) - Static images are rendered at:
- TileJSON at `/{basename}/index.json` - `/static/{id}/{lon},{lat},{zoom}/{width}x{height}[@2x].{format}` (center-based)
- Array of all TileJSONs at `/index.json` - `/static/{id}/{minx},{miny},{maxx},{maxy}/{zoom}[@2x].{format}` (area-based)
- Available formats: - Vector tiles at `/vector/{mbtiles}/{z}/{x}/{y}.pbf`
- raster: `png`, `jpg` (`jpeg`), `webp` - TileJSON at `/vector/{mbtiles}.json`
- vector: `pbf` - Array of all TileJSONs at `/index.json` (`/raster.json`; `/vector.json`)

View File

@@ -1,6 +1,6 @@
{ {
"name": "tileserver-gl", "name": "tileserver-gl",
"version": "0.0.1", "version": "0.0.2",
"description": "Map tile server for JSON GL styles - serverside generated raster tiles", "description": "Map tile server for JSON GL styles - serverside generated raster tiles",
"main": "src/main.js", "main": "src/main.js",
"authors": [ "authors": [
@@ -10,6 +10,9 @@
"type": "git", "type": "git",
"url": "https://github.com/klokantech/tileserver-gl.git" "url": "https://github.com/klokantech/tileserver-gl.git"
}, },
"scripts": {
"test": "mocha test/**.js"
},
"dependencies": { "dependencies": {
"async": "1.5.2", "async": "1.5.2",
"advanced-pool": "0.3.1", "advanced-pool": "0.3.1",
@@ -23,5 +26,10 @@
"request": "2.69.0", "request": "2.69.0",
"sharp": "0.13.1", "sharp": "0.13.1",
"sphericalmercator": "1.0.4" "sphericalmercator": "1.0.4"
},
"devDependencies": {
"should": "^8.2.2",
"mocha": "^2.4.5",
"supertest": "^1.2.0"
} }
} }

View File

@@ -18,13 +18,15 @@
} }
#header {height:120px;} #header {height:120px;}
#sidebar, #code, #map, #wall {top:120px;} #sidebar, #code, #map, #wall {top:120px;}
#styles-link {position:absolute;top:90px;left:0;right:0;text-align:center;z-index:10000;font-size:16px;color:#eee;}
</style> </style>
<body> <body>
<script> <script>
tileserver({ tileserver({
index: "index.json" + location.search, index: "index.json" + location.search,
tilejson: "%n/index.json" + location.search tilejson: "raster/%n.json" + location.search
}); });
</script> </script>
<a href="/styles.html" id="styles-link">View available styles</a>
</body> </body>
</html> </html>

244
public/mapbox-gl.css Normal file
View File

@@ -0,0 +1,244 @@
.mapboxgl-map {
font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
overflow: hidden;
position: relative;
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
.mapboxgl-canvas-container.mapboxgl-interactive,
.mapboxgl-ctrl-nav-compass {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.mapboxgl-canvas-container.mapboxgl-interactive:active,
.mapboxgl-ctrl-nav-compass:active {
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
.mapboxgl-ctrl-top-left,
.mapboxgl-ctrl-top-right,
.mapboxgl-ctrl-bottom-left,
.mapboxgl-ctrl-bottom-right { position:absolute; }
.mapboxgl-ctrl-top-left { top:0; left:0; }
.mapboxgl-ctrl-top-right { top:0; right:0; }
.mapboxgl-ctrl-bottom-left { bottom:0; left:0; }
.mapboxgl-ctrl-bottom-right { right:0; bottom:0; }
.mapboxgl-ctrl { clear:both; }
.mapboxgl-ctrl-top-left .mapboxgl-ctrl { margin:10px 0 0 10px; float:left; }
.mapboxgl-ctrl-top-right .mapboxgl-ctrl{ margin:10px 10px 0 0; float:right; }
.mapboxgl-ctrl-bottom-left .mapboxgl-ctrl { margin:0 0 10px 10px; float:left; }
.mapboxgl-ctrl-bottom-right .mapboxgl-ctrl { margin:0 10px 10px 0; float:right; }
.mapboxgl-ctrl-group {
border-radius: 4px;
-moz-box-shadow: 0px 0px 2px rgba(0,0,0,0.1);
-webkit-box-shadow: 0px 0px 2px rgba(0,0,0,0.1);
box-shadow: 0px 0px 0px 2px rgba(0,0,0,0.1);
overflow: hidden;
background: #fff;
}
.mapboxgl-ctrl-group > button {
width: 30px;
height: 30px;
display: block;
padding: 0;
outline: none;
border: none;
border-bottom: 1px solid #ddd;
box-sizing: border-box;
background-color: rgba(0,0,0,0);
cursor: pointer;
}
/* https://bugzilla.mozilla.org/show_bug.cgi?id=140562 */
.mapboxgl-ctrl > button::-moz-focus-inner {
border: 0;
padding: 0;
}
.mapboxgl-ctrl > button:last-child {
border-bottom: 0;
}
.mapboxgl-ctrl > button:hover {
background-color: rgba(0,0,0,0.05);
}
.mapboxgl-ctrl-icon,
.mapboxgl-ctrl-icon > div.arrow {
speak: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-out {
padding: 5px;
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%270%200%2020%2020%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%0A%20%20%3Cpath%20style%3D%27fill%3A%23333333%3B%27%20d%3D%27m%207%2C9%20c%20-0.554%2C0%20-1%2C0.446%20-1%2C1%200%2C0.554%200.446%2C1%201%2C1%20l%206%2C0%20c%200.554%2C0%201%2C-0.446%201%2C-1%200%2C-0.554%20-0.446%2C-1%20-1%2C-1%20z%27%20%2F%3E%0A%3C%2Fsvg%3E%0A");
}
.mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-in {
padding: 5px;
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%270%200%2020%2020%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%0A%20%20%3Cpath%20style%3D%27fill%3A%23333333%3B%27%20d%3D%27M%2010%206%20C%209.446%206%209%206.4459904%209%207%20L%209%209%20L%207%209%20C%206.446%209%206%209.446%206%2010%20C%206%2010.554%206.446%2011%207%2011%20L%209%2011%20L%209%2013%20C%209%2013.55401%209.446%2014%2010%2014%20C%2010.554%2014%2011%2013.55401%2011%2013%20L%2011%2011%20L%2013%2011%20C%2013.554%2011%2014%2010.554%2014%2010%20C%2014%209.446%2013.554%209%2013%209%20L%2011%209%20L%2011%207%20C%2011%206.4459904%2010.554%206%2010%206%20z%27%20%2F%3E%0A%3C%2Fsvg%3E%0A");
}
.mapboxgl-ctrl-icon.mapboxgl-ctrl-compass > div.arrow {
width: 20px;
height: 20px;
margin: 5px;
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2020%2020%27%3E%0A%09%3Cpolygon%20fill%3D%27%23333333%27%20points%3D%276%2C9%2010%2C1%2014%2C9%27%2F%3E%0A%09%3Cpolygon%20fill%3D%27%23CCCCCC%27%20points%3D%276%2C11%2010%2C19%2014%2C11%20%27%2F%3E%0A%3C%2Fsvg%3E");
background-repeat: no-repeat;
}
.mapboxgl-ctrl.mapboxgl-ctrl-attrib {
padding: 0 5px;
background-color: rgba(255,255,255,0.5);
margin: 0;
}
.mapboxgl-ctrl-attrib a {
color: rgba(0,0,0,0.75);
text-decoration: none;
}
.mapboxgl-ctrl-attrib a:hover {
color: inherit;
text-decoration: underline;
}
.mapboxgl-ctrl-attrib .mapbox-improve-map {
font-weight: bold;
margin-left: 2px;
}
.mapboxgl-popup {
position: absolute;
display: -webkit-flex;
display: flex;
will-change: transform;
pointer-events: none;
}
.mapboxgl-popup-anchor-top,
.mapboxgl-popup-anchor-top-left,
.mapboxgl-popup-anchor-top-right {
-webkit-flex-direction: column;
flex-direction: column;
}
.mapboxgl-popup-anchor-bottom,
.mapboxgl-popup-anchor-bottom-left,
.mapboxgl-popup-anchor-bottom-right {
-webkit-flex-direction: column-reverse;
flex-direction: column-reverse;
}
.mapboxgl-popup-anchor-left {
-webkit-flex-direction: row;
flex-direction: row;
}
.mapboxgl-popup-anchor-right {
-webkit-flex-direction: row-reverse;
flex-direction: row-reverse;
}
.mapboxgl-popup-tip {
width: 0;
height: 0;
border: 10px solid transparent;
z-index: 1;
}
.mapboxgl-popup-anchor-top .mapboxgl-popup-tip {
-webkit-align-self: center;
align-self: center;
border-top: none;
border-bottom-color: #fff;
}
.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip {
-webkit-align-self: flex-start;
align-self: flex-start;
border-top: none;
border-left: none;
border-bottom-color: #fff;
}
.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip {
-webkit-align-self: flex-end;
align-self: flex-end;
border-top: none;
border-right: none;
border-bottom-color: #fff;
}
.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip {
-webkit-align-self: center;
align-self: center;
border-bottom: none;
border-top-color: #fff;
}
.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip {
-webkit-align-self: flex-start;
align-self: flex-start;
border-bottom: none;
border-left: none;
border-top-color: #fff;
}
.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip {
-webkit-align-self: flex-end;
align-self: flex-end;
border-bottom: none;
border-right: none;
border-top-color: #fff;
}
.mapboxgl-popup-anchor-left .mapboxgl-popup-tip {
-webkit-align-self: center;
align-self: center;
border-left: none;
border-right-color: #fff;
}
.mapboxgl-popup-anchor-right .mapboxgl-popup-tip {
-webkit-align-self: center;
align-self: center;
border-right: none;
border-left-color: #fff;
}
.mapboxgl-popup-close-button {
position: absolute;
right: 0;
top: 0;
border: none;
border-radius: 0 3px 0 0;
cursor: pointer;
background-color: rgba(0,0,0,0);
}
.mapboxgl-popup-close-button:hover {
background-color: rgba(0,0,0,0.05);
}
.mapboxgl-popup-content {
position: relative;
background: #fff;
border-radius: 3px;
box-shadow: 0 1px 2px rgba(0,0,0,0.10);
padding: 10px 10px 15px;
pointer-events: auto;
}
.mapboxgl-popup-anchor-top-left .mapboxgl-popup-content {
border-top-left-radius: 0;
}
.mapboxgl-popup-anchor-top-right .mapboxgl-popup-content {
border-top-right-radius: 0;
}
.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-content {
border-bottom-left-radius: 0;
}
.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-content {
border-bottom-right-radius: 0;
}
.mapboxgl-crosshair,
.mapboxgl-crosshair .mapboxgl-interactive,
.mapboxgl-crosshair .mapboxgl-interactive:active {
cursor: crosshair;
}
.mapboxgl-boxzoom {
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
background: #fff;
border: 2px dotted #202020;
opacity: 0.5;
}
@media print {
.mapbox-improve-map {
display:none;
}
}

338
public/mapbox-gl.js Normal file

File diff suppressed because one or more lines are too long

58
public/styles.html Normal file
View File

@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="mapbox-gl.css" />
<script src="mapbox-gl.js"></script>
<title>Offline vector tiles</title>
<style>
body { margin:0; padding:0; }
#map { position:absolute; top:0; bottom:0; width:100%; }
#dropdown { position: absolute; top: 10px; left:10px; }
</style>
<body>
<div id='map'></div>
<select id='dropdown'></select>
<script>
var styles;
var request = new XMLHttpRequest();
request.responseType = 'json';
request.open('GET', '/styles.json', true);
request.onload = function(e) {
if (request.readyState != 4) return;
if (request.status === 200) {
styles = request.response;
}
var map = new mapboxgl.Map({
container: 'map',
style: 'styles/' + styles[0].id + '.json',
center: [0, 0],
zoom: 0,
hash: true
});
map.addControl(new mapboxgl.Navigation());
var select = document.getElementById('dropdown');
for (var i in styles) {
var style = styles[i];
var el = document.createElement('option');
el.textContent = style.name + ' (' + style.id + '.json)';
el.value = style.id;
select.appendChild(el);
};
select.onchange = function() {
mapboxgl.util.getJSON(
'styles/' + document.getElementById('dropdown').value + '.json',
function (err, style) {
if (err) throw err;
map.setStyle(style);
});
}
};
request.send(null);
</script>
</body>
</html>

60
src/serve_font.js Normal file
View File

@@ -0,0 +1,60 @@
'use strict';
var async = require('async'),
path = require('path'),
fs = require('fs');
var clone = require('clone'),
express = require('express');
module.exports = function(options, allowedFonts) {
var app = express().disable('x-powered-by');
var fontPath = options.paths.fonts;
var getFontPbf = function(name, range, callback) {
// if some of the files failed to load (does not exist or not allowed),
// return empty buffer so the other fonts can still work
if (allowedFonts[name]) {
var filename = path.join(fontPath, name, range + '.pbf');
return fs.readFile(filename, function(err, data) {
if (err) {
console.log('Font load error:', filename);
return callback(null, new Buffer([]));
} else {
return callback(null, data);
}
});
} else {
return callback(null, new Buffer([]));
}
};
app.get('/fonts/:fontstack/:range([\\d]+-[\\d]+).pbf',
function(req, res, next) {
var fontstack = decodeURI(req.params.fontstack);
var range = req.params.range;
var fonts = fontstack.split(',');
var queue = [];
fonts.forEach(function(font) {
queue.push(function(callback) {
getFontPbf(font, range, callback);
});
});
return async.parallel(queue, function(err, results) {
var concated = Buffer.concat(results);
if (err || concated.length == 0) {
return res.status(400).send('');
} else {
res.header('Content-type', 'application/x-protobuf');
return res.send(concated);
}
});
});
return app;
};

View File

@@ -31,18 +31,15 @@ mbgl.on('message', function(e) {
} }
}); });
module.exports = function(maps, options, prefix) { module.exports = function(options, repo, params, id) {
var app = express().disable('x-powered-by'), var app = express().disable('x-powered-by');
domains = options.domains,
tilePath = '/{z}/{x}/{y}.{format}';
var rootPath = path.join(process.cwd(), options.root || ''); var rootPath = options.paths.root;
var styleUrl = options.style; var styleFile = params.style;
var map = { var map = {
renderers: [], renderers: [],
sources: {}, sources: {}
tileJSON: {}
}; };
var styleJSON; var styleJSON;
@@ -53,8 +50,10 @@ module.exports = function(maps, options, prefix) {
request: function(req, callback) { request: function(req, callback) {
var protocol = req.url.split(':')[0]; var protocol = req.url.split(':')[0];
//console.log('Handling request:', req); //console.log('Handling request:', req);
if (protocol == req.url) { if (protocol == 'sprites' || protocol == 'fonts') {
fs.readFile(path.join(rootPath, unescape(req.url)), function(err, data) { var dir = options.paths[protocol];
var file = unescape(req.url).substring(protocol.length + 3);
fs.readFile(path.join(dir, file), function(err, data) {
callback(err, { data: data }); callback(err, { data: data });
}); });
} else if (protocol == 'mbtiles') { } else if (protocol == 'mbtiles') {
@@ -128,19 +127,22 @@ module.exports = function(maps, options, prefix) {
}); });
}; };
styleJSON = require(path.join(rootPath, styleUrl)); styleJSON = require(path.join(options.paths.styles, styleFile));
styleJSON.sprite = 'sprites://' + path.basename(styleFile, '.json');
styleJSON.glyphs = 'fonts://{fontstack}/{range}.pbf';
map.tileJSON = { var tileJSON = {
'tilejson': '2.0.0', 'tilejson': '2.0.0',
'name': styleJSON.name, 'name': styleJSON.name,
'basename': prefix.substr(1), 'basename': id,
'minzoom': 0, 'minzoom': 0,
'maxzoom': 20, 'maxzoom': 20,
'bounds': [-180, -85.0511, 180, 85.0511], 'bounds': [-180, -85.0511, 180, 85.0511],
'format': 'png', 'format': 'png',
'type': 'baselayer' 'type': 'baselayer'
}; };
Object.assign(map.tileJSON, options.options || {}); Object.assign(tileJSON, params.tilejson || {});
tileJSON.tiles = params.domains || options.domains;
var queue = []; var queue = [];
Object.keys(styleJSON.sources).forEach(function(name) { Object.keys(styleJSON.sources).forEach(function(name) {
@@ -151,14 +153,15 @@ module.exports = function(maps, options, prefix) {
delete source.url; delete source.url;
queue.push(function(callback) { queue.push(function(callback) {
var mbtilesUrl = url.substring('mbtiles://'.length); var mbtilesFile = url.substring('mbtiles://'.length);
map.sources[name] = new mbtiles(path.join(rootPath, mbtilesUrl), function(err) { map.sources[name] = new mbtiles(
path.join(options.paths.mbtiles, mbtilesFile), function(err) {
map.sources[name].getInfo(function(err, info) { map.sources[name].getInfo(function(err, info) {
Object.assign(source, info); Object.assign(source, info);
source.basename = name; source.basename = name;
source.tiles = [ source.tiles = [
// meta url which will be detected when requested // meta url which will be detected when requested
'mbtiles://' + name + tilePath.replace('{format}', 'pbf') 'mbtiles://' + name + '/{z}/{x}/{y}.pbf'
]; ];
callback(null); callback(null);
}); });
@@ -174,22 +177,25 @@ module.exports = function(maps, options, prefix) {
map.renderers[3] = createPool(3, 2, 4); map.renderers[3] = createPool(3, 2, 4);
}); });
maps[prefix] = map; repo[id] = tileJSON;
var tilePattern = tilePath var tilePattern = '/raster/' + id + '/:z(\\d+)/:x(\\d+)/:y(\\d+)' +
.replace(/\.(?!.*\.)/, ':scale(' + SCALE_PATTERN + ')?.') ':scale(' + SCALE_PATTERN + ')?\.:format([\\w]+)';
.replace(/\./g, '\.')
.replace('{z}', ':z(\\d+)')
.replace('{x}', ':x(\\d+)')
.replace('{y}', ':y(\\d+)')
.replace('{format}', ':format([\\w\\.]+)');
var respondImage = function(z, lon, lat, width, height, scale, format, res, next) { var respondImage = function(z, lon, lat, bearing, pitch,
width, height, scale, format, res, next) {
if (Math.abs(lon) > 180 || Math.abs(lat) > 85.06) {
return res.status(400).send('Invalid center');
}
if (Math.min(width, height) <= 0 ||
Math.max(width, height) * scale > 6000) {
return res.status(400).send('Invalid size');
}
if (format == 'png' || format == 'webp') { if (format == 'png' || format == 'webp') {
} else if (format == 'jpg' || format == 'jpeg') { } else if (format == 'jpg' || format == 'jpeg') {
format = 'jpeg'; format = 'jpeg';
} else { } else {
return res.status(404).send('Invalid format'); return res.status(400).send('Invalid format');
} }
var pool = map.renderers[scale]; var pool = map.renderers[scale];
@@ -198,6 +204,8 @@ module.exports = function(maps, options, prefix) {
var params = { var params = {
zoom: mbglZ, zoom: mbglZ,
center: [lon, lat], center: [lon, lat],
bearing: bearing,
pitch: pitch,
width: width, width: width,
height: height height: height
}; };
@@ -246,31 +254,39 @@ module.exports = function(maps, options, prefix) {
y = req.params.y | 0, y = req.params.y | 0,
scale = getScale(req.params.scale), scale = getScale(req.params.scale),
format = req.params.format; format = req.params.format;
if (z < 0 || x < 0 || y < 0 ||
z > 20 || x >= Math.pow(2, z) || y >= Math.pow(2, z)) {
return res.status(404).send('Out of bounds');
}
var tileSize = 256; var tileSize = 256;
var tileCenter = mercator.ll([ var tileCenter = mercator.ll([
((x + 0.5) / (1 << z)) * (256 << z), ((x + 0.5) / (1 << z)) * (256 << z),
((y + 0.5) / (1 << z)) * (256 << z) ((y + 0.5) / (1 << z)) * (256 << z)
], z); ], z);
return respondImage(z, tileCenter[0], tileCenter[1], tileSize, tileSize, return respondImage(z, tileCenter[0], tileCenter[1], 0, 0,
scale, format, res, next); tileSize, tileSize, scale, format, res, next);
}); });
var staticPattern = var staticPattern =
'/static/%s:scale(' + SCALE_PATTERN + ')?\.:format([\\w\\.]+)'; '/static/' + id + '/%s:scale(' + SCALE_PATTERN + ')?\.:format([\\w]+)';
var centerPattern = var centerPattern =
util.format(':lon(%s),:lat(%s),:z(\\d+)/:width(\\d+)x:height(\\d+)', util.format(':lon(%s),:lat(%s),:z(\\d+):bearing(,%s)?:pitch(,%s)?/' +
FLOAT_PATTERN, FLOAT_PATTERN); ':width(\\d+)x:height(\\d+)',
FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN);
app.get(util.format(staticPattern, centerPattern), function(req, res, next) { app.get(util.format(staticPattern, centerPattern), function(req, res, next) {
var z = req.params.z | 0, var z = req.params.z | 0,
x = +req.params.lon, x = +req.params.lon,
y = +req.params.lat, y = +req.params.lat,
bearing = +(req.params.bearing || ',0').substring(1),
pitch = +(req.params.pitch || ',0').substring(1),
w = req.params.width | 0, w = req.params.width | 0,
h = req.params.height | 0, h = req.params.height | 0,
scale = getScale(req.params.scale), scale = getScale(req.params.scale),
format = req.params.format; format = req.params.format;
return respondImage(z, x, y, w, h, scale, format, res, next); return respondImage(z, x, y, bearing, pitch,
w, h, scale, format, res, next);
}); });
var boundsPattern = var boundsPattern =
@@ -278,23 +294,24 @@ module.exports = function(maps, options, prefix) {
FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN); FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN);
app.get(util.format(staticPattern, boundsPattern), function(req, res, next) { app.get(util.format(staticPattern, boundsPattern), function(req, res, next) {
var bbox = [+req.params.minx, +req.params.miny,
+req.params.maxx, +req.params.maxy];
var z = req.params.z | 0, var z = req.params.z | 0,
x = ((+req.params.minx) + (+req.params.maxx)) / 2, x = (bbox[0] + bbox[2]) / 2,
y = ((+req.params.miny) + (+req.params.maxy)) / 2, y = (bbox[1] + bbox[3]) / 2;
w = req.params.width | 0, var minCorner = mercator.px([bbox[0], bbox[3]], z),
h = req.params.height | 0, maxCorner = mercator.px([bbox[2], bbox[1]], z);
var w = (maxCorner[0] - minCorner[0]) | 0,
h = (maxCorner[1] - minCorner[1]) | 0,
scale = getScale(req.params.scale), scale = getScale(req.params.scale),
format = req.params.format; format = req.params.format;
return respondImage(z, x, y, w, h, scale, format, res, next); return respondImage(z, x, y, 0, 0, w, h, scale, format, res, next);
}); });
app.get('/index.json', function(req, res, next) { app.get('/raster/' + id + '.json', function(req, res, next) {
var info = clone(map.tileJSON); var info = clone(tileJSON);
info.tiles = utils.getTileUrls(req, info.tiles,
info.tiles = utils.getTileUrls(req.protocol, domains, req.headers.host, 'raster/' + id, info.format);
prefix, tilePath, info.format,
req.query.key);
return res.send(info); return res.send(info);
}); });

82
src/serve_style.js Normal file
View File

@@ -0,0 +1,82 @@
'use strict';
var path = require('path'),
fs = require('fs');
var clone = require('clone'),
express = require('express');
module.exports = function(options, repo, params, id, reportVector, reportFont) {
var app = express().disable('x-powered-by');
var styleFile = path.join(options.paths.styles, params.style);
var styleJSON = clone(require(styleFile));
Object.keys(styleJSON.sources).forEach(function(name) {
var source = styleJSON.sources[name];
var url = source.url;
if (url.lastIndexOf('mbtiles:', 0) === 0) {
var mbtiles = url.substring('mbtiles://'.length);
var identifier = reportVector(mbtiles);
source.url = 'local://vector/' + identifier + '.json';
}
});
var findFontReferences = function(obj) {
Object.keys(obj).forEach(function(key) {
var value = obj[key];
if (key == 'text-font') {
if (value && value.length > 0) {
value.forEach(reportFont);
}
} else if (value && typeof value == 'object') {
findFontReferences(value);
}
});
};
styleJSON.layers.forEach(findFontReferences);
var spritePath = path.join(options.paths.sprites,
path.basename(styleFile, '.json'));
styleJSON.sprite = 'local://styles/' + id + '/sprite';
styleJSON.glyphs = 'local://fonts/{fontstack}/{range}.pbf';
repo[id] = styleJSON;
app.get('/styles/' + id + '.json', function(req, res, next) {
var fixUrl = function(url) {
return url.replace(
'local://', req.protocol + '://' + req.headers.host + '/');
};
var styleJSON_ = clone(styleJSON);
Object.keys(styleJSON_.sources).forEach(function(name) {
var source = styleJSON_.sources[name];
source.url = fixUrl(source.url);
});
styleJSON_.sprite = fixUrl(styleJSON_.sprite);
styleJSON_.glyphs = fixUrl(styleJSON_.glyphs);
return res.send(styleJSON_);
});
app.get('/styles/' + id + '/sprite:scale(@[23]x)?\.:format([\\w]+)',
function(req, res, next) {
var scale = req.params.scale,
format = req.params.format;
var filename = spritePath + (scale || '') + '.' + format;
return fs.readFile(filename, function(err, data) {
if (err) {
console.log('Sprite load error:', filename);
return res.status(404).send('File not found');
} else {
if (format == 'json') res.header('Content-type', 'application/json');
if (format == 'png') res.header('Content-type', 'image/png');
return res.send(data);
}
});
});
return app;
};

View File

@@ -9,79 +9,69 @@ var clone = require('clone'),
var utils = require('./utils'); var utils = require('./utils');
module.exports = function(maps, options, prefix) { module.exports = function(options, repo, params, id) {
var app = express().disable('x-powered-by'), var app = express().disable('x-powered-by');
domains = options.domains,
tilePath = '/{z}/{x}/{y}.pbf';
var rootPath = path.join(process.cwd(), options.root || ''); var mbtilesFile = params.mbtiles;
var tileJSON = {
var mbtilesPath = options.mbtiles; 'tiles': params.domains || options.domains
var map = {
tileJSON: {}
}; };
maps[prefix] = map;
var source = new mbtiles(path.join(rootPath, mbtilesPath), function(err) { repo[id] = tileJSON;
var source = new mbtiles(path.join(options.paths.mbtiles, mbtilesFile),
function(err) {
source.getInfo(function(err, info) { source.getInfo(function(err, info) {
map.tileJSON['name'] = prefix.substr(1); tileJSON['name'] = id;
Object.assign(map.tileJSON, info); Object.assign(tileJSON, info);
map.tileJSON['tilejson'] = '2.0.0'; tileJSON['tilejson'] = '2.0.0';
map.tileJSON['basename'] = prefix.substr(1); tileJSON['basename'] = id;
map.tileJSON['format'] = 'pbf'; tileJSON['format'] = 'pbf';
Object.assign(map.tileJSON, options.options || {}); Object.assign(tileJSON, params.tilejson || {});
}); });
}); });
var tilePattern = tilePath var tilePattern = '/vector/' + id + '/:z(\\d+)/:x(\\d+)/:y(\\d+).pbf';
.replace('{z}', ':z(\\d+)')
.replace('{x}', ':x(\\d+)')
.replace('{y}', ':y(\\d+)');
var getTile = function(z, x, y, callback) {
source.getTile(z, x, y, function(err, data, headers) {
if (err) {
callback(err);
} else {
var md5 = crypto.createHash('md5').update(data).digest('base64');
headers['content-md5'] = md5;
headers['content-type'] = 'application/x-protobuf';
headers['content-encoding'] = 'gzip';
callback(null, data, headers);
}
});
};
app.get(tilePattern, function(req, res, next) { app.get(tilePattern, function(req, res, next) {
var z = req.params.z | 0, var z = req.params.z | 0,
x = req.params.x | 0, x = req.params.x | 0,
y = req.params.y | 0; y = req.params.y | 0;
return getTile(z, x, y, function(err, data, headers) { if (z < tileJSON.minzoom || 0 || x < 0 || y < 0 ||
if (err) { z > tileJSON.maxzoom ||
return next(err); x >= Math.pow(2, z) || y >= Math.pow(2, z)) {
} return res.status(404).send('Out of bounds');
if (headers) { }
res.set(headers); source.getTile(z, x, y, function(err, data, headers) {
if (err) {
if (/does not exist/.test(err.message)) {
return res.status(404).send(err.message);
} else {
return res.status(500).send(err.message);
} }
} else {
var md5 = crypto.createHash('md5').update(data).digest('base64');
headers['content-md5'] = md5;
headers['content-type'] = 'application/x-protobuf';
headers['content-encoding'] = 'gzip';
res.set(headers);
if (data == null) { if (data == null) {
return res.status(404).send('Not found'); return res.status(404).send('Not found');
} else { } else {
return res.status(200).send(data); return res.status(200).send(data);
} }
}, res, next); }
});
}); });
app.get('/index.json', function(req, res, next) { app.get('/vector/' + id + '.json', function(req, res, next) {
var info = clone(map.tileJSON); var info = clone(tileJSON);
info.tiles = utils.getTileUrls(req, info.tiles,
info.tiles = utils.getTileUrls(req.protocol, domains, req.headers.host, 'vector/' + id, info.format);
prefix, tilePath, info.format,
req.query.key);
return res.send(info); return res.send(info);
}); });

View File

@@ -7,70 +7,151 @@ process.env.UV_THREADPOOL_SIZE =
var fs = require('fs'), var fs = require('fs'),
path = require('path'); path = require('path');
var async = require('async'), var clone = require('clone'),
clone = require('clone'),
cors = require('cors'), cors = require('cors'),
express = require('express'), express = require('express'),
morgan = require('morgan'); morgan = require('morgan');
var serve_raster = require('./serve_raster'), var serve_font = require('./serve_font'),
serve_raster = require('./serve_raster'),
serve_style = require('./serve_style'),
serve_vector = require('./serve_vector'), serve_vector = require('./serve_vector'),
utils = require('./utils'); utils = require('./utils');
module.exports = function(opts, callback) { module.exports = function(opts, callback) {
var app = express().disable('x-powered-by'), var app = express().disable('x-powered-by'),
maps = {}; serving = {
styles: {},
raster: {},
vector: {},
fonts: { // default fonts, always expose these (if they exist)
'Open Sans Regular': true,
'Arial Unicode MS Regular': true
}
};
app.enable('trust proxy'); app.enable('trust proxy');
callback = callback || function() {}; callback = callback || function() {};
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production' &&
process.env.NODE_ENV !== 'test') {
app.use(morgan('dev')); app.use(morgan('dev'));
} }
var configPath = path.resolve(opts.config), var configPath = path.resolve(opts.config),
config = require(configPath); config = require(configPath);
Object.keys(config).forEach(function(prefix) { var options = config.options || {};
if (config[prefix].cors !== false) { var paths = options.paths || {};
app.use(prefix, cors()); options.paths = paths;
paths.root = path.join(process.cwd(), paths.root || '');
paths.styles = path.join(paths.root, paths.styles || '');
paths.fonts = path.join(paths.root, paths.fonts || '');
paths.sprites = path.join(paths.root, paths.sprites || '');
paths.mbtiles = path.join(paths.root, paths.mbtiles || '');
var vector = clone(config.vector);
Object.keys(config.styles || {}).forEach(function(id) {
var item = config.styles[id];
if (!item.style || item.style.length == 0) {
console.log('Missing "style" property for ' + id);
return;
} }
if (config[prefix].style) { if (item.vector !== false) {
app.use(prefix, serve_raster(maps, config[prefix], prefix)); app.use('/', serve_style(options, serving.styles, item, id,
} else { function(mbtiles) {
app.use(prefix, serve_vector(maps, config[prefix], prefix)); var vectorItemId;
Object.keys(vector).forEach(function(id) {
if (vector[id].mbtiles == mbtiles) {
vectorItemId = id;
}
});
if (vectorItemId) { // mbtiles exist in the vector config
return vectorItemId;
} else {
var id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles;
while (vector[id]) id += '_';
vector[id] = {
'mbtiles': mbtiles
};
return id;
}
}, function(font) {
serving.fonts[font] = true;
}));
}
if (item.raster !== false) {
app.use('/', serve_raster(options, serving.raster, item, id));
} }
}); });
// serve index.html on the root if (Object.keys(serving.styles).length > 0) {
app.use('/', express.static(path.join(__dirname, '../public'))); // serve fonts only if serving some styles
app.use('/', serve_font(options, serving.fonts));
}
// aggregate index.json on root for multiple sources //TODO: cors
app.get('/index.json', function(req, res, next) {
var queue = [];
Object.keys(config).forEach(function(prefix) {
var map = maps[prefix];
queue.push(function(callback) {
var info = clone(map.tileJSON);
info.tiles = utils.getTileUrls( Object.keys(vector).forEach(function(id) {
req.protocol, config[prefix].domains, req.headers.host, var item = vector[id];
prefix, '/{z}/{x}/{y}.{format}', info.format, req.query.key); if (!item.mbtiles || item.mbtiles.length == 0) {
console.log('Missing "mbtiles" property for ' + id);
return;
}
callback(null, info); app.use('/', serve_vector(options, serving.vector, item, id));
});
app.get('/styles.json', function(req, res, next) {
var result = [];
Object.keys(serving.styles).forEach(function(id) {
var styleJSON = serving.styles[id];
result.push({
version: styleJSON.version,
name: styleJSON.name,
id: id,
url: req.protocol + '://' + req.headers.host + '/styles/' + id + '.json'
}); });
}); });
return async.parallel(queue, function(err, results) { res.send(result);
return res.send(results);
});
}); });
app.listen(process.env.PORT || opts.port, function() { var addTileJSONs = function(arr, req, type) {
Object.keys(serving[type]).forEach(function(id) {
var info = clone(serving[type][id]);
info.tiles = utils.getTileUrls(req, info.tiles,
type + '/' + id, info.format);
arr.push(info);
});
return arr;
};
app.get('/raster.json', function(req, res, next) {
res.send(addTileJSONs([], req, 'raster'));
});
app.get('/vector.json', function(req, res, next) {
res.send(addTileJSONs([], req, 'vector'));
});
app.get('/index.json', function(req, res, next) {
res.send(addTileJSONs(addTileJSONs([], req, 'raster'), req, 'vector'));
});
// serve viewer on the root
app.use('/', express.static(path.join(__dirname, '../public')));
var server = app.listen(process.env.PORT || opts.port, function() {
console.log('Listening at http://%s:%d/', console.log('Listening at http://%s:%d/',
this.address().address, this.address().port); this.address().address, this.address().port);
return callback(); return callback();
}); });
setTimeout(callback, 1000);
return {
app: app,
server: server
};
}; };

View File

@@ -1,7 +1,6 @@
'use strict'; 'use strict';
module.exports.getTileUrls = function( module.exports.getTileUrls = function(req, domains, path, format) {
protocol, domains, host, path, tilePath, format, key) {
if (domains) { if (domains) {
if (domains.constructor === String && domains.length > 0) { if (domains.constructor === String && domains.length > 0) {
@@ -9,19 +8,16 @@ module.exports.getTileUrls = function(
} }
} }
if (!domains || domains.length == 0) { if (!domains || domains.length == 0) {
domains = [host]; domains = [req.headers.host];
} }
var key = req.query.key;
var query = (key && key.length > 0) ? ('?key=' + key) : ''; var query = (key && key.length > 0) ? ('?key=' + key) : '';
if (path == '/') {
path = '';
}
var uris = []; var uris = [];
domains.forEach(function(domain) { domains.forEach(function(domain) {
uris.push(protocol + '://' + domain + path + uris.push(req.protocol + '://' + domain + '/' + path +
tilePath.replace('{format}', format).replace(/\/+/g, '/') + '/{z}/{x}/{y}.' + format + query);
query);
}); });
return uris; return uris;

69
test/metadata.js Normal file
View File

@@ -0,0 +1,69 @@
var testTileJSONArray = function(url) {
describe(url + ' is array of TileJSONs', function() {
it('is json', function(done) {
supertest(app)
.get(url)
.expect(200)
.expect('Content-Type', /application\/json/, done);
});
it('is non-empty array', function(done) {
supertest(app)
.get(url)
.expect(function(res) {
res.body.should.be.Array();
res.body.length.should.be.greaterThan(0);
}).end(done);
});
});
};
var testTileJSON = function(url, basename) {
describe(url + ' is TileJSON', function() {
it('is json', function(done) {
supertest(app)
.get(url)
.expect(200)
.expect('Content-Type', /application\/json/, done);
});
it('has valid basename and tiles', function(done) {
supertest(app)
.get(url)
.expect(function(res) {
res.body.basename.should.equal(basename);
res.body.tiles.length.should.be.greaterThan(0);
}).end(done);
});
});
};
describe('Metadata', function() {
testTileJSONArray('/index.json');
testTileJSONArray('/raster.json');
testTileJSONArray('/vector.json');
describe('/styles.json is valid array', function() {
it('is json', function(done) {
supertest(app)
.get('/styles.json')
.expect(200)
.expect('Content-Type', /application\/json/, done);
});
it('contains valid item', function(done) {
supertest(app)
.get('/styles.json')
.expect(function(res) {
res.body.should.be.Array();
res.body.length.should.be.greaterThan(0);
res.body[0].version.should.equal(8);
res.body[0].id.should.be.String();
res.body[0].name.should.be.String();
}).end(done);
});
});
testTileJSON('/raster/test.json', 'test');
testTileJSON('/vector/zurich-vector.json', 'zurich-vector');
});

20
test/setup.js Normal file
View File

@@ -0,0 +1,20 @@
process.env.NODE_ENV = 'test';
global.should = require('should');
global.supertest = require('supertest');
before(function() {
console.log('global setup');
process.chdir('test_data');
var running = require('../src/server')({
config: 'config.json',
port: 8888
});
global.app = running.app;
global.server = running.server;
});
after(function() {
console.log('global teardown');
global.server.close(function() { console.log('Done'); });
});

80
test/static.js Normal file
View File

@@ -0,0 +1,80 @@
var testStatic = function(prefix, q, format, status, scale, type) {
if (scale) q += '@' + scale + 'x';
var path = '/static/' + prefix + '/' + q + '.' + format;
it(path + ' returns ' + status, function(done) {
var test = supertest(app).get(path);
if (status) test.expect(status);
if (type) test.expect('Content-Type', type);
test.end(done);
});
};
describe('Static endpoints', function() {
describe('center-based', function() {
describe('valid requests', function() {
describe('various formats', function() {
testStatic('test', '0,0,0/256x256', 'png', 200, undefined, /image\/png/);
testStatic('test', '0,0,0/256x256', 'jpg', 200, undefined, /image\/jpeg/);
testStatic('test', '0,0,0/256x256', 'jpeg', 200, undefined, /image\/jpeg/);
testStatic('test', '0,0,0/256x256', 'webp', 200, undefined, /image\/webp/);
});
describe('different parameters', function() {
testStatic('test', '0,0,0/300x300', 'png', 200, 2);
testStatic('test', '0,0,0/300x300', 'png', 200, 3);
testStatic('test', '80,40,20/600x300', 'png', 200, 3);
testStatic('test', '8.5,40.5,20/300x150', 'png', 200, 3);
testStatic('test', '-8.5,-40.5,20/300x150', 'png', 200, 3);
testStatic('test', '8,40,2,0,0/300x150', 'png', 200);
testStatic('test', '8,40,2,180,45/300x150', 'png', 200, 2);
testStatic('test', '8,40,2,10/300x150', 'png', 200, 3);
testStatic('test', '8,40,2,10.3,20.4/300x300', 'png', 200);
testStatic('test', '0,0,2,390,120/300x300', 'png', 200);
});
});
describe('invalid requests return 4xx', function() {
testStatic('test', '190,0,0/256x256', 'png', 400);
testStatic('test', '0,86,0/256x256', 'png', 400);
testStatic('test', '80,40,20/0x0', 'png', 400);
testStatic('test', '0,0,0/256x256', 'gif', 400);
testStatic('test', '0,0,0/256x256', 'png', 404, 1);
testStatic('test', '0,0,-1/256x256', 'png', 404);
testStatic('test', '0,0,1.5/256x256', 'png', 404);
testStatic('test', '0,0,0/256.5x256.5', 'png', 404);
testStatic('test', '0,0,0,/256x256', 'png', 404);
testStatic('test', '0,0,0,0,/256x256', 'png', 404);
});
});
describe('area-based', function() {
describe('valid requests', function() {
describe('various formats', function() {
testStatic('test', '-180,-80,180,80/0', 'png', 200, undefined, /image\/png/);
testStatic('test', '-180,-80,180,80/0', 'jpg', 200, undefined, /image\/jpeg/);
testStatic('test', '-180,-80,180,80/0', 'jpeg', 200, undefined, /image\/jpeg/);
testStatic('test', '-180,-80,180,80/0', 'webp', 200, undefined, /image\/webp/);
});
describe('different parameters', function() {
testStatic('test', '-180,-90,180,90/0', 'png', 200, 2);
testStatic('test', '0,0,1,1/3', 'png', 200, 3);
testStatic('test', '-280,-80,0,80/0', 'png', 200);
});
});
describe('invalid requests return 4xx', function() {
testStatic('test', '0,87,1,88/5', 'png', 400);
testStatic('test', '18,-9,-18,9/0', 'png', 400);
testStatic('test', '0,0,1,1/1', 'gif', 400);
testStatic('test', '-180,-80,180,80/0.5', 'png', 404);
});
});
});

49
test/style.js Normal file
View File

@@ -0,0 +1,49 @@
var testIs = function(url, type, status) {
it(url + ' return ' + (status || 200) + ' and is ' + type.toString(),
function(done) {
supertest(app)
.get(url)
.expect(status || 200)
.expect('Content-Type', type, done);
});
};
describe('Styles', function() {
describe('/styles/test.json is valid style', function() {
testIs('/styles/test.json', /application\/json/);
it('contains expected properties', function(done) {
supertest(app)
.get('/styles/test.json')
.expect(function(res) {
res.body.version.should.equal(8);
res.body.name.should.be.String();
res.body.sources.should.be.Object();
res.body.glyphs.should.be.String();
res.body.sprite.should.be.String();
res.body.layers.should.be.Array();
}).end(done);
});
});
describe('/styles/streets.json is not served', function() {
testIs('/styles/streets.json', /./, 404);
});
describe('/styles/test/sprite[@2x].{format}', function() {
testIs('/styles/test/sprite.json', /application\/json/);
testIs('/styles/test/sprite@2x.json', /application\/json/);
testIs('/styles/test/sprite.png', /image\/png/);
testIs('/styles/test/sprite@2x.png', /image\/png/);
});
});
describe('Fonts', function() {
testIs('/fonts/Open Sans Bold/0-255.pbf', /application\/x-protobuf/);
testIs('/fonts/Open Sans Regular/65280-65533.pbf', /application\/x-protobuf/);
testIs('/fonts/Open Sans Bold,Open Sans Regular/0-255.pbf',
/application\/x-protobuf/);
testIs('/fonts/Nonsense,Open Sans Bold/0-255.pbf', /application\/x-protobuf/);
testIs('/fonts/Nonsense/0-255.pbf', /./, 400);
testIs('/fonts/Nonsense1,Nonsense2/0-255.pbf', /./, 400);
});

44
test/tiles_raster.js Normal file
View File

@@ -0,0 +1,44 @@
var testTile = function(prefix, z, x, y, format, status, scale, type) {
if (scale) y += '@' + scale + 'x';
var path = '/raster/' + prefix + '/' + z + '/' + x + '/' + y + '.' + format;
it(path + ' returns ' + status, function(done) {
var test = supertest(app).get(path);
test.expect(status);
if (type) test.expect('Content-Type', type);
test.end(done);
});
};
describe('Raster tiles', function() {
describe('valid requests', function() {
describe('various formats', function() {
testTile('test', 0, 0, 0, 'png', 200, undefined, /image\/png/);
testTile('test', 0, 0, 0, 'jpg', 200, undefined, /image\/jpeg/);
testTile('test', 0, 0, 0, 'jpeg', 200, undefined, /image\/jpeg/);
testTile('test', 0, 0, 0, 'webp', 200, undefined, /image\/webp/);
});
describe('different coordinates and scales', function() {
testTile('test', 1, 1, 1, 'png', 200);
testTile('test', 0, 0, 0, 'png', 200, 2);
testTile('test', 0, 0, 0, 'png', 200, 3);
testTile('test', 2, 1, 1, 'png', 200, 3);
});
});
describe('invalid requests return 4xx', function() {
testTile('non_existent', 0, 0, 0, 'png', 404);
testTile('test', -1, 0, 0, 'png', 404);
testTile('test', 25, 0, 0, 'png', 404);
testTile('test', 0, 1, 0, 'png', 404);
testTile('test', 0, 0, 1, 'png', 404);
testTile('test', 0, 0, 0, 'gif', 400);
testTile('test', 0, 0, 0, 'pbf', 400);
testTile('test', 0, 0, 0, 'png', 404, 1);
testTile('test', 0, 0, 0, 'png', 404, 4);
testTile('hybrid', 0, 0, 0, 'png', 404);
});
});

28
test/tiles_vector.js Normal file
View File

@@ -0,0 +1,28 @@
var testTile = function(prefix, z, x, y, status) {
var path = '/vector/' + prefix + '/' + z + '/' + x + '/' + y + '.pbf';
it(path + ' returns ' + status, function(done) {
var test = supertest(app).get(path);
if (status) test.expect(status);
if (status == 200) test.expect('Content-Type', /application\/x-protobuf/);
test.end(done);
});
};
var prefix = 'zurich-vector';
describe('Vector tiles', function() {
describe('existing tiles', function() {
testTile(prefix, 0, 0, 0, 200);
testTile(prefix, 14, 8581, 5738, 200);
});
describe('non-existent requests return 4xx', function() {
testTile('non_existent', 0, 0, 0, 404);
testTile(prefix, -1, 0, 0, 404); // err zoom
testTile(prefix, 20, 0, 0, 404); // zoom out of bounds
testTile(prefix, 0, 1, 0, 404);
testTile(prefix, 0, 0, 1, 404);
testTile(prefix, 14, 0, 0, 404); // non existent tile
});
});