Compare commits
24 Commits
v0.0.1-pre
...
v0.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
074c873826 | ||
|
|
72ad669502 | ||
|
|
2210ea2f35 | ||
|
|
a39aa0bd8a | ||
|
|
1c73c14d84 | ||
|
|
8a46bd8b88 | ||
|
|
d1e33d04cb | ||
|
|
06b88bbbe7 | ||
|
|
946cb2ca5f | ||
|
|
d742672238 | ||
|
|
b98b7244f6 | ||
|
|
d4fa224d04 | ||
|
|
4c40700bac | ||
|
|
6f644a4c03 | ||
|
|
9efa22b52b | ||
|
|
a0007b42f9 | ||
|
|
a495993e68 | ||
|
|
ad867f305b | ||
|
|
d6e17c1a3a | ||
|
|
9736649244 | ||
|
|
832b2d22be | ||
|
|
47f6c90a98 | ||
|
|
77755b548b | ||
|
|
7ca7fc721f |
@@ -1,3 +1,4 @@
|
||||
.git
|
||||
node_modules
|
||||
test_data
|
||||
test
|
||||
|
||||
20
.travis.yml
Normal file
20
.travis.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "5"
|
||||
env:
|
||||
- CXX=g++-4.8
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
||||
before_install:
|
||||
- sudo apt-get update -qq
|
||||
- sudo apt-get install -qq xvfb
|
||||
install:
|
||||
- npm install
|
||||
- wget https://github.com/klokantech/tileserver-gl/releases/download/v0.0.2/test_data.zip
|
||||
- unzip -q test_data.zip -d test_data
|
||||
script:
|
||||
- xvfb-run --server-args="-screen 0 1024x768x24" npm test
|
||||
99
README.md
99
README.md
@@ -1,4 +1,5 @@
|
||||
# tileserver-gl
|
||||
[](https://travis-ci.org/klokantech/tileserver-gl)
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -10,64 +11,86 @@
|
||||
- `npm install`
|
||||
- `node src/main.js`
|
||||
|
||||
## Sample data
|
||||
Sample data can be downloaded at https://github.com/klokantech/tileserver-gl/releases/download/v0.0.2/test_data.zip
|
||||
|
||||
#### Usage
|
||||
- unpack somewhere and `cd` to the directory
|
||||
- `docker run -it -v $(pwd):/data -p 8080:80 klokantech/tileserver-gl`
|
||||
- (or `node path/to/repo/src/main.js`)
|
||||
|
||||
#### Data
|
||||
- tiles from http://osm2vectortiles.org/
|
||||
- styles modified from https://github.com/klokantech/osm2vectortiles-gl-styles
|
||||
|
||||
## Configuration
|
||||
|
||||
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
|
||||
|
||||
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",
|
||||
"options": {
|
||||
"paths": {
|
||||
"root": "",
|
||||
"fonts": "glyphs",
|
||||
"sprites": "sprites",
|
||||
"styles": "styles",
|
||||
"mbtiles": ""
|
||||
},
|
||||
"domains": [
|
||||
"localhost:8080",
|
||||
"127.0.0.1:8080"
|
||||
],
|
||||
"options": {
|
||||
"type": "overlay",
|
||||
"bounds": [5.8559113, 45.717995, 10.5922941, 47.9084648]
|
||||
]
|
||||
},
|
||||
"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]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/hybrid": {
|
||||
"root": "test_data",
|
||||
"style": "styles/satellite-hybrid-v8.json",
|
||||
"options": {
|
||||
"format": "webp"
|
||||
"vector": {
|
||||
"zurich-vector": {
|
||||
"mbtiles": "zurich.mbtiles"
|
||||
}
|
||||
},
|
||||
"/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`
|
||||
- Rasterized tiles are at `/raster/{id}/{z}/{x}/{y}[@2x].{format}`
|
||||
- The optional `@2x` (or `@3x`) part can be used to render HiDPI (retina) tiles
|
||||
- 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 `/raster/{id}.json`
|
||||
- Static images are rendered at:
|
||||
- `/static/{id}/{lon},{lat},{zoom}/{width}x{height}[@2x].{format}` (center-based)
|
||||
- `/static/{id}/{minx},{miny},{maxx},{maxy}/{zoom}[@2x].{format}` (area-based)
|
||||
- Vector tiles at `/vector/{mbtiles}/{z}/{x}/{y}.pbf`
|
||||
- TileJSON at `/vector/{mbtiles}.json`
|
||||
- Array of all TileJSONs at `/index.json` (`/raster.json`; `/vector.json`)
|
||||
|
||||
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tileserver-gl",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"description": "Map tile server for JSON GL styles - serverside generated raster tiles",
|
||||
"main": "src/main.js",
|
||||
"authors": [
|
||||
@@ -10,6 +10,9 @@
|
||||
"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",
|
||||
@@ -23,5 +26,10 @@
|
||||
"request": "2.69.0",
|
||||
"sharp": "0.13.1",
|
||||
"sphericalmercator": "1.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"should": "^8.2.2",
|
||||
"mocha": "^2.4.5",
|
||||
"supertest": "^1.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,15 @@
|
||||
}
|
||||
#header {height:120px;}
|
||||
#sidebar, #code, #map, #wall {top:120px;}
|
||||
#styles-link {position:absolute;top:90px;left:0;right:0;text-align:center;z-index:10000;font-size:16px;color:#eee;}
|
||||
</style>
|
||||
<body>
|
||||
<script>
|
||||
tileserver({
|
||||
index: "index.json" + location.search,
|
||||
tilejson: "%n/index.json" + location.search
|
||||
tilejson: "raster/%n.json" + location.search
|
||||
});
|
||||
</script>
|
||||
<a href="/styles.html" id="styles-link">View available styles</a>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
244
public/mapbox-gl.css
Normal file
244
public/mapbox-gl.css
Normal file
@@ -0,0 +1,244 @@
|
||||
.mapboxgl-map {
|
||||
font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
.mapboxgl-canvas-container.mapboxgl-interactive,
|
||||
.mapboxgl-ctrl-nav-compass {
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
}
|
||||
.mapboxgl-canvas-container.mapboxgl-interactive:active,
|
||||
.mapboxgl-ctrl-nav-compass:active {
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-top-left,
|
||||
.mapboxgl-ctrl-top-right,
|
||||
.mapboxgl-ctrl-bottom-left,
|
||||
.mapboxgl-ctrl-bottom-right { position:absolute; }
|
||||
.mapboxgl-ctrl-top-left { top:0; left:0; }
|
||||
.mapboxgl-ctrl-top-right { top:0; right:0; }
|
||||
.mapboxgl-ctrl-bottom-left { bottom:0; left:0; }
|
||||
.mapboxgl-ctrl-bottom-right { right:0; bottom:0; }
|
||||
|
||||
.mapboxgl-ctrl { clear:both; }
|
||||
.mapboxgl-ctrl-top-left .mapboxgl-ctrl { margin:10px 0 0 10px; float:left; }
|
||||
.mapboxgl-ctrl-top-right .mapboxgl-ctrl{ margin:10px 10px 0 0; float:right; }
|
||||
.mapboxgl-ctrl-bottom-left .mapboxgl-ctrl { margin:0 0 10px 10px; float:left; }
|
||||
.mapboxgl-ctrl-bottom-right .mapboxgl-ctrl { margin:0 10px 10px 0; float:right; }
|
||||
|
||||
.mapboxgl-ctrl-group {
|
||||
border-radius: 4px;
|
||||
-moz-box-shadow: 0px 0px 2px rgba(0,0,0,0.1);
|
||||
-webkit-box-shadow: 0px 0px 2px rgba(0,0,0,0.1);
|
||||
box-shadow: 0px 0px 0px 2px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
.mapboxgl-ctrl-group > button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: block;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid #ddd;
|
||||
box-sizing: border-box;
|
||||
background-color: rgba(0,0,0,0);
|
||||
cursor: pointer;
|
||||
}
|
||||
/* https://bugzilla.mozilla.org/show_bug.cgi?id=140562 */
|
||||
.mapboxgl-ctrl > button::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.mapboxgl-ctrl > button:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.mapboxgl-ctrl > button:hover {
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
}
|
||||
.mapboxgl-ctrl-icon,
|
||||
.mapboxgl-ctrl-icon > div.arrow {
|
||||
speak: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
.mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-out {
|
||||
padding: 5px;
|
||||
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%270%200%2020%2020%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%0A%20%20%3Cpath%20style%3D%27fill%3A%23333333%3B%27%20d%3D%27m%207%2C9%20c%20-0.554%2C0%20-1%2C0.446%20-1%2C1%200%2C0.554%200.446%2C1%201%2C1%20l%206%2C0%20c%200.554%2C0%201%2C-0.446%201%2C-1%200%2C-0.554%20-0.446%2C-1%20-1%2C-1%20z%27%20%2F%3E%0A%3C%2Fsvg%3E%0A");
|
||||
}
|
||||
.mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-in {
|
||||
padding: 5px;
|
||||
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%270%200%2020%2020%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%0A%20%20%3Cpath%20style%3D%27fill%3A%23333333%3B%27%20d%3D%27M%2010%206%20C%209.446%206%209%206.4459904%209%207%20L%209%209%20L%207%209%20C%206.446%209%206%209.446%206%2010%20C%206%2010.554%206.446%2011%207%2011%20L%209%2011%20L%209%2013%20C%209%2013.55401%209.446%2014%2010%2014%20C%2010.554%2014%2011%2013.55401%2011%2013%20L%2011%2011%20L%2013%2011%20C%2013.554%2011%2014%2010.554%2014%2010%20C%2014%209.446%2013.554%209%2013%209%20L%2011%209%20L%2011%207%20C%2011%206.4459904%2010.554%206%2010%206%20z%27%20%2F%3E%0A%3C%2Fsvg%3E%0A");
|
||||
}
|
||||
.mapboxgl-ctrl-icon.mapboxgl-ctrl-compass > div.arrow {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 5px;
|
||||
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2020%2020%27%3E%0A%09%3Cpolygon%20fill%3D%27%23333333%27%20points%3D%276%2C9%2010%2C1%2014%2C9%27%2F%3E%0A%09%3Cpolygon%20fill%3D%27%23CCCCCC%27%20points%3D%276%2C11%2010%2C19%2014%2C11%20%27%2F%3E%0A%3C%2Fsvg%3E");
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl.mapboxgl-ctrl-attrib {
|
||||
padding: 0 5px;
|
||||
background-color: rgba(255,255,255,0.5);
|
||||
margin: 0;
|
||||
}
|
||||
.mapboxgl-ctrl-attrib a {
|
||||
color: rgba(0,0,0,0.75);
|
||||
text-decoration: none;
|
||||
}
|
||||
.mapboxgl-ctrl-attrib a:hover {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.mapboxgl-ctrl-attrib .mapbox-improve-map {
|
||||
font-weight: bold;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.mapboxgl-popup {
|
||||
position: absolute;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
will-change: transform;
|
||||
pointer-events: none;
|
||||
}
|
||||
.mapboxgl-popup-anchor-top,
|
||||
.mapboxgl-popup-anchor-top-left,
|
||||
.mapboxgl-popup-anchor-top-right {
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
.mapboxgl-popup-anchor-bottom,
|
||||
.mapboxgl-popup-anchor-bottom-left,
|
||||
.mapboxgl-popup-anchor-bottom-right {
|
||||
-webkit-flex-direction: column-reverse;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
.mapboxgl-popup-anchor-left {
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
}
|
||||
.mapboxgl-popup-anchor-right {
|
||||
-webkit-flex-direction: row-reverse;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.mapboxgl-popup-tip {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 10px solid transparent;
|
||||
z-index: 1;
|
||||
}
|
||||
.mapboxgl-popup-anchor-top .mapboxgl-popup-tip {
|
||||
-webkit-align-self: center;
|
||||
align-self: center;
|
||||
border-top: none;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip {
|
||||
-webkit-align-self: flex-start;
|
||||
align-self: flex-start;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip {
|
||||
-webkit-align-self: flex-end;
|
||||
align-self: flex-end;
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip {
|
||||
-webkit-align-self: center;
|
||||
align-self: center;
|
||||
border-bottom: none;
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip {
|
||||
-webkit-align-self: flex-start;
|
||||
align-self: flex-start;
|
||||
border-bottom: none;
|
||||
border-left: none;
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip {
|
||||
-webkit-align-self: flex-end;
|
||||
align-self: flex-end;
|
||||
border-bottom: none;
|
||||
border-right: none;
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.mapboxgl-popup-anchor-left .mapboxgl-popup-tip {
|
||||
-webkit-align-self: center;
|
||||
align-self: center;
|
||||
border-left: none;
|
||||
border-right-color: #fff;
|
||||
}
|
||||
.mapboxgl-popup-anchor-right .mapboxgl-popup-tip {
|
||||
-webkit-align-self: center;
|
||||
align-self: center;
|
||||
border-right: none;
|
||||
border-left-color: #fff;
|
||||
}
|
||||
.mapboxgl-popup-close-button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
border: none;
|
||||
border-radius: 0 3px 0 0;
|
||||
cursor: pointer;
|
||||
background-color: rgba(0,0,0,0);
|
||||
}
|
||||
.mapboxgl-popup-close-button:hover {
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
}
|
||||
.mapboxgl-popup-content {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.10);
|
||||
padding: 10px 10px 15px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.mapboxgl-popup-anchor-top-left .mapboxgl-popup-content {
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
.mapboxgl-popup-anchor-top-right .mapboxgl-popup-content {
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-content {
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-content {
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.mapboxgl-crosshair,
|
||||
.mapboxgl-crosshair .mapboxgl-interactive,
|
||||
.mapboxgl-crosshair .mapboxgl-interactive:active {
|
||||
cursor: crosshair;
|
||||
}
|
||||
.mapboxgl-boxzoom {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
background: #fff;
|
||||
border: 2px dotted #202020;
|
||||
opacity: 0.5;
|
||||
}
|
||||
@media print {
|
||||
.mapbox-improve-map {
|
||||
display:none;
|
||||
}
|
||||
}
|
||||
338
public/mapbox-gl.js
Normal file
338
public/mapbox-gl.js
Normal file
File diff suppressed because one or more lines are too long
58
public/styles.html
Normal file
58
public/styles.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" type="text/css" href="mapbox-gl.css" />
|
||||
<script src="mapbox-gl.js"></script>
|
||||
<title>Offline vector tiles</title>
|
||||
<style>
|
||||
body { margin:0; padding:0; }
|
||||
#map { position:absolute; top:0; bottom:0; width:100%; }
|
||||
#dropdown { position: absolute; top: 10px; left:10px; }
|
||||
</style>
|
||||
<body>
|
||||
<div id='map'></div>
|
||||
<select id='dropdown'></select>
|
||||
<script>
|
||||
var styles;
|
||||
var request = new XMLHttpRequest();
|
||||
request.responseType = 'json';
|
||||
request.open('GET', '/styles.json', true);
|
||||
request.onload = function(e) {
|
||||
if (request.readyState != 4) return;
|
||||
if (request.status === 200) {
|
||||
styles = request.response;
|
||||
}
|
||||
|
||||
var map = new mapboxgl.Map({
|
||||
container: 'map',
|
||||
style: 'styles/' + styles[0].id + '.json',
|
||||
center: [0, 0],
|
||||
zoom: 0,
|
||||
hash: true
|
||||
});
|
||||
map.addControl(new mapboxgl.Navigation());
|
||||
|
||||
var select = document.getElementById('dropdown');
|
||||
|
||||
for (var i in styles) {
|
||||
var style = styles[i];
|
||||
var el = document.createElement('option');
|
||||
el.textContent = style.name + ' (' + style.id + '.json)';
|
||||
el.value = style.id;
|
||||
select.appendChild(el);
|
||||
};
|
||||
|
||||
select.onchange = function() {
|
||||
mapboxgl.util.getJSON(
|
||||
'styles/' + document.getElementById('dropdown').value + '.json',
|
||||
function (err, style) {
|
||||
if (err) throw err;
|
||||
map.setStyle(style);
|
||||
});
|
||||
}
|
||||
};
|
||||
request.send(null);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
60
src/serve_font.js
Normal file
60
src/serve_font.js
Normal file
@@ -0,0 +1,60 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
path = require('path'),
|
||||
fs = require('fs');
|
||||
|
||||
var clone = require('clone'),
|
||||
express = require('express');
|
||||
|
||||
|
||||
module.exports = function(options, allowedFonts) {
|
||||
var app = express().disable('x-powered-by');
|
||||
|
||||
var fontPath = options.paths.fonts;
|
||||
|
||||
var getFontPbf = function(name, range, callback) {
|
||||
// if some of the files failed to load (does not exist or not allowed),
|
||||
// return empty buffer so the other fonts can still work
|
||||
if (allowedFonts[name]) {
|
||||
var filename = path.join(fontPath, name, range + '.pbf');
|
||||
return fs.readFile(filename, function(err, data) {
|
||||
if (err) {
|
||||
console.log('Font load error:', filename);
|
||||
return callback(null, new Buffer([]));
|
||||
} else {
|
||||
return callback(null, data);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return callback(null, new Buffer([]));
|
||||
}
|
||||
};
|
||||
|
||||
app.get('/fonts/:fontstack/:range([\\d]+-[\\d]+).pbf',
|
||||
function(req, res, next) {
|
||||
var fontstack = decodeURI(req.params.fontstack);
|
||||
var range = req.params.range;
|
||||
|
||||
var fonts = fontstack.split(',');
|
||||
|
||||
var queue = [];
|
||||
fonts.forEach(function(font) {
|
||||
queue.push(function(callback) {
|
||||
getFontPbf(font, range, callback);
|
||||
});
|
||||
});
|
||||
|
||||
return async.parallel(queue, function(err, results) {
|
||||
var concated = Buffer.concat(results);
|
||||
if (err || concated.length == 0) {
|
||||
return res.status(400).send('');
|
||||
} else {
|
||||
res.header('Content-type', 'application/x-protobuf');
|
||||
return res.send(concated);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
};
|
||||
@@ -31,18 +31,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 +50,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') {
|
||||
@@ -128,19 +127,22 @@ module.exports = function(maps, options, prefix) {
|
||||
});
|
||||
};
|
||||
|
||||
styleJSON = require(path.join(rootPath, styleUrl));
|
||||
styleJSON = require(path.join(options.paths.styles, styleFile));
|
||||
styleJSON.sprite = 'sprites://' + path.basename(styleFile, '.json');
|
||||
styleJSON.glyphs = 'fonts://{fontstack}/{range}.pbf';
|
||||
|
||||
map.tileJSON = {
|
||||
var tileJSON = {
|
||||
'tilejson': '2.0.0',
|
||||
'name': styleJSON.name,
|
||||
'basename': prefix.substr(1),
|
||||
'basename': id,
|
||||
'minzoom': 0,
|
||||
'maxzoom': 20,
|
||||
'bounds': [-180, -85.0511, 180, 85.0511],
|
||||
'format': 'png',
|
||||
'type': 'baselayer'
|
||||
};
|
||||
Object.assign(map.tileJSON, options.options || {});
|
||||
Object.assign(tileJSON, params.tilejson || {});
|
||||
tileJSON.tiles = params.domains || options.domains;
|
||||
|
||||
var queue = [];
|
||||
Object.keys(styleJSON.sources).forEach(function(name) {
|
||||
@@ -151,14 +153,15 @@ 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) {
|
||||
Object.assign(source, info);
|
||||
source.basename = name;
|
||||
source.tiles = [
|
||||
// meta url which will be detected when requested
|
||||
'mbtiles://' + name + tilePath.replace('{format}', 'pbf')
|
||||
'mbtiles://' + name + '/{z}/{x}/{y}.pbf'
|
||||
];
|
||||
callback(null);
|
||||
});
|
||||
@@ -174,22 +177,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 = '/raster/' + id + '/: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 +204,8 @@ module.exports = function(maps, options, prefix) {
|
||||
var params = {
|
||||
zoom: mbglZ,
|
||||
center: [lon, lat],
|
||||
bearing: bearing,
|
||||
pitch: pitch,
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
@@ -246,31 +254,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/' + id + '/%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 +294,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('/raster/' + id + '.json', function(req, res, next) {
|
||||
var info = clone(tileJSON);
|
||||
info.tiles = utils.getTileUrls(req, info.tiles,
|
||||
'raster/' + id, info.format);
|
||||
return res.send(info);
|
||||
});
|
||||
|
||||
|
||||
82
src/serve_style.js
Normal file
82
src/serve_style.js
Normal file
@@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
|
||||
var path = require('path'),
|
||||
fs = require('fs');
|
||||
|
||||
var clone = require('clone'),
|
||||
express = require('express');
|
||||
|
||||
|
||||
module.exports = function(options, repo, params, id, reportVector, reportFont) {
|
||||
var app = express().disable('x-powered-by');
|
||||
|
||||
var styleFile = path.join(options.paths.styles, params.style);
|
||||
|
||||
var styleJSON = clone(require(styleFile));
|
||||
Object.keys(styleJSON.sources).forEach(function(name) {
|
||||
var source = styleJSON.sources[name];
|
||||
var url = source.url;
|
||||
if (url.lastIndexOf('mbtiles:', 0) === 0) {
|
||||
var mbtiles = url.substring('mbtiles://'.length);
|
||||
var identifier = reportVector(mbtiles);
|
||||
source.url = 'local://vector/' + identifier + '.json';
|
||||
}
|
||||
});
|
||||
|
||||
var findFontReferences = function(obj) {
|
||||
Object.keys(obj).forEach(function(key) {
|
||||
var value = obj[key];
|
||||
if (key == 'text-font') {
|
||||
if (value && value.length > 0) {
|
||||
value.forEach(reportFont);
|
||||
}
|
||||
} else if (value && typeof value == 'object') {
|
||||
findFontReferences(value);
|
||||
}
|
||||
});
|
||||
};
|
||||
styleJSON.layers.forEach(findFontReferences);
|
||||
|
||||
var spritePath = path.join(options.paths.sprites,
|
||||
path.basename(styleFile, '.json'));
|
||||
|
||||
styleJSON.sprite = 'local://styles/' + id + '/sprite';
|
||||
styleJSON.glyphs = 'local://fonts/{fontstack}/{range}.pbf';
|
||||
|
||||
repo[id] = styleJSON;
|
||||
|
||||
app.get('/styles/' + id + '.json', function(req, res, next) {
|
||||
var fixUrl = function(url) {
|
||||
return url.replace(
|
||||
'local://', req.protocol + '://' + req.headers.host + '/');
|
||||
};
|
||||
|
||||
var styleJSON_ = clone(styleJSON);
|
||||
Object.keys(styleJSON_.sources).forEach(function(name) {
|
||||
var source = styleJSON_.sources[name];
|
||||
source.url = fixUrl(source.url);
|
||||
});
|
||||
styleJSON_.sprite = fixUrl(styleJSON_.sprite);
|
||||
styleJSON_.glyphs = fixUrl(styleJSON_.glyphs);
|
||||
return res.send(styleJSON_);
|
||||
});
|
||||
|
||||
app.get('/styles/' + id + '/sprite:scale(@[23]x)?\.:format([\\w]+)',
|
||||
function(req, res, next) {
|
||||
var scale = req.params.scale,
|
||||
format = req.params.format;
|
||||
var filename = spritePath + (scale || '') + '.' + format;
|
||||
return fs.readFile(filename, function(err, data) {
|
||||
if (err) {
|
||||
console.log('Sprite load error:', filename);
|
||||
return res.status(404).send('File not found');
|
||||
} else {
|
||||
if (format == 'json') res.header('Content-type', 'application/json');
|
||||
if (format == 'png') res.header('Content-type', 'image/png');
|
||||
return res.send(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
};
|
||||
@@ -9,79 +9,69 @@ var clone = require('clone'),
|
||||
|
||||
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';
|
||||
module.exports = function(options, repo, params, id) {
|
||||
var app = express().disable('x-powered-by');
|
||||
|
||||
var rootPath = path.join(process.cwd(), options.root || '');
|
||||
|
||||
var mbtilesPath = options.mbtiles;
|
||||
var map = {
|
||||
tileJSON: {}
|
||||
var mbtilesFile = params.mbtiles;
|
||||
var tileJSON = {
|
||||
'tiles': params.domains || options.domains
|
||||
};
|
||||
maps[prefix] = map;
|
||||
|
||||
var source = new mbtiles(path.join(rootPath, mbtilesPath), function(err) {
|
||||
repo[id] = tileJSON;
|
||||
|
||||
var source = new mbtiles(path.join(options.paths.mbtiles, mbtilesFile),
|
||||
function(err) {
|
||||
source.getInfo(function(err, info) {
|
||||
map.tileJSON['name'] = prefix.substr(1);
|
||||
tileJSON['name'] = id;
|
||||
|
||||
Object.assign(map.tileJSON, info);
|
||||
Object.assign(tileJSON, info);
|
||||
|
||||
map.tileJSON['tilejson'] = '2.0.0';
|
||||
map.tileJSON['basename'] = prefix.substr(1);
|
||||
map.tileJSON['format'] = 'pbf';
|
||||
tileJSON['tilejson'] = '2.0.0';
|
||||
tileJSON['basename'] = id;
|
||||
tileJSON['format'] = 'pbf';
|
||||
|
||||
Object.assign(map.tileJSON, options.options || {});
|
||||
Object.assign(tileJSON, params.tilejson || {});
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
var tilePattern = '/vector/' + id + '/:z(\\d+)/:x(\\d+)/:y(\\d+).pbf';
|
||||
|
||||
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 (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;
|
||||
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);
|
||||
}
|
||||
}, 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('/vector/' + id + '.json', function(req, res, next) {
|
||||
var info = clone(tileJSON);
|
||||
info.tiles = utils.getTileUrls(req, info.tiles,
|
||||
'vector/' + id, info.format);
|
||||
return res.send(info);
|
||||
});
|
||||
|
||||
|
||||
139
src/server.js
139
src/server.js
@@ -7,70 +7,151 @@ 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'),
|
||||
morgan = require('morgan');
|
||||
|
||||
var serve_raster = require('./serve_raster'),
|
||||
var serve_font = require('./serve_font'),
|
||||
serve_raster = require('./serve_raster'),
|
||||
serve_style = require('./serve_style'),
|
||||
serve_vector = require('./serve_vector'),
|
||||
utils = require('./utils');
|
||||
|
||||
module.exports = function(opts, callback) {
|
||||
var app = express().disable('x-powered-by'),
|
||||
maps = {};
|
||||
serving = {
|
||||
styles: {},
|
||||
raster: {},
|
||||
vector: {},
|
||||
fonts: { // default fonts, always expose these (if they exist)
|
||||
'Open Sans Regular': true,
|
||||
'Arial Unicode MS Regular': true
|
||||
}
|
||||
};
|
||||
|
||||
app.enable('trust proxy');
|
||||
|
||||
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),
|
||||
config = require(configPath);
|
||||
|
||||
Object.keys(config).forEach(function(prefix) {
|
||||
if (config[prefix].cors !== false) {
|
||||
app.use(prefix, cors());
|
||||
var options = config.options || {};
|
||||
var paths = options.paths || {};
|
||||
options.paths = paths;
|
||||
paths.root = path.join(process.cwd(), paths.root || '');
|
||||
paths.styles = path.join(paths.root, paths.styles || '');
|
||||
paths.fonts = path.join(paths.root, paths.fonts || '');
|
||||
paths.sprites = path.join(paths.root, paths.sprites || '');
|
||||
paths.mbtiles = path.join(paths.root, paths.mbtiles || '');
|
||||
|
||||
var vector = clone(config.vector);
|
||||
|
||||
Object.keys(config.styles || {}).forEach(function(id) {
|
||||
var item = config.styles[id];
|
||||
if (!item.style || item.style.length == 0) {
|
||||
console.log('Missing "style" property for ' + id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (config[prefix].style) {
|
||||
app.use(prefix, serve_raster(maps, config[prefix], prefix));
|
||||
} else {
|
||||
app.use(prefix, serve_vector(maps, config[prefix], prefix));
|
||||
if (item.vector !== false) {
|
||||
app.use('/', serve_style(options, serving.styles, item, id,
|
||||
function(mbtiles) {
|
||||
var vectorItemId;
|
||||
Object.keys(vector).forEach(function(id) {
|
||||
if (vector[id].mbtiles == mbtiles) {
|
||||
vectorItemId = id;
|
||||
}
|
||||
});
|
||||
if (vectorItemId) { // mbtiles exist in the vector config
|
||||
return vectorItemId;
|
||||
} else {
|
||||
var id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles;
|
||||
while (vector[id]) id += '_';
|
||||
vector[id] = {
|
||||
'mbtiles': mbtiles
|
||||
};
|
||||
return id;
|
||||
}
|
||||
}, function(font) {
|
||||
serving.fonts[font] = true;
|
||||
}));
|
||||
}
|
||||
if (item.raster !== false) {
|
||||
app.use('/', serve_raster(options, serving.raster, item, id));
|
||||
}
|
||||
});
|
||||
|
||||
// serve index.html on the root
|
||||
app.use('/', express.static(path.join(__dirname, '../public')));
|
||||
if (Object.keys(serving.styles).length > 0) {
|
||||
// serve fonts only if serving some styles
|
||||
app.use('/', serve_font(options, serving.fonts));
|
||||
}
|
||||
|
||||
// aggregate index.json on root for multiple sources
|
||||
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);
|
||||
//TODO: cors
|
||||
|
||||
info.tiles = utils.getTileUrls(
|
||||
req.protocol, config[prefix].domains, req.headers.host,
|
||||
prefix, '/{z}/{x}/{y}.{format}', info.format, req.query.key);
|
||||
Object.keys(vector).forEach(function(id) {
|
||||
var item = vector[id];
|
||||
if (!item.mbtiles || item.mbtiles.length == 0) {
|
||||
console.log('Missing "mbtiles" property for ' + id);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, info);
|
||||
app.use('/', serve_vector(options, serving.vector, item, id));
|
||||
});
|
||||
|
||||
app.get('/styles.json', function(req, res, next) {
|
||||
var result = [];
|
||||
Object.keys(serving.styles).forEach(function(id) {
|
||||
var styleJSON = serving.styles[id];
|
||||
result.push({
|
||||
version: styleJSON.version,
|
||||
name: styleJSON.name,
|
||||
id: id,
|
||||
url: req.protocol + '://' + req.headers.host + '/styles/' + id + '.json'
|
||||
});
|
||||
});
|
||||
return async.parallel(queue, function(err, results) {
|
||||
return res.send(results);
|
||||
});
|
||||
res.send(result);
|
||||
});
|
||||
|
||||
app.listen(process.env.PORT || opts.port, function() {
|
||||
var addTileJSONs = function(arr, req, type) {
|
||||
Object.keys(serving[type]).forEach(function(id) {
|
||||
var info = clone(serving[type][id]);
|
||||
info.tiles = utils.getTileUrls(req, info.tiles,
|
||||
type + '/' + id, info.format);
|
||||
arr.push(info);
|
||||
});
|
||||
return arr;
|
||||
};
|
||||
|
||||
app.get('/raster.json', function(req, res, next) {
|
||||
res.send(addTileJSONs([], req, 'raster'));
|
||||
});
|
||||
app.get('/vector.json', function(req, res, next) {
|
||||
res.send(addTileJSONs([], req, 'vector'));
|
||||
});
|
||||
app.get('/index.json', function(req, res, next) {
|
||||
res.send(addTileJSONs(addTileJSONs([], req, 'raster'), req, 'vector'));
|
||||
});
|
||||
|
||||
// serve viewer on the root
|
||||
app.use('/', express.static(path.join(__dirname, '../public')));
|
||||
|
||||
var server = app.listen(process.env.PORT || opts.port, function() {
|
||||
console.log('Listening at http://%s:%d/',
|
||||
this.address().address, this.address().port);
|
||||
|
||||
return callback();
|
||||
});
|
||||
|
||||
setTimeout(callback, 1000);
|
||||
return {
|
||||
app: app,
|
||||
server: server
|
||||
};
|
||||
};
|
||||
|
||||
14
src/utils.js
14
src/utils.js
@@ -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,19 +8,16 @@ 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;
|
||||
|
||||
69
test/metadata.js
Normal file
69
test/metadata.js
Normal file
@@ -0,0 +1,69 @@
|
||||
var testTileJSONArray = function(url) {
|
||||
describe(url + ' is array of TileJSONs', function() {
|
||||
it('is json', function(done) {
|
||||
supertest(app)
|
||||
.get(url)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /application\/json/, done);
|
||||
});
|
||||
|
||||
it('is non-empty array', function(done) {
|
||||
supertest(app)
|
||||
.get(url)
|
||||
.expect(function(res) {
|
||||
res.body.should.be.Array();
|
||||
res.body.length.should.be.greaterThan(0);
|
||||
}).end(done);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var testTileJSON = function(url, basename) {
|
||||
describe(url + ' is TileJSON', function() {
|
||||
it('is json', function(done) {
|
||||
supertest(app)
|
||||
.get(url)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /application\/json/, done);
|
||||
});
|
||||
|
||||
it('has valid basename and tiles', function(done) {
|
||||
supertest(app)
|
||||
.get(url)
|
||||
.expect(function(res) {
|
||||
res.body.basename.should.equal(basename);
|
||||
res.body.tiles.length.should.be.greaterThan(0);
|
||||
}).end(done);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
describe('Metadata', function() {
|
||||
testTileJSONArray('/index.json');
|
||||
testTileJSONArray('/raster.json');
|
||||
testTileJSONArray('/vector.json');
|
||||
|
||||
describe('/styles.json is valid array', function() {
|
||||
it('is json', function(done) {
|
||||
supertest(app)
|
||||
.get('/styles.json')
|
||||
.expect(200)
|
||||
.expect('Content-Type', /application\/json/, done);
|
||||
});
|
||||
|
||||
it('contains valid item', function(done) {
|
||||
supertest(app)
|
||||
.get('/styles.json')
|
||||
.expect(function(res) {
|
||||
res.body.should.be.Array();
|
||||
res.body.length.should.be.greaterThan(0);
|
||||
res.body[0].version.should.equal(8);
|
||||
res.body[0].id.should.be.String();
|
||||
res.body[0].name.should.be.String();
|
||||
}).end(done);
|
||||
});
|
||||
});
|
||||
|
||||
testTileJSON('/raster/test.json', 'test');
|
||||
testTileJSON('/vector/zurich-vector.json', 'zurich-vector');
|
||||
});
|
||||
20
test/setup.js
Normal file
20
test/setup.js
Normal 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
80
test/static.js
Normal file
@@ -0,0 +1,80 @@
|
||||
var testStatic = function(prefix, q, format, status, scale, type) {
|
||||
if (scale) q += '@' + scale + 'x';
|
||||
var path = '/static/' + prefix + '/' + q + '.' + format;
|
||||
it(path + ' returns ' + status, function(done) {
|
||||
var test = supertest(app).get(path);
|
||||
if (status) test.expect(status);
|
||||
if (type) test.expect('Content-Type', type);
|
||||
test.end(done);
|
||||
});
|
||||
};
|
||||
|
||||
describe('Static endpoints', function() {
|
||||
describe('center-based', function() {
|
||||
describe('valid requests', function() {
|
||||
describe('various formats', function() {
|
||||
testStatic('test', '0,0,0/256x256', 'png', 200, undefined, /image\/png/);
|
||||
testStatic('test', '0,0,0/256x256', 'jpg', 200, undefined, /image\/jpeg/);
|
||||
testStatic('test', '0,0,0/256x256', 'jpeg', 200, undefined, /image\/jpeg/);
|
||||
testStatic('test', '0,0,0/256x256', 'webp', 200, undefined, /image\/webp/);
|
||||
});
|
||||
|
||||
describe('different parameters', function() {
|
||||
testStatic('test', '0,0,0/300x300', 'png', 200, 2);
|
||||
testStatic('test', '0,0,0/300x300', 'png', 200, 3);
|
||||
|
||||
testStatic('test', '80,40,20/600x300', 'png', 200, 3);
|
||||
testStatic('test', '8.5,40.5,20/300x150', 'png', 200, 3);
|
||||
testStatic('test', '-8.5,-40.5,20/300x150', 'png', 200, 3);
|
||||
|
||||
testStatic('test', '8,40,2,0,0/300x150', 'png', 200);
|
||||
testStatic('test', '8,40,2,180,45/300x150', 'png', 200, 2);
|
||||
testStatic('test', '8,40,2,10/300x150', 'png', 200, 3);
|
||||
testStatic('test', '8,40,2,10.3,20.4/300x300', 'png', 200);
|
||||
testStatic('test', '0,0,2,390,120/300x300', 'png', 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid requests return 4xx', function() {
|
||||
testStatic('test', '190,0,0/256x256', 'png', 400);
|
||||
testStatic('test', '0,86,0/256x256', 'png', 400);
|
||||
testStatic('test', '80,40,20/0x0', 'png', 400);
|
||||
testStatic('test', '0,0,0/256x256', 'gif', 400);
|
||||
testStatic('test', '0,0,0/256x256', 'png', 404, 1);
|
||||
|
||||
testStatic('test', '0,0,-1/256x256', 'png', 404);
|
||||
testStatic('test', '0,0,1.5/256x256', 'png', 404);
|
||||
testStatic('test', '0,0,0/256.5x256.5', 'png', 404);
|
||||
|
||||
testStatic('test', '0,0,0,/256x256', 'png', 404);
|
||||
testStatic('test', '0,0,0,0,/256x256', 'png', 404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('area-based', function() {
|
||||
describe('valid requests', function() {
|
||||
describe('various formats', function() {
|
||||
testStatic('test', '-180,-80,180,80/0', 'png', 200, undefined, /image\/png/);
|
||||
testStatic('test', '-180,-80,180,80/0', 'jpg', 200, undefined, /image\/jpeg/);
|
||||
testStatic('test', '-180,-80,180,80/0', 'jpeg', 200, undefined, /image\/jpeg/);
|
||||
testStatic('test', '-180,-80,180,80/0', 'webp', 200, undefined, /image\/webp/);
|
||||
});
|
||||
|
||||
describe('different parameters', function() {
|
||||
testStatic('test', '-180,-90,180,90/0', 'png', 200, 2);
|
||||
testStatic('test', '0,0,1,1/3', 'png', 200, 3);
|
||||
|
||||
testStatic('test', '-280,-80,0,80/0', 'png', 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid requests return 4xx', function() {
|
||||
testStatic('test', '0,87,1,88/5', 'png', 400);
|
||||
|
||||
testStatic('test', '18,-9,-18,9/0', 'png', 400);
|
||||
testStatic('test', '0,0,1,1/1', 'gif', 400);
|
||||
|
||||
testStatic('test', '-180,-80,180,80/0.5', 'png', 404);
|
||||
});
|
||||
});
|
||||
});
|
||||
49
test/style.js
Normal file
49
test/style.js
Normal file
@@ -0,0 +1,49 @@
|
||||
var testIs = function(url, type, status) {
|
||||
it(url + ' return ' + (status || 200) + ' and is ' + type.toString(),
|
||||
function(done) {
|
||||
supertest(app)
|
||||
.get(url)
|
||||
.expect(status || 200)
|
||||
.expect('Content-Type', type, done);
|
||||
});
|
||||
};
|
||||
|
||||
describe('Styles', function() {
|
||||
describe('/styles/test.json is valid style', function() {
|
||||
testIs('/styles/test.json', /application\/json/);
|
||||
|
||||
it('contains expected properties', function(done) {
|
||||
supertest(app)
|
||||
.get('/styles/test.json')
|
||||
.expect(function(res) {
|
||||
res.body.version.should.equal(8);
|
||||
res.body.name.should.be.String();
|
||||
res.body.sources.should.be.Object();
|
||||
res.body.glyphs.should.be.String();
|
||||
res.body.sprite.should.be.String();
|
||||
res.body.layers.should.be.Array();
|
||||
}).end(done);
|
||||
});
|
||||
});
|
||||
describe('/styles/streets.json is not served', function() {
|
||||
testIs('/styles/streets.json', /./, 404);
|
||||
});
|
||||
|
||||
describe('/styles/test/sprite[@2x].{format}', function() {
|
||||
testIs('/styles/test/sprite.json', /application\/json/);
|
||||
testIs('/styles/test/sprite@2x.json', /application\/json/);
|
||||
testIs('/styles/test/sprite.png', /image\/png/);
|
||||
testIs('/styles/test/sprite@2x.png', /image\/png/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fonts', function() {
|
||||
testIs('/fonts/Open Sans Bold/0-255.pbf', /application\/x-protobuf/);
|
||||
testIs('/fonts/Open Sans Regular/65280-65533.pbf', /application\/x-protobuf/);
|
||||
testIs('/fonts/Open Sans Bold,Open Sans Regular/0-255.pbf',
|
||||
/application\/x-protobuf/);
|
||||
testIs('/fonts/Nonsense,Open Sans Bold/0-255.pbf', /application\/x-protobuf/);
|
||||
|
||||
testIs('/fonts/Nonsense/0-255.pbf', /./, 400);
|
||||
testIs('/fonts/Nonsense1,Nonsense2/0-255.pbf', /./, 400);
|
||||
});
|
||||
44
test/tiles_raster.js
Normal file
44
test/tiles_raster.js
Normal file
@@ -0,0 +1,44 @@
|
||||
var testTile = function(prefix, z, x, y, format, status, scale, type) {
|
||||
if (scale) y += '@' + scale + 'x';
|
||||
var path = '/raster/' + prefix + '/' + z + '/' + x + '/' + y + '.' + format;
|
||||
it(path + ' returns ' + status, function(done) {
|
||||
var test = supertest(app).get(path);
|
||||
test.expect(status);
|
||||
if (type) test.expect('Content-Type', type);
|
||||
test.end(done);
|
||||
});
|
||||
};
|
||||
|
||||
describe('Raster tiles', function() {
|
||||
describe('valid requests', function() {
|
||||
describe('various formats', function() {
|
||||
testTile('test', 0, 0, 0, 'png', 200, undefined, /image\/png/);
|
||||
testTile('test', 0, 0, 0, 'jpg', 200, undefined, /image\/jpeg/);
|
||||
testTile('test', 0, 0, 0, 'jpeg', 200, undefined, /image\/jpeg/);
|
||||
testTile('test', 0, 0, 0, 'webp', 200, undefined, /image\/webp/);
|
||||
});
|
||||
|
||||
describe('different coordinates and scales', function() {
|
||||
testTile('test', 1, 1, 1, 'png', 200);
|
||||
|
||||
testTile('test', 0, 0, 0, 'png', 200, 2);
|
||||
testTile('test', 0, 0, 0, 'png', 200, 3);
|
||||
testTile('test', 2, 1, 1, 'png', 200, 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid requests return 4xx', function() {
|
||||
testTile('non_existent', 0, 0, 0, 'png', 404);
|
||||
testTile('test', -1, 0, 0, 'png', 404);
|
||||
testTile('test', 25, 0, 0, 'png', 404);
|
||||
testTile('test', 0, 1, 0, 'png', 404);
|
||||
testTile('test', 0, 0, 1, 'png', 404);
|
||||
testTile('test', 0, 0, 0, 'gif', 400);
|
||||
testTile('test', 0, 0, 0, 'pbf', 400);
|
||||
|
||||
testTile('test', 0, 0, 0, 'png', 404, 1);
|
||||
testTile('test', 0, 0, 0, 'png', 404, 4);
|
||||
|
||||
testTile('hybrid', 0, 0, 0, 'png', 404);
|
||||
});
|
||||
});
|
||||
28
test/tiles_vector.js
Normal file
28
test/tiles_vector.js
Normal file
@@ -0,0 +1,28 @@
|
||||
var testTile = function(prefix, z, x, y, status) {
|
||||
var path = '/vector/' + prefix + '/' + z + '/' + x + '/' + y + '.pbf';
|
||||
it(path + ' returns ' + status, function(done) {
|
||||
var test = supertest(app).get(path);
|
||||
if (status) test.expect(status);
|
||||
if (status == 200) test.expect('Content-Type', /application\/x-protobuf/);
|
||||
test.end(done);
|
||||
});
|
||||
};
|
||||
|
||||
var prefix = 'zurich-vector';
|
||||
|
||||
describe('Vector tiles', function() {
|
||||
describe('existing tiles', function() {
|
||||
testTile(prefix, 0, 0, 0, 200);
|
||||
testTile(prefix, 14, 8581, 5738, 200);
|
||||
});
|
||||
|
||||
describe('non-existent requests return 4xx', function() {
|
||||
testTile('non_existent', 0, 0, 0, 404);
|
||||
testTile(prefix, -1, 0, 0, 404); // err zoom
|
||||
testTile(prefix, 20, 0, 0, 404); // zoom out of bounds
|
||||
testTile(prefix, 0, 1, 0, 404);
|
||||
testTile(prefix, 0, 0, 1, 404);
|
||||
|
||||
testTile(prefix, 14, 0, 0, 404); // non existent tile
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user