Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00aaafa6b5 | |||
| bb45d0bf6d | |||
| 0dc8d45265 | |||
| 42171015b9 | |||
| edfb05186f | |||
| 41027dd975 | |||
| 8585720380 | |||
| cf729bc893 | |||
| 539125df6f |
@@ -1,3 +0,0 @@
|
|||||||
.git
|
|
||||||
node_modules
|
|
||||||
test_data
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
node_modules
|
|
||||||
test_data
|
|
||||||
config.json
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
FROM debian:stretch
|
|
||||||
MAINTAINER Petr Sloup <petr.sloup@klokantech.com>
|
|
||||||
|
|
||||||
RUN apt-get -qq update \
|
|
||||||
&& DEBIAN_FRONTEND=noninteractive apt-get -y install \
|
|
||||||
curl \
|
|
||||||
build-essential \
|
|
||||||
python \
|
|
||||||
xvfb \
|
|
||||||
&& curl -sL https://deb.nodesource.com/setup_5.x | bash - \
|
|
||||||
&& apt-get -y install nodejs \
|
|
||||||
&& apt-get clean
|
|
||||||
|
|
||||||
RUN mkdir -p /usr/src/app
|
|
||||||
COPY / /usr/src/app
|
|
||||||
RUN cd /usr/src/app && npm install
|
|
||||||
|
|
||||||
VOLUME /data
|
|
||||||
WORKDIR /data
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
CMD ["/usr/src/app/run.sh"]
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# tileserver-gl
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
- `docker run -it -v $(pwd):/data -p 8080:80 klokantech/tileserver-gl`
|
|
||||||
|
|
||||||
### Without docker
|
|
||||||
- Make sure you have Node v4 or higher (`nvm install 4`)
|
|
||||||
- `npm install`
|
|
||||||
- `node src/main.js`
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Create `config.json` file in the root directory.
|
|
||||||
The config file can contain definition of several paths where the tiles will be served.
|
|
||||||
|
|
||||||
Every path needs to have `root` specified. All other paths (in the config (`style`) **and** in the style (`sprites`, `glyphs`, `sources`, ...)) are relative to this root.
|
|
||||||
|
|
||||||
For raster endpoints specify `style`, for serving raw `pbf` vector tiles specify `mbtiles` property.
|
|
||||||
|
|
||||||
Alternative `domains` can be specified (array or comma-separated string). These will be used to generate tile urls for `index.json`.
|
|
||||||
|
|
||||||
Every path can also have `options` object and its content is directly copied into served `index.json`.
|
|
||||||
The `options.format` can be used to modify the extension in tile urls inside `index.json`, but the server will still serve all the supported formats.
|
|
||||||
|
|
||||||
### Example configuration file
|
|
||||||
|
|
||||||
Example styles can be downloaded from https://github.com/klokantech/osm2vectortiles-gl-styles.
|
|
||||||
Rendered vector tiles can be found at http://osm2vectortiles.org/downloads/.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"/basic": {
|
|
||||||
"root": "test_data",
|
|
||||||
"style": "styles/basic-v8.json",
|
|
||||||
"domains": [
|
|
||||||
"localhost:8080",
|
|
||||||
"127.0.0.1:8080"
|
|
||||||
],
|
|
||||||
"options": {
|
|
||||||
"type": "overlay",
|
|
||||||
"bounds": [5.8559113, 45.717995, 10.5922941, 47.9084648]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/hybrid": {
|
|
||||||
"root": "test_data",
|
|
||||||
"style": "styles/satellite-hybrid-v8.json",
|
|
||||||
"options": {
|
|
||||||
"format": "webp"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/switzerland-vector": {
|
|
||||||
"root": "test_data",
|
|
||||||
"mbtiles": "switzerland.mbtiles"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**Note**: To specify local mbtiles as source of the vector tiles inside the style, use urls with `mbtiles` protocol with path relative to the `root`. (For example `mbtiles://switzerland.mbtiles`)
|
|
||||||
|
|
||||||
## 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}`
|
|
||||||
- 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`
|
|
||||||
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 530 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
@@ -0,0 +1,67 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="2034.203px" height="552.055px" viewBox="0 0 2034.203 552.055" enable-background="new 0 0 2034.203 552.055"
|
||||||
|
xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path fill="#3A1888" d="M3.604-242.717"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path fill="#3A1888" d="M152.645,436.647c25.674,25.668,94.015,95.335,93.983,95.406c-0.249,0.454,67.892-67.963,95.032-95.087
|
||||||
|
l-94.67-94.665L152.645,436.647z"/>
|
||||||
|
<path fill="#03A1C4" d="M246.99,342.301l94.67,94.665c0.141-0.157,0.314-0.336,0.466-0.477l94.578-94.583l-94.66-94.662
|
||||||
|
L246.99,342.301z"/>
|
||||||
|
<path fill="#05D0DF" d="M436.704,341.907l0.243-0.244c52.317-52.312,52.36-137.096,0.157-189.473l-95.06,95.055L436.704,341.907z"
|
||||||
|
/>
|
||||||
|
<path fill="#761FE8" d="M151.931,247.245l-94.329,94.326c0.027,0.032,0.043,0.064,0.076,0.092l94.811,94.827
|
||||||
|
c0.054,0.049,0.108,0.098,0.157,0.157l94.345-94.346L151.931,247.245z"/>
|
||||||
|
<path fill="#FFAA01" d="M246.99,152.184l95.054,95.061l95.06-95.055c-0.076-0.054-0.103-0.108-0.157-0.162l-94.821-94.816
|
||||||
|
c-0.022-0.027-0.054-0.054-0.082-0.081L246.99,152.184z"/>
|
||||||
|
<path fill="#F1175D" d="M57.201,152.514c-51.852,52.377-51.722,136.848,0.4,189.057l94.329-94.326L57.201,152.514z"/>
|
||||||
|
<path fill="#FB3A1B" d="M246.99,152.184L152.255,57.45l-94.578,94.578c-0.163,0.162-0.309,0.336-0.476,0.486l94.729,94.73
|
||||||
|
L246.99,152.184z"/>
|
||||||
|
<path fill="#FBC935" d="M342.044,57.13C289.663,4.846,204.832,4.874,152.488,57.211l-0.233,0.238l94.735,94.734L342.044,57.13z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path fill="#333359" d="M734.146,365.616v-96.875c0-23.851-12.479-45.492-37.077-45.492c-24.224,0-38.517,21.642-38.517,45.492
|
||||||
|
v96.875h-44.761V184.347h41.46l3.301,22.021c9.542-18.353,30.458-24.949,47.685-24.949c21.669,0,43.306,8.811,53.588,33.754
|
||||||
|
c16.144-25.685,37.066-33.022,60.537-33.022c51.38,0,76.692,31.551,76.692,85.859v97.605h-44.767V268.01
|
||||||
|
c0-23.84-9.904-44.037-34.106-44.037c-24.234,0-39.279,20.917-39.279,44.768v96.875H734.146z"/>
|
||||||
|
<path fill="#333359" d="M1086.026,184.726h42.938v180.89h-42.208l-2.208-26.41c-10.266,21.269-38.516,31.535-58.702,31.914
|
||||||
|
c-53.556,0.368-93.198-32.655-93.198-96.137c0-62.375,41.477-95.029,94.313-94.662c24.212,0,47.321,11.371,57.587,29.354
|
||||||
|
L1086.026,184.726z M977.416,274.983c0,34.479,23.85,55.039,53.573,55.039c70.446,0,70.446-109.713,0-109.713
|
||||||
|
C1001.266,220.309,977.416,240.496,977.416,274.983z"/>
|
||||||
|
<path fill="#333359" d="M1166.756,441.214V184.726h41.839l2.923,24.949c13.951-20.187,38.175-28.991,58.719-28.991
|
||||||
|
c55.753,0,92.835,41.471,92.835,94.667c0,52.847-33.401,94.675-91.374,94.675c-19.065,0-47.332-5.888-60.18-25.695v96.884
|
||||||
|
H1166.756z M1318.305,275.351c0-28.253-19.082-51.378-51.37-51.378c-32.298,0-51.38,23.125-51.38,51.378
|
||||||
|
c0,28.244,20.922,51.38,51.38,51.38C1297.404,326.731,1318.305,303.595,1318.305,275.351z"/>
|
||||||
|
<path fill="#333359" d="M1443.064,129.682v54.665h61.642v15.046h-61.642v110.453c0,24.575,5.146,41.823,33.392,41.823
|
||||||
|
c8.805,0,18.709-2.938,27.882-7.339l6.24,14.666c-11.382,5.521-22.763,9.185-34.122,9.185c-38.527,0-51.002-22.752-51.002-58.335
|
||||||
|
V199.393h-38.538v-15.046h38.538v-52.831L1443.064,129.682z"/>
|
||||||
|
<path fill="#333359" d="M1570.027,125.272c0,19.082-28.986,19.082-28.986,0C1541.041,106.2,1570.027,106.2,1570.027,125.272z
|
||||||
|
M1546.188,183.612v182.004h17.962V183.612H1546.188z"/>
|
||||||
|
<path fill="#333359" d="M1633.503,108.776v256.84h-17.983v-256.84H1633.503z"/>
|
||||||
|
<path fill="#333359" d="M1918.606,184.347l0.73,32.304c11.365-24.603,37.066-34.133,60.181-34.133
|
||||||
|
c13.589-0.367,26.772,3.307,38.896,10.646l-8.08,14.671c-9.525-5.871-20.187-8.441-30.815-8.441
|
||||||
|
c-33.771,0.379-59.817,27.524-59.817,60.553v105.67h-17.979V184.347H1918.606z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path fill="none" d="M1694.655,305.711c0.006,0.016,0.014,0.031,0.02,0.047l146.748-38.832c-0.007-0.055-0.012-0.11-0.018-0.166
|
||||||
|
L1694.655,305.711z"/>
|
||||||
|
<path fill="none" d="M1765.447,197.873c-42.255,0-76.514,34.997-76.514,78.169c0,4.196,0.333,8.312,0.956,12.329l147.452-39.137
|
||||||
|
C1826.633,219.268,1798.486,197.873,1765.447,197.873z"/>
|
||||||
|
<g>
|
||||||
|
<path fill="none" d="M1765.447,198.374c-42.255,0-76.514,34.996-76.514,78.169c0,4.196,0.333,8.312,0.956,12.329l147.452-39.137
|
||||||
|
C1826.633,219.768,1798.486,198.374,1765.447,198.374z"/>
|
||||||
|
<path fill="#333359" d="M1765.447,354.709c-31.946,0-59.308-20.014-70.764-48.431l-0.1,0.004l0.091-0.024
|
||||||
|
c-0.006-0.016-0.014-0.031-0.02-0.047l146.75-38.951c0.006,0.056,0.011,0.111,0.018,0.166l15.616-4.133
|
||||||
|
c-6.306-45.918-44.904-81.253-91.59-81.253c-51.089,0-92.501,42.31-92.501,94.5s41.412,94.501,92.501,94.501
|
||||||
|
c38.213,0,71.011-23.675,85.115-57.448l-14.717-6.398C1824.179,335.126,1797.054,354.709,1765.447,354.709z M1688.934,276.542
|
||||||
|
c0-43.173,34.259-78.169,76.514-78.169c33.039,0,61.186,21.395,71.895,51.361l-147.452,39.137
|
||||||
|
C1689.267,284.854,1688.934,280.739,1688.934,276.542z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,204 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'OpenSans';
|
||||||
|
src: url('/fonts/OpenSans-Regular.ttf');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'OpenSans';
|
||||||
|
src: url('/fonts/OpenSans-Italic.ttf');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'OpenSans';
|
||||||
|
src: url('/fonts/OpenSans-Bold.ttf');
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body{
|
||||||
|
background-color: #fff;
|
||||||
|
color: #212121;
|
||||||
|
font-family:'OpenSans', sans-serif, Arial;
|
||||||
|
font-size: 14px;
|
||||||
|
margin:0;
|
||||||
|
}
|
||||||
|
a{
|
||||||
|
color: #499DCE;
|
||||||
|
transition: color .2s;
|
||||||
|
}
|
||||||
|
a:hover{
|
||||||
|
color: #395D73;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 32px;
|
||||||
|
text-align:center;
|
||||||
|
margin:90px 0 0 0;
|
||||||
|
}
|
||||||
|
section{
|
||||||
|
margin: 15px auto;
|
||||||
|
width: 930px;
|
||||||
|
padding: 30px 0;
|
||||||
|
}
|
||||||
|
section p {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.title img {
|
||||||
|
width: 320px;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight:normal;
|
||||||
|
text-align:center;
|
||||||
|
margin:10px 0 95px 0;
|
||||||
|
}
|
||||||
|
.box-header {
|
||||||
|
text-align:left;
|
||||||
|
text-transform:uppercase;
|
||||||
|
border:1px solid #ededed;
|
||||||
|
margin:25px 0 0 0;
|
||||||
|
padding:12px 30px;
|
||||||
|
font-size:20px;
|
||||||
|
background:#fff;
|
||||||
|
}
|
||||||
|
.item{
|
||||||
|
background:#fff;
|
||||||
|
height: 191px;
|
||||||
|
border: 1px solid #ededed;
|
||||||
|
border-top:none;
|
||||||
|
}
|
||||||
|
.item:nth-child(odd) {
|
||||||
|
background-color:#fbfbfb;
|
||||||
|
}
|
||||||
|
.item img{
|
||||||
|
position: absolute;
|
||||||
|
margin: 30px;
|
||||||
|
width: 128px;
|
||||||
|
height: 128px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
.details {
|
||||||
|
float:left;
|
||||||
|
width:180px;
|
||||||
|
height: 128px;
|
||||||
|
padding: 20px 30px 20px 188px;
|
||||||
|
}
|
||||||
|
.details h3 {
|
||||||
|
font-size:18px;
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
||||||
|
.details p {
|
||||||
|
padding:0;
|
||||||
|
margin:18px 0;
|
||||||
|
}
|
||||||
|
.viewers {
|
||||||
|
float:right;
|
||||||
|
text-align:center;
|
||||||
|
width: 120px;
|
||||||
|
margin-top: 25px;
|
||||||
|
padding-right: 30px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
display:block;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 36px;
|
||||||
|
}
|
||||||
|
.btn:first-child {
|
||||||
|
position: relative;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius:4px;
|
||||||
|
background-color: #499DCE;
|
||||||
|
background: linear-gradient(90deg, #5aaad8, #4a9ecf);
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.btn:first-child:hover{
|
||||||
|
background: #395D73;
|
||||||
|
}
|
||||||
|
footer{
|
||||||
|
width:100%;
|
||||||
|
border-top:1px solid #ededed;
|
||||||
|
text-align:center;
|
||||||
|
color:#d3d3d3;
|
||||||
|
padding-top:10px;
|
||||||
|
font-size:12px;
|
||||||
|
}
|
||||||
|
footer img{
|
||||||
|
width: 252px;
|
||||||
|
height: auto;
|
||||||
|
margin: 20px 0 10px 0;
|
||||||
|
}
|
||||||
|
footer p {
|
||||||
|
margin-top:0;
|
||||||
|
}
|
||||||
|
footer a {
|
||||||
|
color: #787878;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* body background image */
|
||||||
|
body {
|
||||||
|
background-repeat:no-repeat !important;
|
||||||
|
background-size: contain !important;
|
||||||
|
background-image: url(/images/header-map-640px.png);
|
||||||
|
}
|
||||||
|
@media only screen and (min-width: 641px) {
|
||||||
|
body {
|
||||||
|
background-image: url(/images/header-map-1280px.png);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media only screen and (min-width: 1281px) {
|
||||||
|
body {
|
||||||
|
background-image: url(/images/header-map-1600px.png);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media only screen and (min-width: 1601px) {
|
||||||
|
body {
|
||||||
|
background-image: url(/images/header-map-2560px.png);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 950px) {
|
||||||
|
section{
|
||||||
|
margin: 0;
|
||||||
|
width: 96%;
|
||||||
|
padding: 2%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.title{
|
||||||
|
margin: 25px 0 0 0;
|
||||||
|
}
|
||||||
|
.title img{
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
.subtitle{
|
||||||
|
font-size: 20px;
|
||||||
|
margin: 0 0 35px 0;
|
||||||
|
}
|
||||||
|
.item{
|
||||||
|
height: 245px;
|
||||||
|
}
|
||||||
|
.viewers{
|
||||||
|
float: left;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
margin-left: 30px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.viewers a{
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.btn{
|
||||||
|
margin: 0 20px 0 0;
|
||||||
|
}
|
||||||
|
.btn:first-child{
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>TileServerGL</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/index.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section>
|
||||||
|
<h1 class="title"><img src="/images/logo.png" alt="TileServerGL" /></h1>
|
||||||
|
<h2 class="subtitle">Vector and raster maps with GL styles</h2>
|
||||||
|
<p>
|
||||||
|
An open-source map server made for vector tiles, and able to render into raster tiles with MapBox GL Native engine on the server side.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
It provides maps to web and mobile applications. Supported are Mapbox GL JS, Android SDK, iOS SDK, Leaflet, OpenLayers, HighDPI/Retina, GIS via WMTS, etc.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For details see the project repository on GitHub:<br/>
|
||||||
|
<a href="https://github.com/maptiler/tileserver-gl">https://github.com/maptiler/tileserver-gl</a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<footer>
|
||||||
|
<a href="https://www.maptiler.com/" target="_blank"><img src="/images/maptiler-logo.svg" /></a>
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/maptiler/tileserver-gl" target="_blank">TileServer GL</a>
|
||||||
|
<a href="https://www.maptiler.com/" target="_blank">is an open-source project from MapTiler team.</a>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "tileserver-gl",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"description": "Map tile server for JSON GL styles - serverside generated raster tiles",
|
|
||||||
"main": "src/main.js",
|
|
||||||
"authors": [
|
|
||||||
"Petr Sloup <petr.sloup@klokantech.com>"
|
|
||||||
],
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/klokantech/tileserver-gl.git"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"async": "1.5.2",
|
|
||||||
"advanced-pool": "0.3.1",
|
|
||||||
"clone": "1.0.2",
|
|
||||||
"cors": "2.7.1",
|
|
||||||
"express": "4.13.4",
|
|
||||||
"mapbox-gl-native": "3.0.2-earcut",
|
|
||||||
"mbtiles": "0.8.2",
|
|
||||||
"morgan": "1.7.0",
|
|
||||||
"nomnom": "1.8.1",
|
|
||||||
"request": "2.69.0",
|
|
||||||
"sharp": "0.13.1",
|
|
||||||
"sphericalmercator": "1.0.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
xvfb-run --server-args="-screen 0 1024x768x24" node /usr/src/app/src/main.js -p 80 -c /data/config.json
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var opts = require('nomnom')
|
|
||||||
.option('config', {
|
|
||||||
abbr: 'c',
|
|
||||||
default: 'config.json',
|
|
||||||
help: 'Configuration file'
|
|
||||||
})
|
|
||||||
.option('port', {
|
|
||||||
abbr: 'p',
|
|
||||||
default: 8080,
|
|
||||||
help: 'Port'
|
|
||||||
})
|
|
||||||
.option('version', {
|
|
||||||
abbr: 'v',
|
|
||||||
flag: true,
|
|
||||||
help: 'Version info',
|
|
||||||
callback: function() {
|
|
||||||
return 'version ' + require('../package.json').version;
|
|
||||||
}
|
|
||||||
}).parse();
|
|
||||||
|
|
||||||
return require('./server')({
|
|
||||||
config: opts.config,
|
|
||||||
port: opts.port
|
|
||||||
});
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
var async = require('async'),
|
|
||||||
advancedPool = require('advanced-pool'),
|
|
||||||
crypto = require('crypto'),
|
|
||||||
fs = require('fs'),
|
|
||||||
path = require('path'),
|
|
||||||
util = require('util'),
|
|
||||||
zlib = require('zlib');
|
|
||||||
|
|
||||||
var clone = require('clone'),
|
|
||||||
express = require('express'),
|
|
||||||
mercator = new (require('sphericalmercator'))(),
|
|
||||||
mbgl = require('mapbox-gl-native'),
|
|
||||||
mbtiles = require('mbtiles'),
|
|
||||||
request = require('request'),
|
|
||||||
sharp = require('sharp');
|
|
||||||
|
|
||||||
var utils = require('./utils');
|
|
||||||
|
|
||||||
var FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+\.?\\d+)';
|
|
||||||
var SCALE_PATTERN = '@[23]x';
|
|
||||||
|
|
||||||
var getScale = function(scale) {
|
|
||||||
return (scale || '@1x').slice(1, 2) | 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
mbgl.on('message', function(e) {
|
|
||||||
if (e.severity == 'WARNING' || e.severity == 'ERROR') {
|
|
||||||
console.log('mbgl:', e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = function(maps, options, prefix) {
|
|
||||||
var app = express().disable('x-powered-by'),
|
|
||||||
domains = options.domains,
|
|
||||||
tilePath = '/{z}/{x}/{y}.{format}';
|
|
||||||
|
|
||||||
var rootPath = path.join(process.cwd(), options.root || '');
|
|
||||||
|
|
||||||
var styleUrl = options.style;
|
|
||||||
var map = {
|
|
||||||
renderers: [],
|
|
||||||
sources: {},
|
|
||||||
tileJSON: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
var styleJSON;
|
|
||||||
var createPool = function(ratio, min, max) {
|
|
||||||
var createRenderer = function(ratio, createCallback) {
|
|
||||||
var renderer = new mbgl.Map({
|
|
||||||
ratio: ratio,
|
|
||||||
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) {
|
|
||||||
callback(err, { data: data });
|
|
||||||
});
|
|
||||||
} else if (protocol == 'mbtiles') {
|
|
||||||
var parts = req.url.split('/');
|
|
||||||
var source = map.sources[parts[2]];
|
|
||||||
var z = parts[3] | 0,
|
|
||||||
x = parts[4] | 0,
|
|
||||||
y = parts[5].split('.')[0] | 0;
|
|
||||||
source.getTile(z, x, y, function(err, data, headers) {
|
|
||||||
if (err) {
|
|
||||||
//console.log('MBTiles error, serving empty', err);
|
|
||||||
callback(null, { data: new Buffer(0) });
|
|
||||||
} else {
|
|
||||||
var response = {};
|
|
||||||
|
|
||||||
if (headers['Last-Modified']) {
|
|
||||||
response.modified = new Date(headers['Last-Modified']);
|
|
||||||
}
|
|
||||||
if (headers['ETag']) {
|
|
||||||
response.etag = headers['ETag'];
|
|
||||||
}
|
|
||||||
|
|
||||||
response.data = zlib.unzipSync(data);
|
|
||||||
|
|
||||||
callback(null, response);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (protocol == 'http' || protocol == 'https') {
|
|
||||||
request({
|
|
||||||
url: req.url,
|
|
||||||
encoding: null,
|
|
||||||
gzip: true
|
|
||||||
}, function(err, res, body) {
|
|
||||||
if (err) {
|
|
||||||
//console.log('HTTP tile error', err);
|
|
||||||
callback(null, { data: new Buffer(0) });
|
|
||||||
} else if (res.statusCode == 200) {
|
|
||||||
var response = {};
|
|
||||||
|
|
||||||
if (res.headers.modified) {
|
|
||||||
response.modified = new Date(res.headers.modified);
|
|
||||||
}
|
|
||||||
if (res.headers.expires) {
|
|
||||||
response.expires = new Date(res.headers.expires);
|
|
||||||
}
|
|
||||||
if (res.headers.etag) {
|
|
||||||
response.etag = res.headers.etag;
|
|
||||||
}
|
|
||||||
|
|
||||||
response.data = body;
|
|
||||||
|
|
||||||
callback(null, response);
|
|
||||||
} else {
|
|
||||||
//console.log('HTTP error', JSON.parse(body).message);
|
|
||||||
callback(null, { data: new Buffer(0) });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
renderer.load(styleJSON);
|
|
||||||
createCallback(null, renderer);
|
|
||||||
};
|
|
||||||
return new advancedPool.Pool({
|
|
||||||
min: min,
|
|
||||||
max: max,
|
|
||||||
create: createRenderer.bind(null, ratio),
|
|
||||||
destroy: function(renderer) {
|
|
||||||
renderer.release();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
styleJSON = require(path.join(rootPath, styleUrl));
|
|
||||||
|
|
||||||
map.tileJSON = {
|
|
||||||
'tilejson': '2.0.0',
|
|
||||||
'name': styleJSON.name,
|
|
||||||
'basename': prefix.substr(1),
|
|
||||||
'minzoom': 0,
|
|
||||||
'maxzoom': 20,
|
|
||||||
'bounds': [-180, -85.0511, 180, 85.0511],
|
|
||||||
'format': 'png',
|
|
||||||
'type': 'baselayer'
|
|
||||||
};
|
|
||||||
Object.assign(map.tileJSON, options.options || {});
|
|
||||||
|
|
||||||
var queue = [];
|
|
||||||
Object.keys(styleJSON.sources).forEach(function(name) {
|
|
||||||
var source = styleJSON.sources[name];
|
|
||||||
var url = source.url;
|
|
||||||
if (url.lastIndexOf('mbtiles:', 0) === 0) {
|
|
||||||
// found mbtiles source, replace with info from local file
|
|
||||||
delete source.url;
|
|
||||||
|
|
||||||
queue.push(function(callback) {
|
|
||||||
var mbtilesUrl = url.substring('mbtiles://'.length);
|
|
||||||
map.sources[name] = new mbtiles(path.join(rootPath, mbtilesUrl), 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')
|
|
||||||
];
|
|
||||||
callback(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async.parallel(queue, function(err, results) {
|
|
||||||
// TODO: make pool sizes configurable
|
|
||||||
map.renderers[1] = createPool(1, 4, 16);
|
|
||||||
map.renderers[2] = createPool(2, 2, 8);
|
|
||||||
map.renderers[3] = createPool(3, 2, 4);
|
|
||||||
});
|
|
||||||
|
|
||||||
maps[prefix] = map;
|
|
||||||
|
|
||||||
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 respondImage = function(z, lon, lat, width, height, scale, format, res, next) {
|
|
||||||
if (format == 'png' || format == 'webp') {
|
|
||||||
} else if (format == 'jpg' || format == 'jpeg') {
|
|
||||||
format = 'jpeg';
|
|
||||||
} else {
|
|
||||||
return res.status(404).send('Invalid format');
|
|
||||||
}
|
|
||||||
|
|
||||||
var pool = map.renderers[scale];
|
|
||||||
pool.acquire(function(err, renderer) {
|
|
||||||
var mbglZ = Math.max(0, z - 1);
|
|
||||||
var params = {
|
|
||||||
zoom: mbglZ,
|
|
||||||
center: [lon, lat],
|
|
||||||
width: width,
|
|
||||||
height: height
|
|
||||||
};
|
|
||||||
if (z == 0) {
|
|
||||||
params.width *= 2;
|
|
||||||
params.height *= 2;
|
|
||||||
}
|
|
||||||
renderer.render(params, function(err, data) {
|
|
||||||
pool.release(renderer);
|
|
||||||
if (err) console.log(err);
|
|
||||||
|
|
||||||
var image = sharp(data, {
|
|
||||||
raw: {
|
|
||||||
width: params.width * scale,
|
|
||||||
height: params.height * scale,
|
|
||||||
channels: 4
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (z == 0) {
|
|
||||||
// HACK: when serving zoom 0, resize the 0 tile from 512 to 256
|
|
||||||
image.resize(width * scale, height * scale);
|
|
||||||
}
|
|
||||||
|
|
||||||
image.toFormat(format)
|
|
||||||
.compressionLevel(9)
|
|
||||||
.toBuffer(function(err, buffer, info) {
|
|
||||||
if (!buffer) {
|
|
||||||
return res.status(404).send('Not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
var md5 = crypto.createHash('md5').update(buffer).digest('base64');
|
|
||||||
res.set({
|
|
||||||
'content-md5': md5,
|
|
||||||
'content-type': 'image/' + format
|
|
||||||
});
|
|
||||||
return res.status(200).send(buffer);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
app.get(tilePattern, function(req, res, next) {
|
|
||||||
var z = req.params.z | 0,
|
|
||||||
x = req.params.x | 0,
|
|
||||||
y = req.params.y | 0,
|
|
||||||
scale = getScale(req.params.scale),
|
|
||||||
format = req.params.format;
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
var staticPattern =
|
|
||||||
'/static/%s:scale(' + SCALE_PATTERN + ')?\.:format([\\w\\.]+)';
|
|
||||||
|
|
||||||
var centerPattern =
|
|
||||||
util.format(':lon(%s),:lat(%s),:z(\\d+)/:width(\\d+)x:height(\\d+)',
|
|
||||||
FLOAT_PATTERN, FLOAT_PATTERN);
|
|
||||||
|
|
||||||
app.get(util.format(staticPattern, centerPattern), function(req, res, next) {
|
|
||||||
var z = req.params.z | 0,
|
|
||||||
x = +req.params.lon,
|
|
||||||
y = +req.params.lat,
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
var boundsPattern =
|
|
||||||
util.format(':minx(%s),:miny(%s),:maxx(%s),:maxy(%s)/:z(\\d+)',
|
|
||||||
FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN);
|
|
||||||
|
|
||||||
app.get(util.format(staticPattern, boundsPattern), function(req, res, next) {
|
|
||||||
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,
|
|
||||||
scale = getScale(req.params.scale),
|
|
||||||
format = req.params.format;
|
|
||||||
return respondImage(z, x, y, 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);
|
|
||||||
|
|
||||||
return res.send(info);
|
|
||||||
});
|
|
||||||
|
|
||||||
return app;
|
|
||||||
};
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
var crypto = require('crypto'),
|
|
||||||
path = require('path');
|
|
||||||
|
|
||||||
var clone = require('clone'),
|
|
||||||
express = require('express'),
|
|
||||||
mbtiles = require('mbtiles');
|
|
||||||
|
|
||||||
var utils = require('./utils');
|
|
||||||
|
|
||||||
module.exports = function(maps, options, prefix) {
|
|
||||||
var app = express().disable('x-powered-by'),
|
|
||||||
domains = options.domains,
|
|
||||||
tilePath = '/{z}/{x}/{y}.pbf';
|
|
||||||
|
|
||||||
var rootPath = path.join(process.cwd(), options.root || '');
|
|
||||||
|
|
||||||
var mbtilesPath = options.mbtiles;
|
|
||||||
var map = {
|
|
||||||
tileJSON: {}
|
|
||||||
};
|
|
||||||
maps[prefix] = map;
|
|
||||||
|
|
||||||
var source = new mbtiles(path.join(rootPath, mbtilesPath), function(err) {
|
|
||||||
source.getInfo(function(err, info) {
|
|
||||||
map.tileJSON['name'] = prefix.substr(1);
|
|
||||||
|
|
||||||
Object.assign(map.tileJSON, info);
|
|
||||||
|
|
||||||
map.tileJSON['tilejson'] = '2.0.0';
|
|
||||||
map.tileJSON['basename'] = prefix.substr(1);
|
|
||||||
map.tileJSON['format'] = 'pbf';
|
|
||||||
|
|
||||||
Object.assign(map.tileJSON, options.options || {});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var tilePattern = tilePath
|
|
||||||
.replace('{z}', ':z(\\d+)')
|
|
||||||
.replace('{x}', ':x(\\d+)')
|
|
||||||
.replace('{y}', ':y(\\d+)');
|
|
||||||
|
|
||||||
var getTile = function(z, x, y, callback) {
|
|
||||||
source.getTile(z, x, y, function(err, data, headers) {
|
|
||||||
if (err) {
|
|
||||||
callback(err);
|
|
||||||
} else {
|
|
||||||
var md5 = crypto.createHash('md5').update(data).digest('base64');
|
|
||||||
headers['content-md5'] = md5;
|
|
||||||
headers['content-type'] = 'application/x-protobuf';
|
|
||||||
headers['content-encoding'] = 'gzip';
|
|
||||||
|
|
||||||
callback(null, data, headers);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
app.get(tilePattern, function(req, res, next) {
|
|
||||||
var z = req.params.z | 0,
|
|
||||||
x = req.params.x | 0,
|
|
||||||
y = req.params.y | 0;
|
|
||||||
return getTile(z, x, y, function(err, data, headers) {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
if (headers) {
|
|
||||||
res.set(headers);
|
|
||||||
}
|
|
||||||
if (data == null) {
|
|
||||||
return res.status(404).send('Not found');
|
|
||||||
} else {
|
|
||||||
return res.status(200).send(data);
|
|
||||||
}
|
|
||||||
}, res, next);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/index.json', function(req, res, next) {
|
|
||||||
var info = clone(map.tileJSON);
|
|
||||||
|
|
||||||
info.tiles = utils.getTileUrls(req.protocol, domains, req.headers.host,
|
|
||||||
prefix, tilePath, info.format,
|
|
||||||
req.query.key);
|
|
||||||
|
|
||||||
return res.send(info);
|
|
||||||
});
|
|
||||||
|
|
||||||
return app;
|
|
||||||
};
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
process.env.UV_THREADPOOL_SIZE =
|
|
||||||
Math.ceil(Math.max(4, require('os').cpus().length * 1.5));
|
|
||||||
|
|
||||||
var fs = require('fs'),
|
|
||||||
path = require('path');
|
|
||||||
|
|
||||||
var async = require('async'),
|
|
||||||
clone = require('clone'),
|
|
||||||
cors = require('cors'),
|
|
||||||
express = require('express'),
|
|
||||||
morgan = require('morgan');
|
|
||||||
|
|
||||||
var serve_raster = require('./serve_raster'),
|
|
||||||
serve_vector = require('./serve_vector'),
|
|
||||||
utils = require('./utils');
|
|
||||||
|
|
||||||
module.exports = function(opts, callback) {
|
|
||||||
var app = express().disable('x-powered-by'),
|
|
||||||
maps = {};
|
|
||||||
|
|
||||||
app.enable('trust proxy');
|
|
||||||
|
|
||||||
callback = callback || function() {};
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config[prefix].style) {
|
|
||||||
app.use(prefix, serve_raster(maps, config[prefix], prefix));
|
|
||||||
} else {
|
|
||||||
app.use(prefix, serve_vector(maps, config[prefix], prefix));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// serve index.html on the root
|
|
||||||
app.use('/', express.static(path.join(__dirname, '../public')));
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
info.tiles = utils.getTileUrls(
|
|
||||||
req.protocol, config[prefix].domains, req.headers.host,
|
|
||||||
prefix, '/{z}/{x}/{y}.{format}', info.format, req.query.key);
|
|
||||||
|
|
||||||
callback(null, info);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return async.parallel(queue, function(err, results) {
|
|
||||||
return res.send(results);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.listen(process.env.PORT || opts.port, function() {
|
|
||||||
console.log('Listening at http://%s:%d/',
|
|
||||||
this.address().address, this.address().port);
|
|
||||||
|
|
||||||
return callback();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports.getTileUrls = function(
|
|
||||||
protocol, domains, host, path, tilePath, format, key) {
|
|
||||||
|
|
||||||
if (domains) {
|
|
||||||
if (domains.constructor === String && domains.length > 0) {
|
|
||||||
domains = domains.split(',');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!domains || domains.length == 0) {
|
|
||||||
domains = [host];
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
return uris;
|
|
||||||
};
|
|
||||||