Compare commits

..

32 Commits

Author SHA1 Message Date
Petr Sloup
8e289684d5 Update version to v0.0.3 2016-05-04 13:57:44 +02:00
Petr Sloup
59cc66095f Minor test update 2016-05-04 13:57:00 +02:00
Petr Sloup
187da7bb58 Add raster view for raster data 2016-05-04 13:53:47 +02:00
Petr Sloup
daa94dc806 Chain attributions from mbtiles into the tilejson of the rendered tiles 2016-05-04 13:13:37 +02:00
Petr Sloup
5d940066d9 Display filesizes of the mbtiles 2016-05-04 13:07:09 +02:00
Petr Sloup
10caaa1e8b Minor travis script fix 2016-05-03 17:49:04 +02:00
Petr Sloup
9edf7c0cae Fix empty raster tiles 2016-05-03 17:42:47 +02:00
Petr Sloup
f979e25fd7 Update travis script to use new data 2016-05-03 16:24:13 +02:00
Petr Sloup
d3a9b6cfbf Update README with links to tileserver-gl-data 2016-05-03 15:49:39 +02:00
Petr Sloup
2a55517a2d Update dependencies 2016-05-03 15:12:17 +02:00
Petr Sloup
927a3eda87 Minor README fix 2016-05-03 15:12:10 +02:00
Petr Sloup
54073cecce Simplify URL for "static"
/styles/{id}/rendered/static/... -> /styles/{id}/static/...
2016-04-22 15:50:56 +02:00
Petr Sloup
287a632295 Update readme 2016-04-22 12:37:39 +02:00
Petr Sloup
a25ce62662 New urls for source data tiles and rendered tiles 2016-04-22 12:33:20 +02:00
Petr Sloup
c0fb4fd400 Support for raster mbtiles (issue #13) 2016-04-21 18:23:13 +02:00
Petr Sloup
d486a8595b Use node 4 for travis and docker 2016-04-18 11:05:52 +02:00
Petr Sloup
3b92c6109b Do not install dev dependencies when build docker image 2016-04-18 10:47:45 +02:00
Petr Sloup
b6ad565e31 Update dependencies 2016-04-18 10:47:36 +02:00
Petr Sloup
f794f6b8ba Minor xray viewer fix 2016-04-15 15:42:54 +02:00
Dalibor Janák
640a18ca49 Css moved to external file + minor style fixes 2016-04-06 11:21:04 +02:00
Dalibor Janák
75f64924b5 Basic index styling #11 2016-04-05 16:19:34 +02:00
Petr Sloup
ec5d282d87 Update npm dependencies 2016-04-05 13:04:13 +02:00
Petr Sloup
bdea327437 Use CORS 2016-04-05 13:02:33 +02:00
Petr Sloup
6cf006ec50 More strict routing pattern matching (fix tests) 2016-03-17 11:45:55 +01:00
Petr Sloup
34befd43c9 Add xray viewer for vector data 2016-03-17 11:31:33 +01:00
Petr Sloup
c132d7fba8 Add redirect from /raster/:id/ to /styles/:id/ 2016-03-17 11:04:51 +01:00
Petr Sloup
62a6917778 Show proper thumbnails on index 2016-03-17 11:01:54 +01:00
Petr Sloup
403bc949a5 Pregenerate permalinks for the viewers 2016-03-17 10:51:16 +01:00
Petr Sloup
837cb7d1fb New index and viewer (+ templating system) 2016-03-16 20:47:11 +01:00
Petr Sloup
d0c0430dca Improve config usability (close #10) 2016-03-14 16:11:29 +01:00
Petr Sloup
1ade82bf05 More user-friendly error message for invalid config (close #7) 2016-03-11 20:29:21 +01:00
Petr Sloup
5a94689385 Make compressionLevel/quality configurable + change defaults 2016-03-11 16:40:05 +01:00
26 changed files with 844 additions and 232 deletions

View File

@@ -1,6 +1,6 @@
language: node_js language: node_js
node_js: node_js:
- "5" - "4"
env: env:
- CXX=g++-4.8 - CXX=g++-4.8
addons: addons:
@@ -14,7 +14,9 @@ before_install:
- sudo apt-get install -qq xvfb - sudo apt-get install -qq xvfb
install: install:
- npm install - npm install
- wget https://github.com/klokantech/tileserver-gl/releases/download/v0.0.2/test_data.zip - wget -O test_data.zip https://github.com/klokantech/tileserver-gl-data/archive/v0.0.3.zip
- unzip -q test_data.zip -d test_data - unzip -q test_data.zip -d tmp_test_data
- mkdir test_data
- mv tmp_test_data/tileserver-gl-data-*/* -t test_data
script: script:
- xvfb-run --server-args="-screen 0 1024x768x24" npm test - xvfb-run --server-args="-screen 0 1024x768x24" npm test

View File

@@ -7,13 +7,13 @@ RUN apt-get -qq update \
build-essential \ build-essential \
python \ python \
xvfb \ xvfb \
&& curl -sL https://deb.nodesource.com/setup_5.x | bash - \ && curl -sL https://deb.nodesource.com/setup_4.x | bash - \
&& apt-get -y install nodejs \ && apt-get -y install nodejs \
&& apt-get clean && apt-get clean
RUN mkdir -p /usr/src/app RUN mkdir -p /usr/src/app
COPY / /usr/src/app COPY / /usr/src/app
RUN cd /usr/src/app && npm install RUN cd /usr/src/app && npm install --production
VOLUME /data VOLUME /data
WORKDIR /data WORKDIR /data

View File

@@ -12,70 +12,21 @@
- `node src/main.js` - `node src/main.js`
## Sample data ## Sample data
Sample data can be downloaded at https://github.com/klokantech/tileserver-gl/releases/download/v0.0.2/test_data.zip Sample data can be downloaded at https://github.com/klokantech/tileserver-gl-data/archive/master.zip
#### Usage #### Usage
- unpack somewhere and `cd` to the directory - unpack somewhere and `cd` to the directory
- `docker run -it -v $(pwd):/data -p 8080:80 klokantech/tileserver-gl` - `docker run -it -v $(pwd):/data -p 8080:80 klokantech/tileserver-gl`
- (or `node path/to/repo/src/main.js`) - (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.
### Example configuration file ### Example configuration file
See https://github.com/klokantech/tileserver-gl-data/blob/master/config.json
```json
{
"options": {
"paths": {
"root": "",
"fonts": "glyphs",
"sprites": "sprites",
"styles": "styles",
"mbtiles": ""
},
"domains": [
"localhost:8080",
"127.0.0.1:8080"
]
},
"styles": {
"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]
}
}
},
"vector": {
"zurich-vector": {
"mbtiles": "zurich.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`) **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
@@ -84,13 +35,13 @@ The config file can contain definition of several paths where the tiles will be
- Style is served at `/styles/{id}.json` (+ array at `/styles.json`) - Style is served at `/styles/{id}.json` (+ array at `/styles.json`)
- Sprites at `/styles/{id}/sprite[@2x].{format}` - Sprites at `/styles/{id}/sprite[@2x].{format}`
- Fonts at `/fonts/{fontstack}/{start}-{end}.pbf` - Fonts at `/fonts/{fontstack}/{start}-{end}.pbf`
- Rasterized tiles are at `/raster/{id}/{z}/{x}/{y}[@2x].{format}` - Rendered tiles are at `/styles/{id}/rendered/{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
- Available formats: `png`, `jpg` (`jpeg`), `webp` - Available formats: `png`, `jpg` (`jpeg`), `webp`
- TileJSON at `/raster/{id}.json` - TileJSON at `/styles/{id}/rendered.json`
- Static images are rendered at: - Static images are rendered at:
- `/static/{id}/{lon},{lat},{zoom}/{width}x{height}[@2x].{format}` (center-based) - `/styles/{id}/static/{lon},{lat},{zoom}/{width}x{height}[@2x].{format}` (center-based)
- `/static/{id}/{minx},{miny},{maxx},{maxy}/{zoom}[@2x].{format}` (area-based) - `/styles/{id}/static/{minx},{miny},{maxx},{maxy}/{zoom}[@2x].{format}` (area-based)
- Vector tiles at `/vector/{mbtiles}/{z}/{x}/{y}.pbf` - Source data at `/data/{mbtiles}/{z}/{x}/{y}.{format}`
- TileJSON at `/vector/{mbtiles}.json` - TileJSON at `/data/{mbtiles}.json`
- Array of all TileJSONs at `/index.json` (`/raster.json`; `/vector.json`) - Array of all TileJSONs at `/index.json` (`/rendered.json`; `/data.json`)

View File

@@ -1,6 +1,6 @@
{ {
"name": "tileserver-gl", "name": "tileserver-gl",
"version": "0.0.2", "version": "0.0.3",
"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": [
@@ -17,18 +17,20 @@
"async": "1.5.2", "async": "1.5.2",
"advanced-pool": "0.3.1", "advanced-pool": "0.3.1",
"clone": "1.0.2", "clone": "1.0.2",
"color": "0.11.1",
"cors": "2.7.1", "cors": "2.7.1",
"express": "4.13.4", "express": "4.13.4",
"mapbox-gl-native": "3.0.2-earcut", "handlebars": "4.0.5",
"mbtiles": "0.8.2", "mapbox-gl-native": "3.1.2",
"mbtiles": "0.9.0",
"morgan": "1.7.0", "morgan": "1.7.0",
"nomnom": "1.8.1", "nomnom": "1.8.1",
"request": "2.69.0", "request": "2.72.0",
"sharp": "0.13.1", "sharp": "0.14.1",
"sphericalmercator": "1.0.4" "sphericalmercator": "1.0.5"
}, },
"devDependencies": { "devDependencies": {
"should": "^8.2.2", "should": "^8.3.0",
"mocha": "^2.4.5", "mocha": "^2.4.5",
"supertest": "^1.2.0" "supertest": "^1.2.0"
} }

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,162 @@
(function(window) {
var HAS_HASHCHANGE = (function() {
var doc_mode = window.documentMode;
return ('onhashchange' in window) &&
(doc_mode === undefined || doc_mode > 7);
})();
L.Hash = function(map) {
this.onHashChange = L.Util.bind(this.onHashChange, this);
if (map) {
this.init(map);
}
};
L.Hash.parseHash = function(hash) {
if(hash.indexOf('#') === 0) {
hash = hash.substr(1);
}
var args = hash.split("/");
if (args.length == 3) {
var zoom = parseInt(args[0], 10),
lat = parseFloat(args[1]),
lon = parseFloat(args[2]);
if (isNaN(zoom) || isNaN(lat) || isNaN(lon)) {
return false;
} else {
return {
center: new L.LatLng(lat, lon),
zoom: zoom
};
}
} else {
return false;
}
};
L.Hash.formatHash = function(map) {
var center = map.getCenter(),
zoom = map.getZoom(),
precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2));
return "#" + [zoom,
center.lat.toFixed(precision),
center.lng.toFixed(precision)
].join("/");
},
L.Hash.prototype = {
map: null,
lastHash: null,
parseHash: L.Hash.parseHash,
formatHash: L.Hash.formatHash,
init: function(map) {
this.map = map;
// reset the hash
this.lastHash = null;
this.onHashChange();
if (!this.isListening) {
this.startListening();
}
},
removeFrom: function(map) {
if (this.changeTimeout) {
clearTimeout(this.changeTimeout);
}
if (this.isListening) {
this.stopListening();
}
this.map = null;
},
onMapMove: function() {
// bail if we're moving the map (updating from a hash),
// or if the map is not yet loaded
if (this.movingMap || !this.map._loaded) {
return false;
}
var hash = this.formatHash(this.map);
if (this.lastHash != hash) {
location.replace(hash);
this.lastHash = hash;
}
},
movingMap: false,
update: function() {
var hash = location.hash;
if (hash === this.lastHash) {
return;
}
var parsed = this.parseHash(hash);
if (parsed) {
this.movingMap = true;
this.map.setView(parsed.center, parsed.zoom);
this.movingMap = false;
} else {
this.onMapMove(this.map);
}
},
// defer hash change updates every 100ms
changeDefer: 100,
changeTimeout: null,
onHashChange: function() {
// throttle calls to update() so that they only happen every
// `changeDefer` ms
if (!this.changeTimeout) {
var that = this;
this.changeTimeout = setTimeout(function() {
that.update();
that.changeTimeout = null;
}, this.changeDefer);
}
},
isListening: false,
hashChangeInterval: null,
startListening: function() {
this.map.on("moveend", this.onMapMove, this);
if (HAS_HASHCHANGE) {
L.DomEvent.addListener(window, "hashchange", this.onHashChange);
} else {
clearInterval(this.hashChangeInterval);
this.hashChangeInterval = setInterval(this.onHashChange, 50);
}
this.isListening = true;
},
stopListening: function() {
this.map.off("moveend", this.onMapMove, this);
if (HAS_HASHCHANGE) {
L.DomEvent.removeListener(window, "hashchange", this.onHashChange);
} else {
clearInterval(this.hashChangeInterval);
}
this.isListening = false;
}
};
L.hash = function(map) {
return new L.Hash(map);
};
L.Map.prototype.addHash = function() {
this._hash = L.hash(this);
};
L.Map.prototype.removeHash = function() {
this._hash.removeFrom();
};
})(window);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,58 +0,0 @@
<!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>

127
public/templates/data.tmpl Normal file
View File

@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{name}} - TileServer GL</title>
{{#is_vector}}
<link rel="stylesheet" type="text/css" href="/mapbox-gl.css" />
<script src="/mapbox-gl.js"></script>
<style>
body {background:#000;color:#ccc;}
#map {position:absolute;top:0;left:0;right:250px;bottom:0;}
h1 {position:absolute;top:5px;right:0;width:240px;margin:0;line-height:20px;font-size:20px;}
#layerList {position:absolute;top:35px;right:0;bottom:60%;width:240px;overflow:auto;}
#layerList div div {width:15px;height:15px;display:inline-block;}
#propertyList {position:absolute;top:40%;bottom:0;right:0;width:240px;overflow:auto;color:#fff;}
</style>
{{/is_vector}}
{{^is_vector}}
<link rel="stylesheet" type="text/css" href="/mapbox.css" />
<script src="/mapbox.js"></script>
<script src="/leaflet-hash.js"></script>
<style>
body { margin:0; padding:0; }
#map { position:absolute; top:0; bottom:0; width:100%; }
</style>
{{/is_vector}}
</head>
<body>
{{#is_vector}}
<h1>{{name}}</h1>
<div id="map"></div>
<div id="layerList"></div>
<pre id="propertyList"></pre>
<script>
var map = new mapboxgl.Map({
container: 'map',
hash: true
});
map.addControl(new mapboxgl.Navigation());
function generateColor(str) {
var rgb = [0, 0, 0];
for (var i = 0; i < str.length; i++) {
var v = str.charCodeAt(i);
rgb[v % 3] = (rgb[i % 3] + (13*(v%13))) % 12;
}
var r = 4 + rgb[0];
var g = 4 + rgb[1];
var b = 4 + rgb[2];
r = (r * 16) + r;
g = (g * 16) + g;
b = (b * 16) + b;
return [r, g, b, 1];
};
function initLayer(data) {
var layer;
var layerList = document.getElementById('layerList');
var layers_ = [];
data['vector_layers'].forEach(function(el) {
var color = generateColor(el['id']);
var colorText = 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',' + color[3] + ')';
layers_.push({
id: el['id'] + Math.random(),
source: 'vector_layer_',
'source-layer': el['id'],
interactive: true,
type: 'line',
paint: {'line-color': colorText}
});
var item = document.createElement('div');
item.innerHTML = '<div style="' +
'background:rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',1);' +
'"></div> ' + el['id'];
layerList.appendChild(item);
});
map.setStyle({
version: 8,
sources: {
'vector_layer_': {
type: 'vector',
tiles: data['tiles'],
minzoom: data['minzoom'],
maxzoom: data['maxzoom']
}
},
layers: layers_
});
return layer;
}
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4 && xhttp.status == 200) {
initLayer(xhttp.response);
}
};
xhttp.responseType = 'json';
xhttp.open('GET', '/data/{{id}}.json', true);
xhttp.send();
var propertyList = document.getElementById('propertyList');
map.on('mousemove', function(e) {
propertyList.innerHTML = '';
map.featuresAt(e.point, {radius: 3}, function(err, features) {
if (err) throw err;
if (features[0]) {
propertyList.innerHTML = JSON.stringify(features[0].properties, null, 2);
}
});
});
</script>
{{/is_vector}}
{{^is_vector}}
<h1 style="display:none;">{{name}}</h1>
<div id='map'></div>
<script>
var map = L.mapbox.map('map', '/data/{{id}}.json', { zoomControl: false });
new L.Control.Zoom({ position: 'topright' }).addTo(map);
setTimeout(function() {
new L.Hash(map);
}, 0);
</script>
{{/is_vector}}
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{name}} - TileServer GL</title>
<link rel="stylesheet" type="text/css" href="/mapbox-gl.css" />
<script src="/mapbox-gl.js"></script>
<link rel="stylesheet" type="text/css" href="/mapbox.css" />
<script src="/mapbox.js"></script>
<script src="/leaflet-hash.js"></script>
<style>
body { margin:0; padding:0; }
#map { position:absolute; top:0; bottom:0; width:100%; }
</style>
</head>
<body>
<h1 style="display:none;">{{name}}</h1>
<div id='map'></div>
<script>
var preference = (location.search || '').substr(1);
if (preference != 'vector' && preference != 'raster') {
preference = mapboxgl.supported() ? 'vector' : 'raster';
}
if (preference == 'vector') {
var map = new mapboxgl.Map({
container: 'map',
style: '/styles/{{id}}.json',
hash: true
});
map.addControl(new mapboxgl.Navigation());
} else {
var map = L.mapbox.map('map', '/styles/{{id}}/rendered.json', { zoomControl: false });
new L.Control.Zoom({ position: 'topright' }).addTo(map);
setTimeout(function() {
new L.Hash(map);
}, 0);
}
</script>
</body>
</html>

View File

@@ -1,6 +1,7 @@
'use strict'; 'use strict';
var crypto = require('crypto'), var crypto = require('crypto'),
fs = require('fs'),
path = require('path'); path = require('path');
var clone = require('clone'), var clone = require('clone'),
@@ -12,34 +13,39 @@ var utils = require('./utils');
module.exports = function(options, repo, params, id) { module.exports = function(options, repo, params, id) {
var app = express().disable('x-powered-by'); var app = express().disable('x-powered-by');
var mbtilesFile = params.mbtiles; var mbtilesFile = path.join(options.paths.mbtiles, params.mbtiles);
var tileJSON = { var tileJSON = {
'tiles': params.domains || options.domains 'tiles': params.domains || options.domains
}; };
repo[id] = tileJSON; repo[id] = tileJSON;
var source = new mbtiles(path.join(options.paths.mbtiles, mbtilesFile), var source = new mbtiles(mbtilesFile, function(err) {
function(err) {
source.getInfo(function(err, info) { source.getInfo(function(err, info) {
tileJSON['name'] = id; tileJSON['name'] = id;
tileJSON['format'] = 'pbf';
Object.assign(tileJSON, info); Object.assign(tileJSON, info);
tileJSON['tilejson'] = '2.0.0'; tileJSON['tilejson'] = '2.0.0';
tileJSON['basename'] = id; tileJSON['basename'] = id;
tileJSON['format'] = 'pbf'; tileJSON['filesize'] = fs.statSync(mbtilesFile)['size'];
delete tileJSON['scheme'];
Object.assign(tileJSON, params.tilejson || {}); Object.assign(tileJSON, params.tilejson || {});
utils.fixTileJSONCenter(tileJSON);
}); });
}); });
var tilePattern = '/vector/' + id + '/:z(\\d+)/:x(\\d+)/:y(\\d+).pbf'; var tilePattern = '/' + id + '/:z(\\d+)/:x(\\d+)/:y(\\d+).:format([\\w]+)';
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;
if (req.params.format != tileJSON.format) {
return res.status(404).send('Invalid format');
}
if (z < tileJSON.minzoom || 0 || x < 0 || y < 0 || if (z < tileJSON.minzoom || 0 || x < 0 || y < 0 ||
z > tileJSON.maxzoom || z > tileJSON.maxzoom ||
x >= Math.pow(2, z) || y >= Math.pow(2, z)) { x >= Math.pow(2, z) || y >= Math.pow(2, z)) {
@@ -55,8 +61,10 @@ module.exports = function(options, repo, params, id) {
} else { } else {
var md5 = crypto.createHash('md5').update(data).digest('base64'); var md5 = crypto.createHash('md5').update(data).digest('base64');
headers['content-md5'] = md5; headers['content-md5'] = md5;
headers['content-type'] = 'application/x-protobuf'; if (tileJSON['format'] == 'pbf') {
headers['content-encoding'] = 'gzip'; headers['content-type'] = 'application/x-protobuf';
headers['content-encoding'] = 'gzip';
}
res.set(headers); res.set(headers);
if (data == null) { if (data == null) {
@@ -68,10 +76,10 @@ module.exports = function(options, repo, params, id) {
}); });
}); });
app.get('/vector/' + id + '.json', function(req, res, next) { app.get('/' + id + '.json', function(req, res, next) {
var info = clone(tileJSON); var info = clone(tileJSON);
info.tiles = utils.getTileUrls(req, info.tiles, info.tiles = utils.getTileUrls(req, info.tiles,
'vector/' + id, info.format); 'data/' + id, info.format);
return res.send(info); return res.send(info);
}); });

View File

@@ -31,7 +31,7 @@ module.exports = function(options, allowedFonts) {
} }
}; };
app.get('/fonts/:fontstack/:range([\\d]+-[\\d]+).pbf', app.get('/:fontstack/:range([\\d]+-[\\d]+).pbf',
function(req, res, next) { function(req, res, next) {
var fontstack = decodeURI(req.params.fontstack); var fontstack = decodeURI(req.params.fontstack);
var range = req.params.range; var range = req.params.range;

View File

@@ -9,6 +9,7 @@ var async = require('async'),
zlib = require('zlib'); zlib = require('zlib');
var clone = require('clone'), var clone = require('clone'),
Color = require('color'),
express = require('express'), express = require('express'),
mercator = new (require('sphericalmercator'))(), mercator = new (require('sphericalmercator'))(),
mbgl = require('mapbox-gl-native'), mbgl = require('mapbox-gl-native'),
@@ -61,11 +62,12 @@ module.exports = function(options, repo, params, id) {
var source = map.sources[parts[2]]; var source = map.sources[parts[2]];
var z = parts[3] | 0, var z = parts[3] | 0,
x = parts[4] | 0, x = parts[4] | 0,
y = parts[5].split('.')[0] | 0; y = parts[5].split('.')[0] | 0,
format = parts[5].split('.')[1];
source.getTile(z, x, y, function(err, data, headers) { source.getTile(z, x, y, function(err, data, headers) {
if (err) { if (err) {
//console.log('MBTiles error, serving empty', err); //console.log('MBTiles error, serving empty', err);
callback(null, { data: new Buffer(0) }); callback(null, { data: source.emptyTile });
} else { } else {
var response = {}; var response = {};
@@ -76,7 +78,11 @@ module.exports = function(options, repo, params, id) {
response.etag = headers['ETag']; response.etag = headers['ETag'];
} }
response.data = zlib.unzipSync(data); if (format == 'pbf') {
response.data = zlib.unzipSync(data);
} else {
response.data = data;
}
callback(null, response); callback(null, response);
} }
@@ -134,6 +140,7 @@ module.exports = function(options, repo, params, id) {
var tileJSON = { var tileJSON = {
'tilejson': '2.0.0', 'tilejson': '2.0.0',
'name': styleJSON.name, 'name': styleJSON.name,
'attribution': '',
'basename': id, 'basename': id,
'minzoom': 0, 'minzoom': 0,
'maxzoom': 20, 'maxzoom': 20,
@@ -141,8 +148,10 @@ module.exports = function(options, repo, params, id) {
'format': 'png', 'format': 'png',
'type': 'baselayer' 'type': 'baselayer'
}; };
var attributionOverride = params.tilejson && params.tilejson.attribution;
Object.assign(tileJSON, params.tilejson || {}); Object.assign(tileJSON, params.tilejson || {});
tileJSON.tiles = params.domains || options.domains; tileJSON.tiles = params.domains || options.domains;
utils.fixTileJSONCenter(tileJSON);
var queue = []; var queue = [];
Object.keys(styleJSON.sources).forEach(function(name) { Object.keys(styleJSON.sources).forEach(function(name) {
@@ -157,12 +166,39 @@ module.exports = function(options, repo, params, id) {
map.sources[name] = new mbtiles( map.sources[name] = new mbtiles(
path.join(options.paths.mbtiles, mbtilesFile), function(err) { path.join(options.paths.mbtiles, mbtilesFile), function(err) {
map.sources[name].getInfo(function(err, info) { map.sources[name].getInfo(function(err, info) {
var type = source.type;
Object.assign(source, info); Object.assign(source, info);
source.type = type;
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 + '/{z}/{x}/{y}.pbf' 'mbtiles://' + name + '/{z}/{x}/{y}.' + (info.format || 'pbf')
]; ];
if (source.format == 'pbf') {
map.sources[name].emptyTile = new Buffer(0);
} else {
var color = new Color(source.color || '#fff');
var format = source.format;
if (format == 'jpg') {
format = 'jpeg';
}
sharp(new Buffer(color.rgbArray()), {
raw: {
width: 1,
height: 1,
channels: 3
}
}).toFormat(format).toBuffer(function(err, buffer, info) {
map.sources[name].emptyTile = buffer;
});
}
if (!attributionOverride &&
source.attribution && source.attribution.length > 0) {
if (tileJSON.attribution.length > 0) {
tileJSON.attribution += '; ';
}
tileJSON.attribution += source.attribution;
}
callback(null); callback(null);
}); });
}); });
@@ -179,7 +215,7 @@ module.exports = function(options, repo, params, id) {
repo[id] = tileJSON; repo[id] = tileJSON;
var tilePattern = '/raster/' + id + '/:z(\\d+)/:x(\\d+)/:y(\\d+)' + var tilePattern = '/rendered/:z(\\d+)/:x(\\d+)/:y(\\d+)' +
':scale(' + SCALE_PATTERN + ')?\.:format([\\w]+)'; ':scale(' + SCALE_PATTERN + ')?\.:format([\\w]+)';
var respondImage = function(z, lon, lat, bearing, pitch, var respondImage = function(z, lon, lat, bearing, pitch,
@@ -230,9 +266,19 @@ module.exports = function(options, repo, params, id) {
image.resize(width * scale, height * scale); image.resize(width * scale, height * scale);
} }
image.toFormat(format) image.toFormat(format);
.compressionLevel(9)
.toBuffer(function(err, buffer, info) { var formatEncoding = (params.formatEncoding || {})[format] ||
(options.formatEncoding || {})[format];
if (format == 'png') {
image.compressionLevel(formatEncoding || 6)
.withoutAdaptiveFiltering();
} else if (format == 'jpeg') {
image.quality(formatEncoding || 80);
} else if (format == 'webp') {
image.quality(formatEncoding || 90);
}
image.toBuffer(function(err, buffer, info) {
if (!buffer) { if (!buffer) {
return res.status(404).send('Not found'); return res.status(404).send('Not found');
} }
@@ -268,7 +314,7 @@ module.exports = function(options, repo, params, id) {
}); });
var staticPattern = var staticPattern =
'/static/' + id + '/%s:scale(' + SCALE_PATTERN + ')?\.:format([\\w]+)'; '/static/%s:scale(' + SCALE_PATTERN + ')?\.:format([\\w]+)';
var centerPattern = var centerPattern =
util.format(':lon(%s),:lat(%s),:z(\\d+):bearing(,%s)?:pitch(,%s)?/' + util.format(':lon(%s),:lat(%s),:z(\\d+):bearing(,%s)?:pitch(,%s)?/' +
@@ -279,8 +325,8 @@ module.exports = function(options, repo, params, id) {
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), bearing = +((req.params.bearing || ',0').substring(1)),
pitch = +(req.params.pitch || ',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),
@@ -308,10 +354,10 @@ module.exports = function(options, repo, params, id) {
return respondImage(z, x, y, 0, 0, w, h, scale, format, res, next); return respondImage(z, x, y, 0, 0, w, h, scale, format, res, next);
}); });
app.get('/raster/' + id + '.json', function(req, res, next) { app.get('/rendered.json', function(req, res, next) {
var info = clone(tileJSON); var info = clone(tileJSON);
info.tiles = utils.getTileUrls(req, info.tiles, info.tiles = utils.getTileUrls(req, info.tiles,
'raster/' + id, info.format); 'styles/' + id + '/rendered', info.format);
return res.send(info); return res.send(info);
}); });

View File

@@ -7,7 +7,7 @@ var clone = require('clone'),
express = require('express'); express = require('express');
module.exports = function(options, repo, params, id, reportVector, reportFont) { module.exports = function(options, repo, params, id, reportTiles, reportFont) {
var app = express().disable('x-powered-by'); var app = express().disable('x-powered-by');
var styleFile = path.join(options.paths.styles, params.style); var styleFile = path.join(options.paths.styles, params.style);
@@ -18,8 +18,8 @@ module.exports = function(options, repo, params, id, reportVector, reportFont) {
var url = source.url; var url = source.url;
if (url.lastIndexOf('mbtiles:', 0) === 0) { if (url.lastIndexOf('mbtiles:', 0) === 0) {
var mbtiles = url.substring('mbtiles://'.length); var mbtiles = url.substring('mbtiles://'.length);
var identifier = reportVector(mbtiles); var identifier = reportTiles(mbtiles);
source.url = 'local://vector/' + identifier + '.json'; source.url = 'local://data/' + identifier + '.json';
} }
}); });
@@ -45,7 +45,7 @@ module.exports = function(options, repo, params, id, reportVector, reportFont) {
repo[id] = styleJSON; repo[id] = styleJSON;
app.get('/styles/' + id + '.json', function(req, res, next) { app.get('/' + id + '.json', function(req, res, next) {
var fixUrl = function(url) { var fixUrl = function(url) {
return url.replace( return url.replace(
'local://', req.protocol + '://' + req.headers.host + '/'); 'local://', req.protocol + '://' + req.headers.host + '/');
@@ -61,7 +61,7 @@ module.exports = function(options, repo, params, id, reportVector, reportFont) {
return res.send(styleJSON_); return res.send(styleJSON_);
}); });
app.get('/styles/' + id + '/sprite:scale(@[23]x)?\.:format([\\w]+)', app.get('/' + id + '/sprite:scale(@[23]x)?\.:format([\\w]+)',
function(req, res, next) { function(req, res, next) {
var scale = req.params.scale, var scale = req.params.scale,
format = req.params.format; format = req.params.format;

View File

@@ -10,20 +10,22 @@ var fs = require('fs'),
var clone = require('clone'), var clone = require('clone'),
cors = require('cors'), cors = require('cors'),
express = require('express'), express = require('express'),
handlebars = require('handlebars'),
mercator = new (require('sphericalmercator'))(),
morgan = require('morgan'); morgan = require('morgan');
var serve_font = require('./serve_font'), var serve_font = require('./serve_font'),
serve_raster = require('./serve_raster'), serve_rendered = require('./serve_rendered'),
serve_style = require('./serve_style'), serve_style = require('./serve_style'),
serve_vector = require('./serve_vector'), serve_data = require('./serve_data'),
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'),
serving = { serving = {
styles: {}, styles: {},
raster: {}, rendered: {},
vector: {}, data: {},
fonts: { // default fonts, always expose these (if they exist) fonts: { // default fonts, always expose these (if they exist)
'Open Sans Regular': true, 'Open Sans Regular': true,
'Arial Unicode MS Regular': true 'Arial Unicode MS Regular': true
@@ -39,19 +41,27 @@ module.exports = function(opts, callback) {
app.use(morgan('dev')); app.use(morgan('dev'));
} }
var configPath = path.resolve(opts.config), var configPath = path.resolve(opts.config);
config = require(configPath);
var config;
try {
config = require(configPath);
} catch (e) {
console.log('ERROR: Config file not found or invalid!');
console.log(' See README.md for instructions and sample data.');
process.exit(1);
}
var options = config.options || {}; var options = config.options || {};
var paths = options.paths || {}; var paths = options.paths || {};
options.paths = paths; options.paths = paths;
paths.root = path.join(process.cwd(), paths.root || ''); paths.root = path.resolve(process.cwd(), paths.root || '');
paths.styles = path.join(paths.root, paths.styles || ''); paths.styles = path.resolve(paths.root, paths.styles || '');
paths.fonts = path.join(paths.root, paths.fonts || ''); paths.fonts = path.resolve(paths.root, paths.fonts || '');
paths.sprites = path.join(paths.root, paths.sprites || ''); paths.sprites = path.resolve(paths.root, paths.sprites || '');
paths.mbtiles = path.join(paths.root, paths.mbtiles || ''); paths.mbtiles = path.resolve(paths.root, paths.mbtiles || '');
var vector = clone(config.vector); var data = clone(config.data || {});
Object.keys(config.styles || {}).forEach(function(id) { Object.keys(config.styles || {}).forEach(function(id) {
var item = config.styles[id]; var item = config.styles[id];
@@ -60,21 +70,21 @@ module.exports = function(opts, callback) {
return; return;
} }
if (item.vector !== false) { if (item.serve_data !== false) {
app.use('/', serve_style(options, serving.styles, item, id, app.use('/styles/', serve_style(options, serving.styles, item, id,
function(mbtiles) { function(mbtiles) {
var vectorItemId; var dataItemId;
Object.keys(vector).forEach(function(id) { Object.keys(data).forEach(function(id) {
if (vector[id].mbtiles == mbtiles) { if (data[id].mbtiles == mbtiles) {
vectorItemId = id; dataItemId = id;
} }
}); });
if (vectorItemId) { // mbtiles exist in the vector config if (dataItemId) { // mbtiles exist in the data config
return vectorItemId; return dataItemId;
} else { } else {
var id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles; var id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles;
while (vector[id]) id += '_'; while (data[id]) id += '_';
vector[id] = { data[id] = {
'mbtiles': mbtiles 'mbtiles': mbtiles
}; };
return id; return id;
@@ -83,26 +93,27 @@ module.exports = function(opts, callback) {
serving.fonts[font] = true; serving.fonts[font] = true;
})); }));
} }
if (item.raster !== false) { if (item.serve_rendered !== false) {
app.use('/', serve_raster(options, serving.raster, item, id)); app.use('/styles/' + id + '/',
serve_rendered(options, serving.rendered, item, id));
} }
}); });
if (Object.keys(serving.styles).length > 0) { if (Object.keys(serving.styles).length > 0) {
// serve fonts only if serving some styles // serve fonts only if serving some styles
app.use('/', serve_font(options, serving.fonts)); app.use('/fonts/', serve_font(options, serving.fonts));
} }
//TODO: cors app.use(cors());
Object.keys(vector).forEach(function(id) { Object.keys(data).forEach(function(id) {
var item = vector[id]; var item = data[id];
if (!item.mbtiles || item.mbtiles.length == 0) { if (!item.mbtiles || item.mbtiles.length == 0) {
console.log('Missing "mbtiles" property for ' + id); console.log('Missing "mbtiles" property for ' + id);
return; return;
} }
app.use('/', serve_vector(options, serving.vector, item, id)); app.use('/data/', serve_data(options, serving.data, item, id));
}); });
app.get('/styles.json', function(req, res, next) { app.get('/styles.json', function(req, res, next) {
@@ -129,18 +140,129 @@ module.exports = function(opts, callback) {
return arr; return arr;
}; };
app.get('/raster.json', function(req, res, next) { app.get('/rendered.json', function(req, res, next) {
res.send(addTileJSONs([], req, 'raster')); res.send(addTileJSONs([], req, 'rendered'));
}); });
app.get('/vector.json', function(req, res, next) { app.get('/data.json', function(req, res, next) {
res.send(addTileJSONs([], req, 'vector')); res.send(addTileJSONs([], req, 'data'));
}); });
app.get('/index.json', function(req, res, next) { app.get('/index.json', function(req, res, next) {
res.send(addTileJSONs(addTileJSONs([], req, 'raster'), req, 'vector')); res.send(addTileJSONs(addTileJSONs([], req, 'rendered'), req, 'data'));
}); });
// serve viewer on the root //------------------------------------
app.use('/', express.static(path.join(__dirname, '../public'))); // serve web presentations
app.use('/', express.static(path.join(__dirname, '../public/resources')));
var templates = path.join(__dirname, '../public/templates');
var serveTemplate = function(path, template, dataGetter) {
fs.readFile(templates + '/' + template + '.tmpl', function(err, content) {
if (err) {
console.log('Template not found:', err);
}
var compiled = handlebars.compile(content.toString());
app.use(path, function(req, res, next) {
var data = {};
if (dataGetter) {
data = dataGetter(req.params);
if (!data) {
return res.status(404).send('Not found');
}
}
return res.status(200).send(compiled(data));
});
});
};
serveTemplate('/$', 'index', function() {
var styles = clone(config.styles || {});
Object.keys(styles).forEach(function(id) {
var style = styles[id];
style.name = (serving.styles[id] || serving.rendered[id] || {}).name;
style.serving_data = serving.styles[id];
style.serving_rendered = serving.rendered[id];
if (style.serving_rendered) {
var center = style.serving_rendered.center;
if (center) {
style.viewer_hash = '#' + center[2] + '/' +
center[1].toFixed(5) + '/' +
center[0].toFixed(5);
var centerPx = mercator.px([center[0], center[1]], center[2]);
style.thumbnail = center[2] + '/' +
Math.floor(centerPx[0] / 256) + '/' +
Math.floor(centerPx[1] / 256) + '.png';
}
}
});
var data = clone(serving.data || {});
Object.keys(data).forEach(function(id) {
var data_ = data[id];
var center = data_.center;
if (center) {
data_.viewer_hash = '#' + center[2] + '/' +
center[1].toFixed(5) + '/' +
center[0].toFixed(5);
}
data_.is_vector = data_.format == 'pbf';
if (!data_.is_vector) {
if (center) {
var centerPx = mercator.px([center[0], center[1]], center[2]);
data_.thumbnail = center[2] + '/' +
Math.floor(centerPx[0] / 256) + '/' +
Math.floor(centerPx[1] / 256) + '.' + data_.format;
}
}
if (data_.filesize) {
var suffix = 'kB';
var size = parseInt(data_.filesize, 10) / 1024;
if (size > 1024) {
suffix = 'MB';
size /= 1024;
}
if (size > 1024) {
suffix = 'GB';
size /= 1024;
}
data_.formatted_filesize = size.toFixed(2) + ' ' + suffix;
}
});
return {
styles: styles,
data: data
};
});
serveTemplate('/styles/:id/$', 'viewer', function(params) {
var id = params.id;
var style = clone((config.styles || {})[id]);
if (!style) {
return null;
}
style.id = id;
style.name = (serving.styles[id] || serving.rendered[id]).name;
style.serving_data = serving.styles[id];
style.serving_rendered = serving.rendered[id];
return style;
});
/*
app.use('/rendered/:id/$', function(req, res, next) {
return res.redirect(301, '/styles/' + req.params.id + '/');
});
*/
serveTemplate('/data/:id/$', 'data', function(params) {
var id = params.id;
var data = clone(serving.data[id]);
if (!data) {
return null;
}
data.id = id;
data.is_vector = data.format == 'pbf';
return data;
});
var server = app.listen(process.env.PORT || opts.port, function() { var server = app.listen(process.env.PORT || opts.port, function() {
console.log('Listening at http://%s:%d/', console.log('Listening at http://%s:%d/',

View File

@@ -22,3 +22,18 @@ module.exports.getTileUrls = function(req, domains, path, format) {
return uris; return uris;
}; };
module.exports.fixTileJSONCenter = function(tileJSON) {
if (tileJSON.bounds && !tileJSON.center) {
var fitWidth = 1024;
var tiles = fitWidth / 256;
tileJSON.center = [
(tileJSON.bounds[0] + tileJSON.bounds[2]) / 2,
(tileJSON.bounds[1] + tileJSON.bounds[3]) / 2,
Math.round(
-Math.log((tileJSON.bounds[2] - tileJSON.bounds[0]) / 360 / tiles) /
Math.LN2
)
];
}
};

View File

@@ -40,8 +40,8 @@ var testTileJSON = function(url, basename) {
describe('Metadata', function() { describe('Metadata', function() {
testTileJSONArray('/index.json'); testTileJSONArray('/index.json');
testTileJSONArray('/raster.json'); testTileJSONArray('/rendered.json');
testTileJSONArray('/vector.json'); testTileJSONArray('/data.json');
describe('/styles.json is valid array', function() { describe('/styles.json is valid array', function() {
it('is json', function(done) { it('is json', function(done) {
@@ -64,6 +64,6 @@ describe('Metadata', function() {
}); });
}); });
testTileJSON('/raster/test.json', 'test'); testTileJSON('/styles/test/rendered.json', 'test');
testTileJSON('/vector/zurich-vector.json', 'zurich-vector'); testTileJSON('/data/zurich-vector.json', 'zurich-vector');
}); });

View File

@@ -1,6 +1,6 @@
var testStatic = function(prefix, q, format, status, scale, type) { var testStatic = function(prefix, q, format, status, scale, type) {
if (scale) q += '@' + scale + 'x'; if (scale) q += '@' + scale + 'x';
var path = '/static/' + prefix + '/' + q + '.' + format; var path = '/styles/' + prefix + '/static/' + q + '.' + format;
it(path + ' returns ' + status, function(done) { it(path + ' returns ' + status, function(done) {
var test = supertest(app).get(path); var test = supertest(app).get(path);
if (status) test.expect(status); if (status) test.expect(status);

View File

@@ -1,5 +1,5 @@
var testTile = function(prefix, z, x, y, status) { var testTile = function(prefix, z, x, y, status) {
var path = '/vector/' + prefix + '/' + z + '/' + x + '/' + y + '.pbf'; var path = '/data/' + prefix + '/' + z + '/' + x + '/' + y + '.pbf';
it(path + ' returns ' + status, function(done) { it(path + ' returns ' + status, function(done) {
var test = supertest(app).get(path); var test = supertest(app).get(path);
if (status) test.expect(status); if (status) test.expect(status);

View File

@@ -1,6 +1,6 @@
var testTile = function(prefix, z, x, y, format, status, scale, type) { var testTile = function(prefix, z, x, y, format, status, scale, type) {
if (scale) y += '@' + scale + 'x'; if (scale) y += '@' + scale + 'x';
var path = '/raster/' + prefix + '/' + z + '/' + x + '/' + y + '.' + format; var path = '/styles/' + prefix + '/rendered/' + z + '/' + x + '/' + y + '.' + format;
it(path + ' returns ' + status, function(done) { it(path + ' returns ' + status, function(done) {
var test = supertest(app).get(path); var test = supertest(app).get(path);
test.expect(status); test.expect(status);
@@ -39,6 +39,6 @@ describe('Raster tiles', function() {
testTile('test', 0, 0, 0, 'png', 404, 1); testTile('test', 0, 0, 0, 'png', 404, 1);
testTile('test', 0, 0, 0, 'png', 404, 4); testTile('test', 0, 0, 0, 'png', 404, 4);
testTile('hybrid', 0, 0, 0, 'png', 404); //testTile('hybrid', 0, 0, 0, 'png', 404); //TODO: test this
}); });
}); });