Compare commits

...

46 Commits

Author SHA1 Message Date
Petr Sloup
654bdda629 Update package version to 2.0.0 2017-06-23 21:45:11 +02:00
Petr Sloup
537313840e Update node version in documentation 2017-06-23 21:44:40 +02:00
Petr Sloup
d30f8464b2 Add some packages to travis and Dockerfile 2017-06-23 21:34:59 +02:00
Petr Sloup
698c527e94 Change endpoint URLs (close #154)
- styles at /style/{id}/style.json
- rendered tiles at /style/{id}/{z}/{x}/{y}.{format}
- TileJSONs at /style/{id}.json
2017-06-22 16:37:32 +02:00
Petr Sloup
8007f1386c Add required package for travis and Dockerfile 2017-06-22 16:37:32 +02:00
Petr Sloup
4f2fdf602b Update Dockerfiles to use node v6 2017-06-22 16:37:32 +02:00
Petr Sloup
6d7397647a Update travis script to use node v6 2017-06-22 16:37:32 +02:00
Petr Sloup
8fd7a9b42b Update dependencies to get ready for node v6 2017-06-22 16:37:32 +02:00
Petr Sloup
95470143b6 Merge pull request #179 from tschaub/err
Log errors to stderr and return
2017-06-20 17:29:39 +02:00
Tim Schaub
de83021c3d Log errors to stderr and return 2017-06-20 08:09:59 -07:00
Petr Sloup
7de3d8b9c7 Update package version to 1.7.0 2017-05-10 10:32:44 +02:00
Petr Sloup
bdc3d20524 Use promises instead of async in font concatenation (remove async dependency) 2017-05-10 10:22:39 +02:00
Petr Sloup
5d93b1d4f9 Add healthcheck endpoint (close #140) 2017-05-10 08:57:51 +02:00
Petr Sloup
d30027e992 Modify all serve_* modules to return a Promise (preparation for #140) 2017-05-10 08:56:43 +02:00
Petr Sloup
c233d23523 Fix broken static maps bounds overlay (for very large areas) 2017-05-05 16:15:10 +02:00
Petr Sloup
1109c77ec2 Update package version to 1.6.0 2017-05-04 12:07:58 +02:00
Petr Sloup
88cf9b37a9 Revert mapbox-gl-native package update
It broke travis (to be further investigated)
2017-05-04 11:21:57 +02:00
Petr Sloup
2d207f792b Update package dependencies 2017-05-04 11:13:24 +02:00
Petr Sloup
d67a57861d Merge pull request #155 from tschaub/reconfigure-on-sighup
Reload configuration on SIGHUP
2017-05-04 11:02:48 +02:00
Petr Sloup
27e9dbfb4e Merge pull request #157 from tschaub/address
Enclose literal IPv6 addresses in brackets
2017-05-04 08:57:17 +02:00
Tim Schaub
a199008fa3 Enclose literal IPv6 addresses in brackets 2017-04-28 07:28:49 -07:00
Tim Schaub
e88b786073 Reload configuration on SIGHUP 2017-04-28 07:13:34 -07:00
Petr Sloup
c03b0a12f8 Merge pull request #156 from tschaub/no-callback
Remove unused callback
2017-04-28 11:04:17 +02:00
Tim Schaub
a234041cd1 Remove unused callback 2017-04-26 08:02:18 -07:00
Petr Sloup
49a779970e Merge pull request #150 from pirxpilot/no-cors
add `--cors` option to allow for optional CORS handling
2017-04-18 09:32:11 +02:00
Petr Sloup
9545c2594e Handle scale in query-based static endpoint 2017-04-14 12:05:03 +02:00
Damian Krzeminski
6c23d95feb add --cors option to allow for optional CORS handling
we are using `cors` middleware with default options which works for most
applications, but does not allow for fine tuning (whitelisting origins
etc.)

this change keeps CORS handling as default to preserve compatibility but
also allows for specying `--no-cors` option which makes it possible to
handle CORS in an independent proxy (NGINX, another node app etc.)
2017-04-12 09:43:43 -07:00
Petr Sloup
366380395e Proper error message when metadata are missing in the mbtiles (close #147) 2017-04-11 19:10:40 +02:00
Petr Sloup
8ea665297f Minor fix in style path handling (allow absolute paths) 2017-04-07 18:53:11 +02:00
Petr Sloup
f6580c0342 Improved request logging 2017-04-04 18:46:18 +02:00
Petr Sloup
34a139040c Slight docker performance optimization 2017-04-04 18:46:18 +02:00
Petr Sloup
66bea8a42b Merge pull request #142 from somthanat/master
Word correction (Forwaded -> Forwarded)
2017-04-03 15:16:40 +02:00
Petr Sloup
28790fda30 Alternative query-based static endpoint 2017-04-03 15:14:40 +02:00
Somthanat Wongsa
0b16af0084 Word correction (Forwaded -> Forwarded) 2017-03-30 20:16:03 +07:00
Petr Sloup
e1654a51de Update package version to 1.5.0 2017-03-29 15:46:30 +02:00
Petr Sloup
5372bab6c5 Update dependencies 2017-03-29 15:45:32 +02:00
Petr Sloup
6a960e8593 Remove optional dependencies from package.json to avoid warnings 2017-03-17 10:42:25 +01:00
Petr Sloup
1577cfa54a Update dependencies 2017-03-15 17:20:55 +01:00
Petr Sloup
640038a115 Add support for watermarks (close #130) 2017-03-15 17:06:26 +01:00
Petr Sloup
49a8562441 Support for proj4 string in mbtiles metadata (for static maps) (close #127) 2017-03-15 16:45:26 +01:00
Petr Sloup
1e402ed207 Add possibility to change the front page (close #128) 2017-03-15 15:50:58 +01:00
Petr Sloup
f8949c1aa9 Configurable scale factors (close #121)
Also changes default maximum from `4x` to `3x`
2017-03-15 12:09:18 +01:00
Petr Sloup
7b952ee5c0 Do not add style parameter when not needed (close #134) 2017-03-15 11:39:21 +01:00
Petr Sloup
0673c8990a Add option to disable static maps (close #129) 2017-03-14 16:12:19 +01:00
Petr Sloup
37386bfb29 Hide empty headers on index (close #125) 2017-03-14 16:02:54 +01:00
Petr Sloup
b93bc5fadc Support for handling relative subdomain patterns 2017-03-14 15:50:14 +01:00
21 changed files with 633 additions and 367 deletions

View File

@@ -1,6 +1,6 @@
language: node_js language: node_js
node_js: node_js:
- "4" - "6"
env: env:
- CXX=g++-4.8 - CXX=g++-4.8
addons: addons:
@@ -12,7 +12,7 @@ addons:
before_install: before_install:
- sudo apt-get update -qq - sudo apt-get update -qq
- sudo apt-get install -qq libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev build-essential g++ - sudo apt-get install -qq libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev build-essential g++
- sudo apt-get install -qq xvfb - sudo apt-get install -qq xvfb libgles2-mesa-dev libgbm-dev libxxf86vm-dev
install: install:
- npm install - npm install
- wget -O test_data.zip https://github.com/klokantech/tileserver-gl/releases/download/v1.3.0/test_data.zip - wget -O test_data.zip https://github.com/klokantech/tileserver-gl/releases/download/v1.3.0/test_data.zip

View File

@@ -9,10 +9,14 @@ RUN apt-get -qq update \
build-essential \ build-essential \
python \ python \
libcairo2-dev \ libcairo2-dev \
libgles2-mesa-dev \
libgbm-dev \
libllvm3.9 \
libprotobuf-dev \ libprotobuf-dev \
libxxf86vm-dev \
xvfb \ xvfb \
&& echo "deb https://deb.nodesource.com/node_4.x jessie main" >> /etc/apt/sources.list.d/nodejs.list \ && echo "deb https://deb.nodesource.com/node_6.x jessie main" >> /etc/apt/sources.list.d/nodejs.list \
&& echo "deb-src https://deb.nodesource.com/node_4.x jessie main" >> /etc/apt/sources.list.d/nodejs.list \ && echo "deb-src https://deb.nodesource.com/node_6.x jessie main" >> /etc/apt/sources.list.d/nodejs.list \
&& apt-get -qq update \ && apt-get -qq update \
&& DEBIAN_FRONTEND=noninteractive apt-get -y --allow-unauthenticated install \ && DEBIAN_FRONTEND=noninteractive apt-get -y --allow-unauthenticated install \
nodejs \ nodejs \
@@ -26,5 +30,7 @@ RUN cd /usr/src/app && npm install --production
VOLUME /data VOLUME /data
WORKDIR /data WORKDIR /data
ENV NODE_ENV="production"
EXPOSE 80 EXPOSE 80
ENTRYPOINT ["/usr/src/app/run.sh"] ENTRYPOINT ["/usr/src/app/run.sh"]

View File

@@ -1,4 +1,4 @@
FROM node:4 FROM node:6
MAINTAINER Petr Sloup <petr.sloup@klokantech.com> MAINTAINER Petr Sloup <petr.sloup@klokantech.com>
RUN mkdir -p /usr/src/app RUN mkdir -p /usr/src/app
@@ -8,5 +8,7 @@ RUN cd /usr/src/app && npm install --production
VOLUME /data VOLUME /data
WORKDIR /data WORKDIR /data
ENV NODE_ENV="production"
EXPOSE 80 EXPOSE 80
ENTRYPOINT ["node", "/usr/src/app/", "-p", "80"] ENTRYPOINT ["node", "/usr/src/app/", "-p", "80"]

View File

@@ -25,9 +25,11 @@ Example::
"pngQuantization": false, "pngQuantization": false,
"png": 90 "png": 90
}, },
"maxScaleFactor": 3,
"maxSize": 2048, "maxSize": 2048,
"pbfAlias": "pbf", "pbfAlias": "pbf",
"serveAllFonts": false "serveAllFonts": false,
"serveStaticMaps": true
}, },
"styles": { "styles": {
"basic": { "basic": {
@@ -68,6 +70,14 @@ The value of ``root`` is used as prefix for all data types.
You can use this to optionally specify on what domains the rendered tiles are accessible. This can be used for basic load-balancing or to bypass browser's limit for the number of connections per domain. You can use this to optionally specify on what domains the rendered tiles are accessible. This can be used for basic load-balancing or to bypass browser's limit for the number of connections per domain.
``frontPage``
-----------------
Path to the html (relative to ``root`` path) to use as a front page.
Use ``true`` (or nothing) to serve the default TileServer GL front page with list of styles and data.
Use ``false`` to disable the front page altogether (404).
``formatQuality`` ``formatQuality``
----------------- -----------------
@@ -75,10 +85,26 @@ Quality of the compression of individual image formats. [0-100]
The value for ``png`` is only used when ``pngQuantization`` is ``true``. The value for ``png`` is only used when ``pngQuantization`` is ``true``.
``maxScaleFactor``
-----------
Maximum scale factor to allow in raster tile and static maps requests (e.g. ``@3x`` suffix).
Also see ``maxSize`` below.
Default value is ``3``, maximum ``9``.
``maxSize`` ``maxSize``
----------- -----------
Maximum image side length to be allowed to be rendered (including scale factor). Default is ``2048``. Maximum image side length to be allowed to be rendered (including scale factor).
Be careful when changing this value since there are hardware limits that need to be considered.
Default is ``2048``.
``watermark``
-----------
Optional string to be rendered into the raster tiles (and static maps) as watermark (bottom-left corner).
Can be used for hard-coding attributions etc. (can also be specified per-style).
Not used by default.
``styles`` ``styles``
========== ==========

View File

@@ -17,4 +17,4 @@ Nginx can be used to add protection via https, password, referrer, IP address re
Running behind a proxy or a load-balancer Running behind a proxy or a load-balancer
========================================= =========================================
If you need to run TileServer GL behind a proxy, make sure the proxy sends ``X-Forwarded-*`` headers to the server (most importantly ``X-Forwarded-Host`` and ``X-Forwaded-Proto``) to ensures the URLs generated inside TileJSON etc. are using the desired domain and protocol. If you need to run TileServer GL behind a proxy, make sure the proxy sends ``X-Forwarded-*`` headers to the server (most importantly ``X-Forwarded-Host`` and ``X-Forwarded-Proto``) to ensures the URLs generated inside TileJSON etc. are using the desired domain and protocol.

View File

@@ -6,18 +6,18 @@ If you visit the server on the configured port (default 8080) you can see your m
Styles Styles
====== ======
* Styles are served at ``/styles/{id}.json`` (+ array at ``/styles.json``) * Styles are served at ``/styles/{id}/style.json`` (+ array at ``/styles.json``)
* Sprites at ``/styles/{id}/sprite[@2x].{format}`` * Sprites at ``/styles/{id}/sprite[@2x].{format}``
* Fonts at ``/fonts/{fontstack}/{start}-{end}.pbf`` * Fonts at ``/fonts/{fontstack}/{start}-{end}.pbf``
Rendered tiles Rendered tiles
============== ==============
* Rendered tiles are served at ``/styles/{id}/rendered/{z}/{x}/{y}[@2x].{format}`` * Rendered tiles are served at ``/styles/{id}/{z}/{x}/{y}[@2x].{format}``
* The optional ``@2x`` (or ``@3x``, ``@4x``) part can be used to render HiDPI (retina) tiles * The optional ``@2x`` (or ``@3x``, ``@4x``) part can be used to render HiDPI (retina) tiles
* Available formats: ``png``, ``jpg`` (``jpeg``), ``webp`` * Available formats: ``png``, ``jpg`` (``jpeg``), ``webp``
* TileJSON at ``/styles/{id}/rendered.json`` * TileJSON at ``/styles/{id}.json``
* The rendered tiles are not available in the ``tileserver-gl-light`` version. * The rendered tiles are not available in the ``tileserver-gl-light`` version.
@@ -64,3 +64,10 @@ Array of all TileJSONs is at ``/index.json`` (``/rendered.json``; ``/data.json``
List of available fonts List of available fonts
======================= =======================
Array of names of the available fonts is at ``/fonts.json`` Array of names of the available fonts is at ``/fonts.json``
Health check
============
Endpoint reporting health status is at ``/health`` and currently returns:
* ``503`` Starting - for a short period before everything is initialized
* ``200`` OK - when the server is running

View File

@@ -41,7 +41,7 @@ Alternatively, you can use ``tileserver-gl-light`` package instead, which is pur
From source From source
=========== ===========
Make sure you have Node v4 (nvm install 4) and run:: Make sure you have Node v6 (nvm install 6) and run::
npm install npm install
node . node .

View File

@@ -1,6 +1,6 @@
{ {
"name": "tileserver-gl", "name": "tileserver-gl",
"version": "1.4.1", "version": "2.0.0",
"description": "Map tile server for JSON GL styles - vector and server side generated raster tiles", "description": "Map tile server for JSON GL styles - vector and server side generated raster tiles",
"main": "src/main.js", "main": "src/main.js",
"bin": "src/main.js", "bin": "src/main.js",
@@ -13,36 +13,34 @@
}, },
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=4.2.1 <5" "node": ">=6 <7"
}, },
"scripts": { "scripts": {
"test": "mocha test/**.js" "test": "mocha test/**.js"
}, },
"dependencies": { "dependencies": {
"async": "2.1.4", "@mapbox/mapbox-gl-native": "3.5.4",
"@mapbox/mbtiles": "0.9.0",
"@mapbox/sphericalmercator": "1.0.5",
"@mapbox/vector-tile": "1.3.0",
"advanced-pool": "0.3.2", "advanced-pool": "0.3.2",
"base64url": "2.0.0", "base64url": "2.0.0",
"canvas": "1.6.2", "canvas": "1.6.5",
"clone": "2.1.0", "clone": "2.1.1",
"color": "1.0.3", "color": "1.0.3",
"cors": "2.8.1", "cors": "2.8.3",
"express": "4.14.1", "express": "4.15.3",
"glyph-pbf-composite": "0.0.2", "glyph-pbf-composite": "0.0.2",
"handlebars": "4.0.6", "handlebars": "4.0.10",
"mbtiles": "0.9.0", "http-shutdown": "^1.2.0",
"morgan": "1.7.0", "morgan": "1.8.2",
"node-pngquant-native": "1.0.4", "node-pngquant-native": "1.0.4",
"nomnom": "1.8.1", "nomnom": "1.8.1",
"pbf": "3.0.5", "pbf": "3.0.5",
"request": "2.79.0", "proj4": "2.4.3",
"sharp": "0.17.1", "request": "2.81.0",
"tileserver-gl-styles": "1.1.0", "sharp": "0.18.1",
"vector-tile": "1.3.0", "tileserver-gl-styles": "1.1.1"
"@mapbox/mapbox-gl-native": "3.4.4",
"@mapbox/sphericalmercator": "1.0.5"
},
"optionalDependencies": {
"tileshrink-gl": "./plugins/tileshrink-gl"
}, },
"devDependencies": { "devDependencies": {
"should": "^11.2.0", "should": "^11.2.0",

View File

@@ -19,85 +19,89 @@
<section> <section>
<h1 class="title {{#if is_light}}light{{/if}}"><img src="/images/logo.png" alt="TileServer GL" /></h1> <h1 class="title {{#if is_light}}light{{/if}}"><img src="/images/logo.png" alt="TileServer GL" /></h1>
<h2 class="subtitle">Vector {{#if is_light}}<s>and raster</s>{{else}}and raster{{/if}} maps with GL styles</h2> <h2 class="subtitle">Vector {{#if is_light}}<s>and raster</s>{{else}}and raster{{/if}} maps with GL styles</h2>
<h2 class="box-header">Styles</h2> {{#if styles}}
<div class="box"> <h2 class="box-header">Styles</h2>
{{#each styles}} <div class="box">
<div class="item"> {{#each styles}}
{{#if thumbnail}} <div class="item">
<img src="/styles/{{@key}}/rendered/{{thumbnail}}{{&../key_query}}" alt="{{name}} preview" /> {{#if thumbnail}}
{{else}} <img src="/styles/{{@key}}/{{thumbnail}}{{&../key_query}}" alt="{{name}} preview" />
<img src="/images/placeholder.png" alt="{{name}} preview" /> {{else}}
{{/if}} <img src="/images/placeholder.png" alt="{{name}} preview" />
<div class="details"> {{/if}}
<h3>{{name}}</h3> <div class="details">
<p class="identifier">identifier: {{@key}}</p> <h3>{{name}}</h3>
<p class="services"> <p class="identifier">identifier: {{@key}}</p>
services: <p class="services">
services:
{{#if serving_data}}
<a href="/styles/{{@key}}/style.json{{&../key_query}}">GL Style</a>
{{/if}}
{{#if serving_rendered}}
{{#if serving_data}}| {{/if}}<a href="/styles/{{@key}}.json{{&../key_query}}">TileJSON</a>
{{/if}}
{{#if wmts_link}}
| <a href="{{&wmts_link}}">WMTS</a>
{{/if}}
{{#if xyz_link}}
| <a href="#" onclick="return toggle_xyz('xyz_style_{{@key}}');">XYZ</a>
<input id="xyz_style_{{@key}}" type="text" value="{{&xyz_link}}" style="display:none;" />
{{/if}}
</p>
</div>
<div class="viewers">
{{#if serving_data}} {{#if serving_data}}
<a href="/styles/{{@key}}.json{{&../key_query}}">GL Style</a> {{#if serving_rendered}}
<a class="btn" href="/styles/{{@key}}/{{&../key_query}}{{viewer_hash}}">Viewer</a>
{{/if}}
{{/if}} {{/if}}
{{#if serving_rendered}} {{#if serving_rendered}}
{{#if serving_data}}| {{/if}}<a href="/styles/{{@key}}/rendered.json{{&../key_query}}">TileJSON</a> <a class="btn" href="/styles/{{@key}}/?{{&../key_query_part}}raster{{viewer_hash}}">Raster</a>
{{/if}} {{/if}}
{{#if wmts_link}} {{#if serving_data}}
| <a href="{{&wmts_link}}">WMTS</a> <a class="btn" href="/styles/{{@key}}/?{{&../key_query_part}}vector{{viewer_hash}}">Vector</a>
{{/if}} {{/if}}
{{#if xyz_link}} </div>
| <a href="#" onclick="return toggle_xyz('xyz_style_{{@key}}');">XYZ</a>
<input id="xyz_style_{{@key}}" type="text" value="{{&xyz_link}}" style="display:none;" />
{{/if}}
</p>
</div>
<div class="viewers">
{{#if serving_data}}
{{#if serving_rendered}}
<a class="btn" href="/styles/{{@key}}/{{&../key_query}}{{viewer_hash}}">Viewer</a>
{{/if}}
{{/if}}
{{#if serving_rendered}}
<a class="btn" href="/styles/{{@key}}/?{{&../key_query_part}}raster{{viewer_hash}}">Raster</a>
{{/if}}
{{#if serving_data}}
<a class="btn" href="/styles/{{@key}}/?{{&../key_query_part}}vector{{viewer_hash}}">Vector</a>
{{/if}}
</div> </div>
{{/each}}
</div> </div>
{{/each}} {{/if}}
</div> {{#if data}}
<h2 class="box-header">Data</h2> <h2 class="box-header">Data</h2>
<div class="box"> <div class="box">
{{#each data}} {{#each data}}
<div class="item"> <div class="item">
{{#if thumbnail}} {{#if thumbnail}}
<img src="/data/{{@key}}/{{thumbnail}}{{&../key_query}}" alt="{{name}} preview" /> <img src="/data/{{@key}}/{{thumbnail}}{{&../key_query}}" alt="{{name}} preview" />
{{else}} {{else}}
<img src="/images/placeholder.png" alt="{{name}} preview" /> <img src="/images/placeholder.png" alt="{{name}} preview" />
{{/if}} {{/if}}
<div class="details"> <div class="details">
<h3>{{name}}</h3> <h3>{{name}}</h3>
<p class="identifier">identifier: {{@key}}{{#if formatted_filesize}} | size: {{formatted_filesize}}{{/if}} | type: {{#is_vector}}vector{{/is_vector}}{{^is_vector}}raster{{/is_vector}} data</p> <p class="identifier">identifier: {{@key}}{{#if formatted_filesize}} | size: {{formatted_filesize}}{{/if}} | type: {{#is_vector}}vector{{/is_vector}}{{^is_vector}}raster{{/is_vector}} data</p>
<p class="services"> <p class="services">
services: <a href="/data/{{@key}}.json{{&../key_query}}">TileJSON</a> services: <a href="/data/{{@key}}.json{{&../key_query}}">TileJSON</a>
{{#if wmts_link}} {{#if wmts_link}}
| <a href="{{&wmts_link}}">WMTS</a> | <a href="{{&wmts_link}}">WMTS</a>
{{/if}} {{/if}}
{{#if xyz_link}} {{#if xyz_link}}
| <a href="#" onclick="return toggle_xyz('xyz_data_{{@key}}');">XYZ</a> | <a href="#" onclick="return toggle_xyz('xyz_data_{{@key}}');">XYZ</a>
<input id="xyz_data_{{@key}}" type="text" value="{{&xyz_link}}" style="display:none;" /> <input id="xyz_data_{{@key}}" type="text" value="{{&xyz_link}}" style="display:none;" />
{{/if}} {{/if}}
</p> </p>
</div> </div>
<div class="viewers"> <div class="viewers">
{{#is_vector}} {{#is_vector}}
<a class="btn" href="/data/{{@key}}/{{&../key_query}}{{viewer_hash}}">Inspect</a> <a class="btn" href="/data/{{@key}}/{{&../key_query}}{{viewer_hash}}">Inspect</a>
{{/is_vector}} {{/is_vector}}
{{^is_vector}} {{^is_vector}}
<a class="btn" href="/data/{{@key}}/{{&../key_query}}{{viewer_hash}}">View</a> <a class="btn" href="/data/{{@key}}/{{&../key_query}}{{viewer_hash}}">View</a>
{{/is_vector}} {{/is_vector}}
</div>
</div> </div>
{{/each}}
</div> </div>
{{/each}} {{/if}}
</div>
</section> </section>
<footer> <footer>
<a href="https://www.klokantech.com/" target="_blank"><img src="/images/klokantech.png" /></a> <a href="https://www.klokantech.com/" target="_blank"><img src="/images/klokantech.png" /></a>

View File

@@ -27,12 +27,12 @@
mapboxgl.setRTLTextPlugin('/mapbox-gl-rtl-text.js{{&key_query}}'); mapboxgl.setRTLTextPlugin('/mapbox-gl-rtl-text.js{{&key_query}}');
var map = new mapboxgl.Map({ var map = new mapboxgl.Map({
container: 'map', container: 'map',
style: '/styles/{{id}}.json{{&key_query}}', style: '/styles/{{id}}/style.json{{&key_query}}',
hash: true hash: true
}); });
map.addControl(new mapboxgl.NavigationControl()); map.addControl(new mapboxgl.NavigationControl());
} else { } else {
var map = L.mapbox.map('map', '/styles/{{id}}/rendered.json{{&key_query}}', { zoomControl: false }); var map = L.mapbox.map('map', '/styles/{{id}}.json{{&key_query}}', { zoomControl: false });
new L.Control.Zoom({ position: 'topright' }).addTo(map); new L.Control.Zoom({ position: 'topright' }).addTo(map);
setTimeout(function() { setTimeout(function() {
new L.Hash(map); new L.Hash(map);

View File

@@ -6,7 +6,7 @@ var fs = require('fs'),
path = require('path'), path = require('path'),
request = require('request'); request = require('request');
var mbtiles = require('mbtiles'); var mbtiles = require('@mapbox/mbtiles');
var packageJson = require('../package'); var packageJson = require('../package');
@@ -32,6 +32,10 @@ var opts = require('nomnom')
default: 8080, default: 8080,
help: 'Port' help: 'Port'
}) })
.option('cors', {
default: true,
help: 'Enable Cross-origin resource sharing headers'
})
.option('verbose', { .option('verbose', {
abbr: 'V', abbr: 'V',
flag: true, flag: true,
@@ -54,7 +58,8 @@ var startServer = function(configPath, config) {
configPath: configPath, configPath: configPath,
config: config, config: config,
bind: opts.bind, bind: opts.bind,
port: opts.port port: opts.port,
cors: opts.cors
}); });
}; };
@@ -70,6 +75,12 @@ var startWithMBTiles = function(mbtilesFile) {
} }
var instance = new mbtiles(mbtilesFile, function(err) { var instance = new mbtiles(mbtilesFile, function(err) {
instance.getInfo(function(err, info) { instance.getInfo(function(err, info) {
if (err || !info) {
console.log('ERROR: Metadata missing in the MBTiles.');
console.log(' Make sure ' + path.basename(mbtilesFile) +
' is valid MBTiles.');
process.exit(1);
}
var bounds = info.bounds; var bounds = info.bounds;
var styleDir = path.resolve(__dirname, "../node_modules/tileserver-gl-styles/"); var styleDir = path.resolve(__dirname, "../node_modules/tileserver-gl-styles/");

View File

@@ -6,13 +6,14 @@ var fs = require('fs'),
var clone = require('clone'), var clone = require('clone'),
express = require('express'), express = require('express'),
mbtiles = require('mbtiles'), mbtiles = require('@mapbox/mbtiles'),
pbf = require('pbf'), pbf = require('pbf'),
VectorTile = require('vector-tile').VectorTile; VectorTile = require('@mapbox/vector-tile').VectorTile;
var tileshrinkGl; var tileshrinkGl;
try { try {
tileshrinkGl = require('tileshrink-gl'); tileshrinkGl = require('tileshrink-gl');
global.addStyleParam = true;
} catch (e) {} } catch (e) {}
var utils = require('./utils'); var utils = require('./utils');
@@ -33,20 +34,24 @@ module.exports = function(options, repo, params, id, styles) {
if (!mbtilesFileStats.isFile() || mbtilesFileStats.size == 0) { if (!mbtilesFileStats.isFile() || mbtilesFileStats.size == 0) {
throw Error('Not valid MBTiles file: ' + mbtilesFile); throw Error('Not valid MBTiles file: ' + mbtilesFile);
} }
var source = new mbtiles(mbtilesFile, function(err) { var source;
source.getInfo(function(err, info) { var sourceInfoPromise = new Promise(function(resolve, reject) {
tileJSON['name'] = id; source = new mbtiles(mbtilesFile, function(err) {
tileJSON['format'] = 'pbf'; source.getInfo(function(err, info) {
tileJSON['name'] = id;
tileJSON['format'] = 'pbf';
Object.assign(tileJSON, info); Object.assign(tileJSON, info);
tileJSON['tilejson'] = '2.0.0'; tileJSON['tilejson'] = '2.0.0';
delete tileJSON['filesize']; delete tileJSON['filesize'];
delete tileJSON['mtime']; delete tileJSON['mtime'];
delete tileJSON['scheme']; delete tileJSON['scheme'];
Object.assign(tileJSON, params.tilejson || {}); Object.assign(tileJSON, params.tilejson || {});
utils.fixTileJSONCenter(tileJSON); utils.fixTileJSONCenter(tileJSON);
resolve();
});
}); });
}); });
@@ -160,5 +165,9 @@ module.exports = function(options, repo, params, id, styles) {
return res.send(info); return res.send(info);
}); });
return app; return new Promise(function(resolve, reject) {
sourceInfoPromise.then(function() {
resolve(app);
});
});
}; };

View File

@@ -15,16 +15,19 @@ module.exports = function(options, allowedFonts) {
var fontPath = options.paths.fonts; var fontPath = options.paths.fonts;
var existingFonts = {}; var existingFonts = {};
fs.readdir(options.paths.fonts, function(err, files) { var fontListingPromise = new Promise(function(resolve, reject) {
files.forEach(function(file) { fs.readdir(options.paths.fonts, function(err, files) {
fs.stat(path.join(fontPath, file), function(err, stats) { files.forEach(function(file) {
if (!err) { fs.stat(path.join(fontPath, file), function(err, stats) {
if (stats.isDirectory() && if (!err) {
fs.existsSync(path.join(fontPath, file, '0-255.pbf'))) { if (stats.isDirectory() &&
existingFonts[path.basename(file)] = true; fs.existsSync(path.join(fontPath, file, '0-255.pbf'))) {
existingFonts[path.basename(file)] = true;
}
} }
} });
}); });
resolve();
}); });
}); });
@@ -33,19 +36,15 @@ module.exports = function(options, allowedFonts) {
var fontstack = decodeURI(req.params.fontstack); var fontstack = decodeURI(req.params.fontstack);
var range = req.params.range; var range = req.params.range;
return utils.getFontsPbf(options.serveAllFonts ? null : allowedFonts, utils.getFontsPbf(options.serveAllFonts ? null : allowedFonts,
fontPath, fontstack, range, existingFonts, fontPath, fontstack, range, existingFonts).then(function(concated) {
function(err, concated) {
if (err || concated.length === 0) {
console.log(err);
console.log(concated.length);
return res.status(400).send('');
} else {
res.header('Content-type', 'application/x-protobuf'); res.header('Content-type', 'application/x-protobuf');
res.header('Last-Modified', lastModified); res.header('Last-Modified', lastModified);
return res.send(concated); return res.send(concated);
}, function(err) {
return res.status(400).send(err);
} }
}); );
}); });
app.get('/fonts.json', function(req, res, next) { app.get('/fonts.json', function(req, res, next) {
@@ -55,5 +54,9 @@ module.exports = function(options, allowedFonts) {
); );
}); });
return app; return new Promise(function(resolve, reject) {
fontListingPromise.then(function() {
resolve(app);
});
});
}; };

View File

@@ -1,7 +1,6 @@
'use strict'; 'use strict';
var async = require('async'), var advancedPool = require('advanced-pool'),
advancedPool = require('advanced-pool'),
fs = require('fs'), fs = require('fs'),
path = require('path'), path = require('path'),
util = require('util'), util = require('util'),
@@ -17,14 +16,14 @@ var Canvas = require('canvas'),
express = require('express'), express = require('express'),
mercator = new (require('@mapbox/sphericalmercator'))(), mercator = new (require('@mapbox/sphericalmercator'))(),
mbgl = require('@mapbox/mapbox-gl-native'), mbgl = require('@mapbox/mapbox-gl-native'),
mbtiles = require('mbtiles'), mbtiles = require('@mapbox/mbtiles'),
pngquant = require('node-pngquant-native'), pngquant = require('node-pngquant-native'),
proj4 = require('proj4'),
request = require('request'); request = require('request');
var utils = require('./utils'); var utils = require('./utils');
var FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+\.?\\d+)'; var FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+\.?\\d+)';
var SCALE_PATTERN = '@[234]x';
var getScale = function(scale) { var getScale = function(scale) {
return (scale || '@1x').slice(1, 2) | 0; return (scale || '@1x').slice(1, 2) | 0;
@@ -39,10 +38,19 @@ mbgl.on('message', function(e) {
module.exports = function(options, repo, params, id, dataResolver) { module.exports = function(options, repo, params, id, dataResolver) {
var app = express().disable('x-powered-by'); var app = express().disable('x-powered-by');
var maxScaleFactor = Math.min(Math.floor(options.maxScaleFactor || 3), 9);
var scalePattern = '';
for (var i = 2; i <= maxScaleFactor; i++) {
scalePattern += i.toFixed();
}
scalePattern = '@[' + scalePattern + ']x';
var lastModified = new Date().toUTCString(); var lastModified = new Date().toUTCString();
var rootPath = options.paths.root; var rootPath = options.paths.root;
var watermark = params.watermark || options.watermark;
var styleFile = params.style; var styleFile = params.style;
var map = { var map = {
renderers: [], renderers: [],
@@ -50,15 +58,18 @@ module.exports = function(options, repo, params, id, dataResolver) {
}; };
var existingFonts = {}; var existingFonts = {};
fs.readdir(options.paths.fonts, function(err, files) { var fontListingPromise = new Promise(function(resolve, reject) {
files.forEach(function(file) { fs.readdir(options.paths.fonts, function(err, files) {
fs.stat(path.join(options.paths.fonts, file), function(err, stats) { files.forEach(function(file) {
if (!err) { fs.stat(path.join(options.paths.fonts, file), function(err, stats) {
if (stats.isDirectory()) { if (!err) {
existingFonts[path.basename(file)] = true; if (stats.isDirectory()) {
existingFonts[path.basename(file)] = true;
}
} }
} });
}); });
resolve();
}); });
}); });
@@ -80,9 +91,12 @@ module.exports = function(options, repo, params, id, dataResolver) {
var parts = req.url.split('/'); var parts = req.url.split('/');
var fontstack = unescape(parts[2]); var fontstack = unescape(parts[2]);
var range = parts[3].split('.')[0]; var range = parts[3].split('.')[0];
utils.getFontsPbf(null, options.paths[protocol], fontstack, range, existingFonts, utils.getFontsPbf(
function(err, concated) { null, options.paths[protocol], fontstack, range, existingFonts
callback(err, {data: concated}); ).then(function(concated) {
callback(null, {data: concated});
}, function(err) {
callback(err, {data: null});
}); });
} else if (protocol == 'mbtiles') { } else if (protocol == 'mbtiles') {
var parts = req.url.split('/'); var parts = req.url.split('/');
@@ -157,7 +171,7 @@ module.exports = function(options, repo, params, id, dataResolver) {
}); });
}; };
var styleJSONPath = path.join(options.paths.styles, styleFile); var styleJSONPath = path.resolve(options.paths.styles, styleFile);
styleJSON = clone(require(styleJSONPath)); styleJSON = clone(require(styleJSONPath));
var httpTester = /^(http(s)?:)?\/\//; var httpTester = /^(http(s)?:)?\/\//;
@@ -186,6 +200,8 @@ module.exports = function(options, repo, params, id, dataResolver) {
tileJSON.tiles = params.domains || options.domains; tileJSON.tiles = params.domains || options.domains;
utils.fixTileJSONCenter(tileJSON); utils.fixTileJSONCenter(tileJSON);
var dataProjWGStoInternalWGS = null;
var queue = []; var queue = [];
Object.keys(styleJSON.sources).forEach(function(name) { Object.keys(styleJSON.sources).forEach(function(name) {
var source = styleJSON.sources[name]; var source = styleJSON.sources[name];
@@ -207,12 +223,12 @@ module.exports = function(options, repo, params, id, dataResolver) {
} }
mbtilesFile = dataResolver(mbtilesFile); mbtilesFile = dataResolver(mbtilesFile);
if (!mbtilesFile) { if (!mbtilesFile) {
console.log('ERROR: data "' + mbtilesFile + '" not found!'); console.error('ERROR: data "' + mbtilesFile + '" not found!');
process.exit(1); process.exit(1);
} }
} }
queue.push(function(callback) { queue.push(new Promise(function(resolve, reject) {
mbtilesFile = path.resolve(options.paths.mbtiles, mbtilesFile); mbtilesFile = path.resolve(options.paths.mbtiles, mbtilesFile);
var mbtilesFileStats = fs.statSync(mbtilesFile); var mbtilesFileStats = fs.statSync(mbtilesFile);
if (!mbtilesFileStats.isFile() || mbtilesFileStats.size == 0) { if (!mbtilesFileStats.isFile() || mbtilesFileStats.size == 0) {
@@ -222,7 +238,18 @@ module.exports = function(options, repo, params, id, dataResolver) {
map.sources[name].getInfo(function(err, info) { map.sources[name].getInfo(function(err, info) {
if (err) { if (err) {
console.error(err); console.error(err);
return;
} }
if (!dataProjWGStoInternalWGS && info.proj4) {
// how to do this for multiple sources with different proj4 defs?
var to3857 = proj4('EPSG:3857');
var toDataProj = proj4(info.proj4);
dataProjWGStoInternalWGS = function(xy) {
return to3857.inverse(toDataProj.forward(xy));
};
}
var type = source.type; var type = source.type;
Object.assign(source, info); Object.assign(source, info);
source.type = type; source.type = type;
@@ -258,34 +285,44 @@ module.exports = function(options, repo, params, id, dataResolver) {
} }
tileJSON.attribution += source.attribution; tileJSON.attribution += source.attribution;
} }
callback(null); resolve();
}); });
}); });
}); }));
} }
}); });
async.parallel(queue, function(err, results) { var renderersReadyPromise = Promise.all(queue).then(function() {
// TODO: make pool sizes configurable // TODO: make pool sizes configurable
map.renderers[1] = createPool(1, 4, 16); for (var s = 1; s <= maxScaleFactor; s++) {
map.renderers[2] = createPool(2, 2, 8); var minPoolSize = 2;
map.renderers[3] = createPool(3, 2, 4);
map.renderers[4] = createPool(4, 2, 4); // standard and @2x tiles are much more usual -> create larger pools
if (s <= 2) {
minPoolSize *= 2;
if (s <= 1) {
minPoolSize *= 2;
}
}
map.renderers[s] = createPool(s, minPoolSize, 2 * minPoolSize);
}
}); });
repo[id] = tileJSON; repo[id] = tileJSON;
var tilePattern = '/rendered/:z(\\d+)/:x(\\d+)/:y(\\d+)' + var tilePattern = '/' + id + '/:z(\\d+)/:x(\\d+)/:y(\\d+)' +
':scale(' + SCALE_PATTERN + ')?\.:format([\\w]+)'; ':scale(' + scalePattern + ')?\.:format([\\w]+)';
var respondImage = function(z, lon, lat, bearing, pitch, var respondImage = function(z, lon, lat, bearing, pitch,
width, height, scale, format, res, next, width, height, scale, format, res, next,
opt_overlay) { opt_overlay) {
if (Math.abs(lon) > 180 || Math.abs(lat) > 85.06) { if (Math.abs(lon) > 180 || Math.abs(lat) > 85.06 ||
lon != lon || lat != lat) {
return res.status(400).send('Invalid center'); return res.status(400).send('Invalid center');
} }
if (Math.min(width, height) <= 0 || if (Math.min(width, height) <= 0 ||
Math.max(width, height) * scale > (options.maxSize || 2048)) { Math.max(width, height) * scale > (options.maxSize || 2048) ||
width != width || height != height) {
return res.status(400).send('Invalid size'); return res.status(400).send('Invalid size');
} }
if (format == 'png' || format == 'webp') { if (format == 'png' || format == 'webp') {
@@ -312,7 +349,10 @@ module.exports = function(options, repo, params, id, dataResolver) {
} }
renderer.render(params, function(err, data) { renderer.render(params, function(err, data) {
pool.release(renderer); pool.release(renderer);
if (err) console.log(err); if (err) {
console.error(err);
return;
}
var image = sharp(data, { var image = sharp(data, {
raw: { raw: {
@@ -330,6 +370,19 @@ module.exports = function(options, repo, params, id, dataResolver) {
if (opt_overlay) { if (opt_overlay) {
image.overlayWith(opt_overlay); image.overlayWith(opt_overlay);
} }
if (watermark) {
var canvas = new Canvas(scale * width, scale * height);
var ctx = canvas.getContext('2d');
ctx.scale(scale, scale);
ctx.font = '10px sans-serif';
ctx.strokeWidth = '1px';
ctx.strokeStyle = 'rgba(255,255,255,.4)';
ctx.strokeText(watermark, 5, height - 5);
ctx.fillStyle = 'rgba(0,0,0,.4)';
ctx.fillText(watermark, 5, height - 5);
image.overlayWith(canvas.toBuffer());
}
var formatQuality = (params.formatQuality || {})[format] || var formatQuality = (params.formatQuality || {})[format] ||
(options.formatQuality || {})[format]; (options.formatQuality || {})[format];
@@ -392,17 +445,22 @@ module.exports = function(options, repo, params, id, dataResolver) {
tileSize, tileSize, scale, format, res, next); tileSize, tileSize, scale, format, res, next);
}); });
var extractPathFromQuery = function(query) { var extractPathFromQuery = function(query, transformer) {
var pathParts = (query.path || '').split('|'); var pathParts = (query.path || '').split('|');
var path = []; var path = [];
pathParts.forEach(function(pair) { pathParts.forEach(function(pair) {
var pairParts = pair.split(','); var pairParts = pair.split(',');
if (pairParts.length == 2) { if (pairParts.length == 2) {
var pair;
if (query.latlng == '1' || query.latlng == 'true') { if (query.latlng == '1' || query.latlng == 'true') {
path.push([+(pairParts[1]), +(pairParts[0])]); pair = [+(pairParts[1]), +(pairParts[0])];
} else { } else {
path.push([+(pairParts[0]), +(pairParts[1])]); pair = [+(pairParts[0]), +(pairParts[1])];
} }
if (transformer) {
pair = transformer(pair);
}
path.push(pair);
} }
}); });
return path; return path;
@@ -419,9 +477,19 @@ module.exports = function(options, repo, params, id, dataResolver) {
return [px[0] * scale, px[1] * scale]; return [px[0] * scale, px[1] * scale];
}; };
var center = precisePx([x, y], z);
var mapHeight = 512 * (1 << z);
var maxEdge = center[1] + h / 2;
var minEdge = center[1] - h / 2;
if (maxEdge > mapHeight) {
center[1] -= (maxEdge - mapHeight);
} else if (minEdge < 0) {
center[1] -= minEdge;
}
var canvas = new Canvas(scale * w, scale * h); var canvas = new Canvas(scale * w, scale * h);
var ctx = canvas.getContext('2d'); var ctx = canvas.getContext('2d');
var center = precisePx([x, y], z);
ctx.scale(scale, scale); ctx.scale(scale, scale);
if (bearing) { if (bearing) {
ctx.translate(w / 2, h / 2); ctx.translate(w / 2, h / 2);
@@ -474,128 +542,168 @@ module.exports = function(options, repo, params, id, dataResolver) {
return z; return z;
}; };
var staticPattern = if (options.serveStaticMaps !== false) {
'/static/:raw(raw)?/%s/:width(\\d+)x:height(\\d+)' + var staticPattern =
':scale(' + SCALE_PATTERN + ')?\.:format([\\w]+)'; '/' + id + '/static/:raw(raw)?/%s/:width(\\d+)x:height(\\d+)' +
':scale(' + scalePattern + ')?\.:format([\\w]+)';
var centerPattern = var centerPattern =
util.format(':x(%s),:y(%s),:z(%s)(@:bearing(%s)(,:pitch(%s))?)?', util.format(':x(%s),:y(%s),:z(%s)(@:bearing(%s)(,:pitch(%s))?)?',
FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN,
FLOAT_PATTERN, FLOAT_PATTERN); FLOAT_PATTERN, FLOAT_PATTERN);
app.get(util.format(staticPattern, centerPattern), function(req, res, next) { app.get(util.format(staticPattern, centerPattern), function(req, res, next) {
var raw = req.params.raw; var raw = req.params.raw;
var z = +req.params.z, var z = +req.params.z,
x = +req.params.x, x = +req.params.x,
y = +req.params.y, y = +req.params.y,
bearing = +(req.params.bearing || '0'), bearing = +(req.params.bearing || '0'),
pitch = +(req.params.pitch || '0'), pitch = +(req.params.pitch || '0'),
w = req.params.width | 0, w = req.params.width | 0,
h = req.params.height | 0, h = req.params.height | 0,
scale = getScale(req.params.scale), scale = getScale(req.params.scale),
format = req.params.format; format = req.params.format;
if (z < 0) { if (z < 0) {
return res.status(404).send('Invalid zoom'); return res.status(404).send('Invalid zoom');
} }
if (raw) { var transformer = raw ?
var ll = mercator.inverse([x, y]); mercator.inverse.bind(mercator) : dataProjWGStoInternalWGS;
x = ll[0];
y = ll[1];
}
var path = extractPathFromQuery(req.query); if (transformer) {
var overlay = renderOverlay(z, x, y, bearing, pitch, w, h, scale, var ll = transformer([x, y]);
path, req.query); x = ll[0];
y = ll[1];
}
return respondImage(z, x, y, bearing, pitch, w, h, scale, format, var path = extractPathFromQuery(req.query, transformer);
res, next, overlay); var overlay = renderOverlay(z, x, y, bearing, pitch, w, h, scale,
}); path, req.query);
var boundsPattern = return respondImage(z, x, y, bearing, pitch, w, h, scale, format,
util.format(':minx(%s),:miny(%s),:maxx(%s),:maxy(%s)', res, next, overlay);
FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN);
app.get(util.format(staticPattern, boundsPattern), function(req, res, next) {
var raw = req.params.raw;
var bbox = [+req.params.minx, +req.params.miny,
+req.params.maxx, +req.params.maxy];
if (raw) {
var minCorner = mercator.inverse(bbox.slice(0, 2));
var maxCorner = mercator.inverse(bbox.slice(2));
bbox[0] = minCorner[0];
bbox[1] = minCorner[1];
bbox[2] = maxCorner[0];
bbox[3] = maxCorner[1];
}
var w = req.params.width | 0,
h = req.params.height | 0,
scale = getScale(req.params.scale),
format = req.params.format;
var z = calcZForBBox(bbox, w, h, req.query),
x = (bbox[0] + bbox[2]) / 2,
y = (bbox[1] + bbox[3]) / 2,
bearing = 0,
pitch = 0;
var path = extractPathFromQuery(req.query);
var overlay = renderOverlay(z, x, y, bearing, pitch, w, h, scale,
path, req.query);
return respondImage(z, x, y, bearing, pitch, w, h, scale, format,
res, next, overlay);
});
var autoPattern = 'auto';
app.get(util.format(staticPattern, autoPattern), function(req, res, next) {
var path = extractPathFromQuery(req.query);
if (path.length < 2) {
return res.status(400).send('Invalid path');
}
var raw = req.params.raw;
var w = req.params.width | 0,
h = req.params.height | 0,
bearing = 0,
pitch = 0,
scale = getScale(req.params.scale),
format = req.params.format;
var bbox = [Infinity, Infinity, -Infinity, -Infinity];
path.forEach(function(pair) {
bbox[0] = Math.min(bbox[0], pair[0]);
bbox[1] = Math.min(bbox[1], pair[1]);
bbox[2] = Math.max(bbox[2], pair[0]);
bbox[3] = Math.max(bbox[3], pair[1]);
}); });
var z = calcZForBBox(bbox, w, h, req.query), var serveBounds = function(req, res, next) {
x = (bbox[0] + bbox[2]) / 2, var raw = req.params.raw;
y = (bbox[1] + bbox[3]) / 2; var bbox = [+req.params.minx, +req.params.miny,
+req.params.maxx, +req.params.maxy];
var center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2];
if (raw) { var transformer = raw ?
var ll = mercator.inverse([x, y]); mercator.inverse.bind(mercator) : dataProjWGStoInternalWGS;
x = ll[0];
y = ll[1];
}
var overlay = renderOverlay(z, x, y, bearing, pitch, w, h, scale, if (transformer) {
path, req.query); var minCorner = transformer(bbox.slice(0, 2));
var maxCorner = transformer(bbox.slice(2));
bbox[0] = minCorner[0];
bbox[1] = minCorner[1];
bbox[2] = maxCorner[0];
bbox[3] = maxCorner[1];
center = transformer(center);
}
return respondImage(z, x, y, bearing, pitch, w, h, scale, format, var w = req.params.width | 0,
res, next, overlay); h = req.params.height | 0,
}); scale = getScale(req.params.scale),
format = req.params.format;
app.get('/rendered.json', function(req, res, next) { var z = calcZForBBox(bbox, w, h, req.query),
x = center[0],
y = center[1],
bearing = 0,
pitch = 0;
var path = extractPathFromQuery(req.query, transformer);
var overlay = renderOverlay(z, x, y, bearing, pitch, w, h, scale,
path, req.query);
return respondImage(z, x, y, bearing, pitch, w, h, scale, format,
res, next, overlay);
};
var boundsPattern =
util.format(':minx(%s),:miny(%s),:maxx(%s),:maxy(%s)',
FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN);
app.get(util.format(staticPattern, boundsPattern), serveBounds);
app.get('/' + id + '/static/', function(req, res, next) {
for (var key in req.query) {
req.query[key.toLowerCase()] = req.query[key];
}
req.params.raw = true;
req.params.format = (req.query.format || 'image/png').split('/').pop();
var bbox = (req.query.bbox || '').split(',');
req.params.minx = bbox[0];
req.params.miny = bbox[1];
req.params.maxx = bbox[2];
req.params.maxy = bbox[3];
req.params.width = req.query.width || '256';
req.params.height = req.query.height || '256';
if (req.query.scale) {
req.params.width /= req.query.scale;
req.params.height /= req.query.scale;
req.params.scale = '@' + req.query.scale;
}
return serveBounds(req, res, next);
});
var autoPattern = 'auto';
app.get(util.format(staticPattern, autoPattern), function(req, res, next) {
var raw = req.params.raw;
var w = req.params.width | 0,
h = req.params.height | 0,
bearing = 0,
pitch = 0,
scale = getScale(req.params.scale),
format = req.params.format;
var transformer = raw ?
mercator.inverse.bind(mercator) : dataProjWGStoInternalWGS;
var path = extractPathFromQuery(req.query, transformer);
if (path.length < 2) {
return res.status(400).send('Invalid path');
}
var bbox = [Infinity, Infinity, -Infinity, -Infinity];
path.forEach(function(pair) {
bbox[0] = Math.min(bbox[0], pair[0]);
bbox[1] = Math.min(bbox[1], pair[1]);
bbox[2] = Math.max(bbox[2], pair[0]);
bbox[3] = Math.max(bbox[3], pair[1]);
});
var bbox_ = mercator.convert(bbox, '900913');
var center = mercator.inverse(
[(bbox_[0] + bbox_[2]) / 2, (bbox_[1] + bbox_[3]) / 2]
);
var z = calcZForBBox(bbox, w, h, req.query),
x = center[0],
y = center[1];
var overlay = renderOverlay(z, x, y, bearing, pitch, w, h, scale,
path, req.query);
return respondImage(z, x, y, bearing, pitch, w, h, scale, format,
res, next, overlay);
});
}
app.get('/' + id + '.json', function(req, res, next) {
var info = clone(tileJSON); var info = clone(tileJSON);
info.tiles = utils.getTileUrls(req, info.tiles, info.tiles = utils.getTileUrls(req, info.tiles,
'styles/' + id + '/rendered', info.format); 'styles/' + id, info.format);
return res.send(info); return res.send(info);
}); });
return app; return new Promise(function(resolve, reject) {
Promise.all([fontListingPromise, renderersReadyPromise]).then(function() {
resolve(app);
});
});
}; };

View File

@@ -10,7 +10,7 @@ var clone = require('clone'),
module.exports = function(options, repo, params, id, reportTiles, reportFont) { module.exports = function(options, repo, params, id, reportTiles, reportFont) {
var app = express().disable('x-powered-by'); var app = express().disable('x-powered-by');
var styleFile = path.join(options.paths.styles, params.style); var styleFile = path.resolve(options.paths.styles, params.style);
var styleJSON = clone(require(styleFile)); var styleJSON = clone(require(styleFile));
Object.keys(styleJSON.sources).forEach(function(name) { Object.keys(styleJSON.sources).forEach(function(name) {
@@ -62,13 +62,13 @@ module.exports = function(options, repo, params, id, reportTiles, reportFont) {
repo[id] = styleJSON; repo[id] = styleJSON;
app.get('/' + id + '.json', function(req, res, next) { app.get('/' + id + '/style.json', function(req, res, next) {
var fixUrl = function(url, opt_nokey, opt_nostyle) { var fixUrl = function(url, opt_nokey, opt_nostyle) {
if (!url || (typeof url !== 'string') || url.indexOf('local://') !== 0) { if (!url || (typeof url !== 'string') || url.indexOf('local://') !== 0) {
return url; return url;
} }
var queryParams = []; var queryParams = [];
if (!opt_nostyle) { if (!opt_nostyle && global.addStyleParam) {
queryParams.push('style=' + id); queryParams.push('style=' + id);
} }
if (!opt_nokey && req.query.key) { if (!opt_nokey && req.query.key) {
@@ -117,5 +117,5 @@ module.exports = function(options, repo, params, id, reportTiles, reportFont) {
}); });
}); });
return app; return Promise.resolve(app);
}; };

View File

@@ -10,6 +10,7 @@ var fs = require('fs'),
var base64url = require('base64url'), var base64url = require('base64url'),
clone = require('clone'), clone = require('clone'),
cors = require('cors'), cors = require('cors'),
enableShutdown = require('http-shutdown'),
express = require('express'), express = require('express'),
handlebars = require('handlebars'), handlebars = require('handlebars'),
mercator = new (require('@mapbox/sphericalmercator'))(), mercator = new (require('@mapbox/sphericalmercator'))(),
@@ -28,7 +29,7 @@ if (!isLight) {
serve_rendered = require('./serve_rendered'); serve_rendered = require('./serve_rendered');
} }
module.exports = function(opts, callback) { function start(opts) {
console.log('Starting server'); console.log('Starting server');
var app = express().disable('x-powered-by'), var app = express().disable('x-powered-by'),
@@ -41,10 +42,9 @@ module.exports = function(opts, callback) {
app.enable('trust proxy'); app.enable('trust proxy');
callback = callback || function() {}; if (process.env.NODE_ENV == 'production') {
app.use(morgan('tiny'));
if (process.env.NODE_ENV !== 'production' && } else if (process.env.NODE_ENV !== 'test') {
process.env.NODE_ENV !== 'test') {
app.use(morgan('dev')); app.use(morgan('dev'));
} }
@@ -76,6 +76,8 @@ module.exports = function(opts, callback) {
paths.sprites = path.resolve(paths.root, paths.sprites || ''); paths.sprites = path.resolve(paths.root, paths.sprites || '');
paths.mbtiles = path.resolve(paths.root, paths.mbtiles || ''); paths.mbtiles = path.resolve(paths.root, paths.mbtiles || '');
var startupPromises = [];
var checkPath = function(type) { var checkPath = function(type) {
if (!fs.existsSync(paths[type])) { if (!fs.existsSync(paths[type])) {
console.error('The specified path for "' + type + '" does not exist (' + paths[type] + ').'); console.error('The specified path for "' + type + '" does not exist (' + paths[type] + ').');
@@ -89,7 +91,9 @@ module.exports = function(opts, callback) {
var data = clone(config.data || {}); var data = clone(config.data || {});
app.use(cors()); if (opts.cors) {
app.use(cors());
}
Object.keys(config.styles || {}).forEach(function(id) { Object.keys(config.styles || {}).forEach(function(id) {
var item = config.styles[id]; var item = config.styles[id];
@@ -99,7 +103,7 @@ module.exports = function(opts, callback) {
} }
if (item.serve_data !== false) { if (item.serve_data !== false) {
app.use('/styles/', serve_style(options, serving.styles, item, id, startupPromises.push(serve_style(options, serving.styles, item, id,
function(mbtiles, fromData) { function(mbtiles, fromData) {
var dataItemId; var dataItemId;
Object.keys(data).forEach(function(id) { Object.keys(data).forEach(function(id) {
@@ -128,28 +132,38 @@ module.exports = function(opts, callback) {
} }
}, function(font) { }, function(font) {
serving.fonts[font] = true; serving.fonts[font] = true;
}).then(function(sub) {
app.use('/styles/', sub);
})); }));
} }
if (item.serve_rendered !== false) { if (item.serve_rendered !== false) {
if (serve_rendered) { if (serve_rendered) {
app.use('/styles/' + id + '/', startupPromises.push(
serve_rendered(options, serving.rendered, item, id, serve_rendered(options, serving.rendered, item, id,
function(mbtiles) { function(mbtiles) {
var mbtilesFile; var mbtilesFile;
Object.keys(data).forEach(function(id) { Object.keys(data).forEach(function(id) {
if (id == mbtiles) { if (id == mbtiles) {
mbtilesFile = data[id].mbtiles; mbtilesFile = data[id].mbtiles;
}
});
return mbtilesFile;
} }
}); ).then(function(sub) {
return mbtilesFile; app.use('/styles/', sub);
})); })
);
} else { } else {
item.serve_rendered = false; item.serve_rendered = false;
} }
} }
}); });
app.use('/', serve_font(options, serving.fonts)); startupPromises.push(
serve_font(options, serving.fonts).then(function(sub) {
app.use('/', sub);
})
);
Object.keys(data).forEach(function(id) { Object.keys(data).forEach(function(id) {
var item = data[id]; var item = data[id];
@@ -158,7 +172,11 @@ module.exports = function(opts, callback) {
return; return;
} }
app.use('/data/', serve_data(options, serving.data, item, id, serving.styles)); startupPromises.push(
serve_data(options, serving.data, item, id, serving.styles).then(function(sub) {
app.use('/data/', sub);
})
);
}); });
app.get('/styles.json', function(req, res, next) { app.get('/styles.json', function(req, res, next) {
@@ -171,7 +189,7 @@ module.exports = function(opts, callback) {
name: styleJSON.name, name: styleJSON.name,
id: id, id: id,
url: req.protocol + '://' + req.headers.host + url: req.protocol + '://' + req.headers.host +
'/styles/' + id + '.json' + query '/styles/' + id + '/style.json' + query
}); });
}); });
res.send(result); res.send(result);
@@ -182,7 +200,7 @@ module.exports = function(opts, callback) {
var info = clone(serving[type][id]); var info = clone(serving[type][id]);
var path = ''; var path = '';
if (type == 'rendered') { if (type == 'rendered') {
path = 'styles/' + id + '/rendered'; path = 'styles/' + id;
} else { } else {
path = type + '/' + id; path = type + '/' + id;
} }
@@ -209,29 +227,42 @@ module.exports = function(opts, callback) {
app.use('/', express.static(path.join(__dirname, '../public/resources'))); app.use('/', express.static(path.join(__dirname, '../public/resources')));
var templates = path.join(__dirname, '../public/templates'); var templates = path.join(__dirname, '../public/templates');
var serveTemplate = function(path, template, dataGetter) { var serveTemplate = function(urlPath, template, dataGetter) {
fs.readFile(templates + '/' + template + '.tmpl', function(err, content) { var templateFile = templates + '/' + template + '.tmpl';
if (err) { if (template == 'index') {
console.log('Template not found:', err); if (options.frontPage === false) {
return;
} else if (options.frontPage &&
options.frontPage.constructor === String) {
templateFile = path.resolve(paths.root, options.frontPage);
} }
var compiled = handlebars.compile(content.toString()); }
startupPromises.push(new Promise(function(resolve, reject) {
app.use(path, function(req, res, next) { fs.readFile(templateFile, function(err, content) {
var data = {}; if (err) {
if (dataGetter) { console.error('Template not found:', err);
data = dataGetter(req); reject(err);
if (!data) {
return res.status(404).send('Not found');
}
} }
data['server_version'] = packageJson.name + ' v' + packageJson.version; var compiled = handlebars.compile(content.toString());
data['is_light'] = isLight;
data['key_query_part'] = app.use(urlPath, function(req, res, next) {
req.query.key ? 'key=' + req.query.key + '&amp;' : ''; var data = {};
data['key_query'] = req.query.key ? '?key=' + req.query.key : ''; if (dataGetter) {
return res.status(200).send(compiled(data)); data = dataGetter(req);
if (!data) {
return res.status(404).send('Not found');
}
}
data['server_version'] = packageJson.name + ' v' + packageJson.version;
data['is_light'] = isLight;
data['key_query_part'] =
req.query.key ? 'key=' + req.query.key + '&amp;' : '';
data['key_query'] = req.query.key ? '?key=' + req.query.key : '';
return res.status(200).send(compiled(data));
});
resolve();
}); });
}); }));
}; };
serveTemplate('/$', 'index', function(req) { serveTemplate('/$', 'index', function(req) {
@@ -257,11 +288,11 @@ module.exports = function(opts, callback) {
var query = req.query.key ? ('?key=' + req.query.key) : ''; var query = req.query.key ? ('?key=' + req.query.key) : '';
style.wmts_link = 'http://wmts.maptiler.com/' + style.wmts_link = 'http://wmts.maptiler.com/' +
base64url('http://' + req.headers.host + base64url('http://' + req.headers.host +
'/styles/' + id + '/rendered.json' + query) + '/wmts'; '/styles/' + id + '.json' + query) + '/wmts';
var tiles = utils.getTileUrls( var tiles = utils.getTileUrls(
req, style.serving_rendered.tiles, req, style.serving_rendered.tiles,
'styles/' + id + '/rendered', style.serving_rendered.format); 'styles/' + id, style.serving_rendered.format);
style.xyz_link = tiles[0]; style.xyz_link = tiles[0];
} }
}); });
@@ -309,8 +340,8 @@ module.exports = function(opts, callback) {
} }
}); });
return { return {
styles: styles, styles: Object.keys(styles).length ? styles : null,
data: data data: Object.keys(data).length ? data : null
}; };
}); });
@@ -344,20 +375,57 @@ module.exports = function(opts, callback) {
return data; return data;
}); });
var startupComplete = false;
var startupPromise = Promise.all(startupPromises).then(function() {
console.log('Startup complete');
startupComplete = true;
});
app.get('/health', function(req, res, next) {
if (startupComplete) {
return res.status(200).send('OK');
} else {
return res.status(503).send('Starting');
}
});
var server = app.listen(process.env.PORT || opts.port, process.env.BIND || opts.bind, function() { var server = app.listen(process.env.PORT || opts.port, process.env.BIND || opts.bind, function() {
console.log('Listening at http://%s:%d/', var address = this.address().address;
this.address().address, this.address().port); if (address.indexOf('::') === 0) {
address = '[' + address + ']'; // literal IPv6 address
return callback(); }
console.log('Listening at http://%s:%d/', address, this.address().port);
}); });
process.on('SIGINT', function() { // add server.shutdown() to gracefully stop serving
process.exit(); enableShutdown(server);
});
setTimeout(callback, 1000);
return { return {
app: app, app: app,
server: server server: server,
startupPromise: startupPromise
}; };
}
module.exports = function(opts) {
var running = start(opts);
process.on('SIGINT', function() {
process.exit();
});
process.on('SIGHUP', function() {
console.log('Stopping server and reloading config');
running.server.shutdown(function() {
for (var key in require.cache) {
delete require.cache[key];
}
var restarted = start(opts);
running.server = restarted.server;
running.app = restarted.app;
});
});
return running;
}; };

View File

@@ -1,7 +1,6 @@
'use strict'; 'use strict';
var async = require('async'), var path = require('path'),
path = require('path'),
fs = require('fs'); fs = require('fs');
var clone = require('clone'), var clone = require('clone'),
@@ -13,6 +12,23 @@ module.exports.getTileUrls = function(req, domains, path, format, aliases) {
if (domains.constructor === String && domains.length > 0) { if (domains.constructor === String && domains.length > 0) {
domains = domains.split(','); domains = domains.split(',');
} }
var host = req.headers.host;
var hostParts = host.split('.');
var relativeSubdomainsUsable = hostParts.length > 1 &&
!/^([0-9]{1,3}\.){3}[0-9]{1,3}(\:[0-9]+)?$/.test(host);
var newDomains = [];
domains.forEach(function(domain) {
if (domain.indexOf('*') !== -1) {
if (relativeSubdomainsUsable) {
var newParts = hostParts.slice(1);
newParts.unshift(domain.replace('*', hostParts[0]));
newDomains.push(newParts.join('.'));
}
} else {
newDomains.push(domain);
}
});
domains = newDomains;
} }
if (!domains || domains.length == 0) { if (!domains || domains.length == 0) {
domains = [req.headers.host]; domains = [req.headers.host];
@@ -56,47 +72,47 @@ module.exports.fixTileJSONCenter = function(tileJSON) {
} }
}; };
module.exports.getFontsPbf = function(allowedFonts, fontPath, names, range, fallbacks, callback) { var getFontPbf = function(allowedFonts, fontPath, name, range, fallbacks) {
var getFontPbf = function(allowedFonts, name, range, callback, fallbacks) { return new Promise(function(resolve, reject) {
if (!allowedFonts || (allowedFonts[name] && fallbacks)) { if (!allowedFonts || (allowedFonts[name] && fallbacks)) {
var filename = path.join(fontPath, name, range + '.pbf'); var filename = path.join(fontPath, name, range + '.pbf');
if (!fallbacks) { if (!fallbacks) {
fallbacks = clone(allowedFonts || {}); fallbacks = clone(allowedFonts || {});
} }
delete fallbacks[name]; delete fallbacks[name];
return fs.readFile(filename, function(err, data) { fs.readFile(filename, function(err, data) {
if (err) { if (err) {
console.error('ERROR: Font not found:', name); console.error('ERROR: Font not found:', name);
if (fallbacks && Object.keys(fallbacks).length) { if (fallbacks && Object.keys(fallbacks).length) {
var fallbackName = Object.keys(fallbacks)[0]; var fallbackName = Object.keys(fallbacks)[0];
console.error('ERROR: Trying to use', fallbackName, 'as a fallback'); console.error('ERROR: Trying to use', fallbackName, 'as a fallback');
delete fallbacks[fallbackName]; delete fallbacks[fallbackName];
return getFontPbf(null, fallbackName, range, callback, fallbacks); getFontPbf(null, fontPath, fallbackName, range, fallbacks).then(resolve, reject);
} else { } else {
return callback(new Error('Font load error: ' + name)); reject('Font load error: ' + name);
} }
} else { } else {
return callback(null, data); resolve(data);
} }
}); });
} else { } else {
return callback(new Error('Font not allowed: ' + name)); reject('Font not allowed: ' + name);
}
};
var fonts = names.split(',');
var queue = [];
fonts.forEach(function(font) {
queue.push(function(callback) {
getFontPbf(allowedFonts, font, range, callback, clone(allowedFonts || fallbacks));
});
});
return async.parallel(queue, function(err, results) {
if (err) {
callback(err, new Buffer([]));
} else {
callback(err, glyphCompose.combine(results));
} }
}); });
}; };
module.exports.getFontsPbf = function(allowedFonts, fontPath, names, range, fallbacks) {
var fonts = names.split(',');
var queue = [];
fonts.forEach(function(font) {
queue.push(
getFontPbf(allowedFonts, fontPath, font, range, clone(allowedFonts || fallbacks))
);
});
return new Promise(function(resolve, reject) {
Promise.all(queue).then(function(values) {
return resolve(glyphCompose.combine(values));
}, reject);
});
};

View File

@@ -38,6 +38,14 @@ var testTileJSON = function(url) {
}; };
describe('Metadata', function() { describe('Metadata', function() {
describe('/health', function() {
it('returns 200', function(done) {
supertest(app)
.get('/health')
.expect(200, done);
});
});
testTileJSONArray('/index.json'); testTileJSONArray('/index.json');
testTileJSONArray('/rendered.json'); testTileJSONArray('/rendered.json');
testTileJSONArray('/data.json'); testTileJSONArray('/data.json');
@@ -63,6 +71,6 @@ describe('Metadata', function() {
}); });
}); });
testTileJSON('/styles/test-style/rendered.json'); testTileJSON('/styles/test-style.json');
testTileJSON('/data/openmaptiles.json'); testTileJSON('/data/openmaptiles.json');
}); });

View File

@@ -12,6 +12,7 @@ before(function() {
}); });
global.app = running.app; global.app = running.app;
global.server = running.server; global.server = running.server;
return running.startupPromise;
}); });
after(function() { after(function() {

View File

@@ -11,12 +11,12 @@ var testIs = function(url, type, status) {
var prefix = 'test-style'; var prefix = 'test-style';
describe('Styles', function() { describe('Styles', function() {
describe('/styles/' + prefix + '.json is valid style', function() { describe('/styles/' + prefix + '/style.json is valid style', function() {
testIs('/styles/' + prefix + '.json', /application\/json/); testIs('/styles/' + prefix + '/style.json', /application\/json/);
it('contains expected properties', function(done) { it('contains expected properties', function(done) {
supertest(app) supertest(app)
.get('/styles/' + prefix + '.json') .get('/styles/' + prefix + '/style.json')
.expect(function(res) { .expect(function(res) {
res.body.version.should.equal(8); res.body.version.should.equal(8);
res.body.name.should.be.String(); res.body.name.should.be.String();
@@ -27,8 +27,8 @@ describe('Styles', function() {
}).end(done); }).end(done);
}); });
}); });
describe('/styles/streets.json is not served', function() { describe('/styles/streets/style.json is not served', function() {
testIs('/styles/streets.json', /./, 404); testIs('/styles/streets/style.json', /./, 404);
}); });
describe('/styles/' + prefix + '/sprite[@2x].{format}', function() { describe('/styles/' + prefix + '/sprite[@2x].{format}', function() {

View File

@@ -1,6 +1,6 @@
var testTile = function(prefix, z, x, y, format, status, scale, type) { var testTile = function(prefix, z, x, y, format, status, scale, type) {
if (scale) y += '@' + scale + 'x'; if (scale) y += '@' + scale + 'x';
var path = '/styles/' + prefix + '/rendered/' + z + '/' + x + '/' + y + '.' + format; var path = '/styles/' + prefix + '/' + z + '/' + x + '/' + y + '.' + format;
it(path + ' returns ' + status, function(done) { it(path + ' returns ' + status, function(done) {
var test = supertest(app).get(path); var test = supertest(app).get(path);
test.expect(status); test.expect(status);
@@ -26,7 +26,6 @@ describe('Raster tiles', function() {
testTile(prefix, 0, 0, 0, 'png', 200, 2); testTile(prefix, 0, 0, 0, 'png', 200, 2);
testTile(prefix, 0, 0, 0, 'png', 200, 3); testTile(prefix, 0, 0, 0, 'png', 200, 3);
testTile(prefix, 2, 1, 1, 'png', 200, 3); testTile(prefix, 2, 1, 1, 'png', 200, 3);
testTile(prefix, 0, 0, 0, 'png', 200, 4);
}); });
}); });