Compare commits

...

56 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
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
29 changed files with 2090 additions and 268 deletions

View File

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

22
.travis.yml Normal file
View File

@@ -0,0 +1,22 @@
language: node_js
node_js:
- "4"
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 -O test_data.zip https://github.com/klokantech/tileserver-gl-data/archive/v0.0.3.zip
- unzip -q test_data.zip -d tmp_test_data
- mkdir test_data
- mv tmp_test_data/tileserver-gl-data-*/* -t test_data
script:
- xvfb-run --server-args="-screen 0 1024x768x24" npm test

View File

@@ -7,13 +7,13 @@ RUN apt-get -qq update \
build-essential \
python \
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 clean
RUN mkdir -p /usr/src/app
COPY / /usr/src/app
RUN cd /usr/src/app && npm install
RUN cd /usr/src/app && npm install --production
VOLUME /data
WORKDIR /data

View File

@@ -1,4 +1,5 @@
# tileserver-gl
[![Build Status](https://travis-ci.org/klokantech/tileserver-gl.svg?branch=master)](https://travis-ci.org/klokantech/tileserver-gl)
## Installation
@@ -10,64 +11,37 @@
- `npm install`
- `node src/main.js`
## Sample data
Sample data can be downloaded at https://github.com/klokantech/tileserver-gl-data/archive/master.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`)
## Configuration
Create `config.json` file in the root directory.
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
See https://github.com/klokantech/tileserver-gl-data/blob/master/config.json
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
{
"/basic": {
"root": "test_data",
"style": "styles/basic-v8.json",
"domains": [
"localhost:8080",
"127.0.0.1:8080"
],
"options": {
"type": "overlay",
"bounds": [5.8559113, 45.717995, 10.5922941, 47.9084648]
}
},
"/hybrid": {
"root": "test_data",
"style": "styles/satellite-hybrid-v8.json",
"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
- 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`
- 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
- Static images (only for raster tiles) are rendered at:
- `/{basename}/static/{lon},{lat},{zoom}/{width}x{height}[@2x].{format}` (center-based)
- `/{basename}/static/{minx},{miny},{maxx},{maxy}/{zoom}[@2x].{format}` (area-based)
- TileJSON at `/{basename}/index.json`
- Array of all TileJSONs at `/index.json`
- Available formats:
- raster: `png`, `jpg` (`jpeg`), `webp`
- vector: `pbf`
- Available formats: `png`, `jpg` (`jpeg`), `webp`
- TileJSON at `/styles/{id}/rendered.json`
- Static images are rendered at:
- `/styles/{id}/static/{lon},{lat},{zoom}/{width}x{height}[@2x].{format}` (center-based)
- `/styles/{id}/static/{minx},{miny},{maxx},{maxy}/{zoom}[@2x].{format}` (area-based)
- Source data at `/data/{mbtiles}/{z}/{x}/{y}.{format}`
- TileJSON at `/data/{mbtiles}.json`
- Array of all TileJSONs at `/index.json` (`/rendered.json`; `/data.json`)

View File

@@ -1,6 +1,6 @@
{
"name": "tileserver-gl",
"version": "0.0.1",
"version": "0.0.3",
"description": "Map tile server for JSON GL styles - serverside generated raster tiles",
"main": "src/main.js",
"authors": [
@@ -10,18 +10,28 @@
"type": "git",
"url": "https://github.com/klokantech/tileserver-gl.git"
},
"scripts": {
"test": "mocha test/**.js"
},
"dependencies": {
"async": "1.5.2",
"advanced-pool": "0.3.1",
"clone": "1.0.2",
"color": "0.11.1",
"cors": "2.7.1",
"express": "4.13.4",
"mapbox-gl-native": "3.0.2-earcut",
"mbtiles": "0.8.2",
"handlebars": "4.0.5",
"mapbox-gl-native": "3.1.2",
"mbtiles": "0.9.0",
"morgan": "1.7.0",
"nomnom": "1.8.1",
"request": "2.69.0",
"sharp": "0.13.1",
"sphericalmercator": "1.0.4"
"request": "2.72.0",
"sharp": "0.14.1",
"sphericalmercator": "1.0.5"
},
"devDependencies": {
"should": "^8.3.0",
"mocha": "^2.4.5",
"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);

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;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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>

87
src/serve_data.js Normal file
View File

@@ -0,0 +1,87 @@
'use strict';
var crypto = require('crypto'),
fs = require('fs'),
path = require('path');
var clone = require('clone'),
express = require('express'),
mbtiles = require('mbtiles');
var utils = require('./utils');
module.exports = function(options, repo, params, id) {
var app = express().disable('x-powered-by');
var mbtilesFile = path.join(options.paths.mbtiles, params.mbtiles);
var tileJSON = {
'tiles': params.domains || options.domains
};
repo[id] = tileJSON;
var source = new mbtiles(mbtilesFile, function(err) {
source.getInfo(function(err, info) {
tileJSON['name'] = id;
tileJSON['format'] = 'pbf';
Object.assign(tileJSON, info);
tileJSON['tilejson'] = '2.0.0';
tileJSON['basename'] = id;
tileJSON['filesize'] = fs.statSync(mbtilesFile)['size'];
delete tileJSON['scheme'];
Object.assign(tileJSON, params.tilejson || {});
utils.fixTileJSONCenter(tileJSON);
});
});
var tilePattern = '/' + id + '/:z(\\d+)/:x(\\d+)/:y(\\d+).:format([\\w]+)';
app.get(tilePattern, function(req, res, next) {
var z = req.params.z | 0,
x = req.params.x | 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 ||
z > tileJSON.maxzoom ||
x >= Math.pow(2, z) || y >= Math.pow(2, z)) {
return res.status(404).send('Out of bounds');
}
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;
if (tileJSON['format'] == 'pbf') {
headers['content-type'] = 'application/x-protobuf';
headers['content-encoding'] = 'gzip';
}
res.set(headers);
if (data == null) {
return res.status(404).send('Not found');
} else {
return res.status(200).send(data);
}
}
});
});
app.get('/' + id + '.json', function(req, res, next) {
var info = clone(tileJSON);
info.tiles = utils.getTileUrls(req, info.tiles,
'data/' + id, info.format);
return res.send(info);
});
return app;
};

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('/: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

@@ -9,6 +9,7 @@ var async = require('async'),
zlib = require('zlib');
var clone = require('clone'),
Color = require('color'),
express = require('express'),
mercator = new (require('sphericalmercator'))(),
mbgl = require('mapbox-gl-native'),
@@ -31,18 +32,15 @@ mbgl.on('message', function(e) {
}
});
module.exports = function(maps, options, prefix) {
var app = express().disable('x-powered-by'),
domains = options.domains,
tilePath = '/{z}/{x}/{y}.{format}';
module.exports = function(options, repo, params, id) {
var app = express().disable('x-powered-by');
var rootPath = path.join(process.cwd(), options.root || '');
var rootPath = options.paths.root;
var styleUrl = options.style;
var styleFile = params.style;
var map = {
renderers: [],
sources: {},
tileJSON: {}
sources: {}
};
var styleJSON;
@@ -53,8 +51,10 @@ module.exports = function(maps, options, prefix) {
request: function(req, callback) {
var protocol = req.url.split(':')[0];
//console.log('Handling request:', req);
if (protocol == req.url) {
fs.readFile(path.join(rootPath, unescape(req.url)), function(err, data) {
if (protocol == 'sprites' || protocol == 'fonts') {
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 });
});
} else if (protocol == 'mbtiles') {
@@ -62,11 +62,12 @@ module.exports = function(maps, options, prefix) {
var source = map.sources[parts[2]];
var z = parts[3] | 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) {
if (err) {
//console.log('MBTiles error, serving empty', err);
callback(null, { data: new Buffer(0) });
callback(null, { data: source.emptyTile });
} else {
var response = {};
@@ -77,7 +78,11 @@ module.exports = function(maps, options, prefix) {
response.etag = headers['ETag'];
}
if (format == 'pbf') {
response.data = zlib.unzipSync(data);
} else {
response.data = data;
}
callback(null, response);
}
@@ -128,19 +133,25 @@ 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',
'name': styleJSON.name,
'basename': prefix.substr(1),
'attribution': '',
'basename': id,
'minzoom': 0,
'maxzoom': 20,
'bounds': [-180, -85.0511, 180, 85.0511],
'format': 'png',
'type': 'baselayer'
};
Object.assign(map.tileJSON, options.options || {});
var attributionOverride = params.tilejson && params.tilejson.attribution;
Object.assign(tileJSON, params.tilejson || {});
tileJSON.tiles = params.domains || options.domains;
utils.fixTileJSONCenter(tileJSON);
var queue = [];
Object.keys(styleJSON.sources).forEach(function(name) {
@@ -151,15 +162,43 @@ module.exports = function(maps, options, prefix) {
delete source.url;
queue.push(function(callback) {
var mbtilesUrl = url.substring('mbtiles://'.length);
map.sources[name] = new mbtiles(path.join(rootPath, mbtilesUrl), function(err) {
var mbtilesFile = url.substring('mbtiles://'.length);
map.sources[name] = new mbtiles(
path.join(options.paths.mbtiles, mbtilesFile), function(err) {
map.sources[name].getInfo(function(err, info) {
var type = source.type;
Object.assign(source, info);
source.type = type;
source.basename = name;
source.tiles = [
// meta url which will be detected when requested
'mbtiles://' + name + tilePath.replace('{format}', '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);
});
});
@@ -174,22 +213,25 @@ module.exports = function(maps, options, prefix) {
map.renderers[3] = createPool(3, 2, 4);
});
maps[prefix] = map;
repo[id] = tileJSON;
var tilePattern = tilePath
.replace(/\.(?!.*\.)/, ':scale(' + SCALE_PATTERN + ')?.')
.replace(/\./g, '\.')
.replace('{z}', ':z(\\d+)')
.replace('{x}', ':x(\\d+)')
.replace('{y}', ':y(\\d+)')
.replace('{format}', ':format([\\w\\.]+)');
var tilePattern = '/rendered/:z(\\d+)/:x(\\d+)/:y(\\d+)' +
':scale(' + SCALE_PATTERN + ')?\.: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') {
} else if (format == 'jpg' || format == 'jpeg') {
format = 'jpeg';
} else {
return res.status(404).send('Invalid format');
return res.status(400).send('Invalid format');
}
var pool = map.renderers[scale];
@@ -198,6 +240,8 @@ module.exports = function(maps, options, prefix) {
var params = {
zoom: mbglZ,
center: [lon, lat],
bearing: bearing,
pitch: pitch,
width: width,
height: height
};
@@ -222,9 +266,19 @@ module.exports = function(maps, options, prefix) {
image.resize(width * scale, height * scale);
}
image.toFormat(format)
.compressionLevel(9)
.toBuffer(function(err, buffer, info) {
image.toFormat(format);
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) {
return res.status(404).send('Not found');
}
@@ -246,31 +300,39 @@ module.exports = function(maps, options, prefix) {
y = req.params.y | 0,
scale = getScale(req.params.scale),
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 tileCenter = mercator.ll([
((x + 0.5) / (1 << z)) * (256 << z),
((y + 0.5) / (1 << z)) * (256 << z)
], z);
return respondImage(z, tileCenter[0], tileCenter[1], tileSize, tileSize,
scale, format, res, next);
return respondImage(z, tileCenter[0], tileCenter[1], 0, 0,
tileSize, tileSize, scale, format, res, next);
});
var staticPattern =
'/static/%s:scale(' + SCALE_PATTERN + ')?\.:format([\\w\\.]+)';
'/static/%s:scale(' + SCALE_PATTERN + ')?\.:format([\\w]+)';
var centerPattern =
util.format(':lon(%s),:lat(%s),:z(\\d+)/:width(\\d+)x:height(\\d+)',
FLOAT_PATTERN, FLOAT_PATTERN);
util.format(':lon(%s),:lat(%s),:z(\\d+):bearing(,%s)?:pitch(,%s)?/' +
':width(\\d+)x:height(\\d+)',
FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN);
app.get(util.format(staticPattern, centerPattern), function(req, res, next) {
var z = req.params.z | 0,
x = +req.params.lon,
y = +req.params.lat,
bearing = +((req.params.bearing || ',0').substring(1)),
pitch = +((req.params.pitch || ',0').substring(1)),
w = req.params.width | 0,
h = req.params.height | 0,
scale = getScale(req.params.scale),
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 =
@@ -278,23 +340,24 @@ module.exports = function(maps, options, prefix) {
FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN);
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,
x = ((+req.params.minx) + (+req.params.maxx)) / 2,
y = ((+req.params.miny) + (+req.params.maxy)) / 2,
w = req.params.width | 0,
h = req.params.height | 0,
x = (bbox[0] + bbox[2]) / 2,
y = (bbox[1] + bbox[3]) / 2;
var minCorner = mercator.px([bbox[0], bbox[3]], z),
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),
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) {
var info = clone(map.tileJSON);
info.tiles = utils.getTileUrls(req.protocol, domains, req.headers.host,
prefix, tilePath, info.format,
req.query.key);
app.get('/rendered.json', function(req, res, next) {
var info = clone(tileJSON);
info.tiles = utils.getTileUrls(req, info.tiles,
'styles/' + id + '/rendered', info.format);
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, reportTiles, 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 = reportTiles(mbtiles);
source.url = 'local://data/' + 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('/' + 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('/' + 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

@@ -1,89 +0,0 @@
'use strict';
var crypto = require('crypto'),
path = require('path');
var clone = require('clone'),
express = require('express'),
mbtiles = require('mbtiles');
var utils = require('./utils');
module.exports = function(maps, options, prefix) {
var app = express().disable('x-powered-by'),
domains = options.domains,
tilePath = '/{z}/{x}/{y}.pbf';
var rootPath = path.join(process.cwd(), options.root || '');
var mbtilesPath = options.mbtiles;
var map = {
tileJSON: {}
};
maps[prefix] = map;
var source = new mbtiles(path.join(rootPath, mbtilesPath), function(err) {
source.getInfo(function(err, info) {
map.tileJSON['name'] = prefix.substr(1);
Object.assign(map.tileJSON, info);
map.tileJSON['tilejson'] = '2.0.0';
map.tileJSON['basename'] = prefix.substr(1);
map.tileJSON['format'] = 'pbf';
Object.assign(map.tileJSON, options.options || {});
});
});
var tilePattern = tilePath
.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) {
var z = req.params.z | 0,
x = req.params.x | 0,
y = req.params.y | 0;
return getTile(z, x, y, function(err, data, headers) {
if (err) {
return next(err);
}
if (headers) {
res.set(headers);
}
if (data == null) {
return res.status(404).send('Not found');
} else {
return res.status(200).send(data);
}
}, res, next);
});
app.get('/index.json', function(req, res, next) {
var info = clone(map.tileJSON);
info.tiles = utils.getTileUrls(req.protocol, domains, req.headers.host,
prefix, tilePath, info.format,
req.query.key);
return res.send(info);
});
return app;
};

View File

@@ -7,70 +7,273 @@ process.env.UV_THREADPOOL_SIZE =
var fs = require('fs'),
path = require('path');
var async = require('async'),
clone = require('clone'),
var clone = require('clone'),
cors = require('cors'),
express = require('express'),
handlebars = require('handlebars'),
mercator = new (require('sphericalmercator'))(),
morgan = require('morgan');
var serve_raster = require('./serve_raster'),
serve_vector = require('./serve_vector'),
var serve_font = require('./serve_font'),
serve_rendered = require('./serve_rendered'),
serve_style = require('./serve_style'),
serve_data = require('./serve_data'),
utils = require('./utils');
module.exports = function(opts, callback) {
var app = express().disable('x-powered-by'),
maps = {};
serving = {
styles: {},
rendered: {},
data: {},
fonts: { // default fonts, always expose these (if they exist)
'Open Sans Regular': true,
'Arial Unicode MS Regular': true
}
};
app.enable('trust proxy');
callback = callback || function() {};
if (process.env.NODE_ENV !== 'production') {
if (process.env.NODE_ENV !== 'production' &&
process.env.NODE_ENV !== 'test') {
app.use(morgan('dev'));
}
var configPath = path.resolve(opts.config),
var configPath = path.resolve(opts.config);
var config;
try {
config = require(configPath);
Object.keys(config).forEach(function(prefix) {
if (config[prefix].cors !== false) {
app.use(prefix, cors());
} catch (e) {
console.log('ERROR: Config file not found or invalid!');
console.log(' See README.md for instructions and sample data.');
process.exit(1);
}
if (config[prefix].style) {
app.use(prefix, serve_raster(maps, config[prefix], prefix));
var options = config.options || {};
var paths = options.paths || {};
options.paths = paths;
paths.root = path.resolve(process.cwd(), paths.root || '');
paths.styles = path.resolve(paths.root, paths.styles || '');
paths.fonts = path.resolve(paths.root, paths.fonts || '');
paths.sprites = path.resolve(paths.root, paths.sprites || '');
paths.mbtiles = path.resolve(paths.root, paths.mbtiles || '');
var data = clone(config.data || {});
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 (item.serve_data !== false) {
app.use('/styles/', serve_style(options, serving.styles, item, id,
function(mbtiles) {
var dataItemId;
Object.keys(data).forEach(function(id) {
if (data[id].mbtiles == mbtiles) {
dataItemId = id;
}
});
if (dataItemId) { // mbtiles exist in the data config
return dataItemId;
} else {
app.use(prefix, serve_vector(maps, config[prefix], prefix));
var id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles;
while (data[id]) id += '_';
data[id] = {
'mbtiles': mbtiles
};
return id;
}
}, function(font) {
serving.fonts[font] = true;
}));
}
if (item.serve_rendered !== false) {
app.use('/styles/' + id + '/',
serve_rendered(options, serving.rendered, item, id));
}
});
// serve index.html on the root
app.use('/', express.static(path.join(__dirname, '../public')));
if (Object.keys(serving.styles).length > 0) {
// serve fonts only if serving some styles
app.use('/fonts/', serve_font(options, serving.fonts));
}
// aggregate index.json on root for multiple sources
app.use(cors());
Object.keys(data).forEach(function(id) {
var item = data[id];
if (!item.mbtiles || item.mbtiles.length == 0) {
console.log('Missing "mbtiles" property for ' + id);
return;
}
app.use('/data/', serve_data(options, serving.data, 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'
});
});
res.send(result);
});
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('/rendered.json', function(req, res, next) {
res.send(addTileJSONs([], req, 'rendered'));
});
app.get('/data.json', function(req, res, next) {
res.send(addTileJSONs([], req, 'data'));
});
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(
req.protocol, config[prefix].domains, req.headers.host,
prefix, '/{z}/{x}/{y}.{format}', info.format, req.query.key);
callback(null, info);
});
});
return async.parallel(queue, function(err, results) {
return res.send(results);
});
res.send(addTileJSONs(addTileJSONs([], req, 'rendered'), req, 'data'));
});
app.listen(process.env.PORT || opts.port, function() {
//------------------------------------
// 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() {
console.log('Listening at http://%s:%d/',
this.address().address, this.address().port);
return callback();
});
setTimeout(callback, 1000);
return {
app: app,
server: server
};
};

View File

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

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('/rendered.json');
testTileJSONArray('/data.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('/styles/test/rendered.json', 'test');
testTileJSON('/data/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 = '/styles/' + prefix + '/static/' + 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);
});

28
test/tiles_data.js Normal file
View File

@@ -0,0 +1,28 @@
var testTile = function(prefix, z, x, y, status) {
var path = '/data/' + 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
});
});

44
test/tiles_rendered.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 = '/styles/' + prefix + '/rendered/' + 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); //TODO: test this
});
});