Merge branch 'master' into healthcheck

This commit is contained in:
zstadler 2022-11-03 09:57:34 +02:00 committed by GitHub
commit 8c96604d55
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 12465 additions and 1461 deletions

View file

@ -1,7 +1,7 @@
.git *
docs/_build !src
node_modules !public
test_data !test
light !package.json
config.json !package-lock.json
*.mbtiles !docker-entrypoint.sh

View file

@ -1,29 +1,84 @@
FROM node:10-stretch FROM ubuntu:focal AS builder
ENV NODE_ENV="production" ENV NODE_ENV="production"
VOLUME /data
WORKDIR /data
EXPOSE 80
ENTRYPOINT ["/bin/bash", "/usr/src/app/run.sh"]
HEALTHCHECK CMD node /usr/src/app/src/healthcheck.js
RUN apt-get -qq update \ RUN set -ex; \
&& DEBIAN_FRONTEND=noninteractive apt-get -y install \ export DEBIAN_FRONTEND=noninteractive; \
apt-transport-https \ apt-get -qq update; \
curl \ apt-get -y --no-install-recommends install \
unzip \ build-essential \
build-essential \ ca-certificates \
python \ wget \
libcairo2-dev \ pkg-config \
libgles2-mesa-dev \ xvfb \
libgbm-dev \ libglfw3-dev \
libllvm3.9 \ libuv1-dev \
libprotobuf-dev \ libjpeg-turbo8 \
libxxf86vm-dev \ libicu66 \
xvfb \ libcairo2-dev \
x11-utils \ libpango1.0-dev \
&& apt-get clean libjpeg-dev \
libgif-dev \
librsvg2-dev \
libcurl4-openssl-dev \
libpixman-1-dev; \
wget -qO- https://deb.nodesource.com/setup_16.x | bash; \
apt-get install -y nodejs; \
apt-get -y remove wget; \
apt-get -y --purge autoremove; \
apt-get clean; \
rm -rf /var/lib/apt/lists/*;
RUN mkdir -p /usr/src/app RUN mkdir -p /usr/src/app
COPY / /usr/src/app COPY package* /usr/src/app/
RUN cd /usr/src/app && npm install --production
RUN cd /usr/src/app && npm ci --omit=dev
FROM ubuntu:focal AS final
ENV \
NODE_ENV="production" \
CHOKIDAR_USEPOLLING=1 \
CHOKIDAR_INTERVAL=500
RUN set -ex; \
export DEBIAN_FRONTEND=noninteractive; \
groupadd -r node; \
useradd -r -g node node; \
apt-get -qq update; \
apt-get -y --no-install-recommends install \
ca-certificates \
wget \
xvfb \
libglfw3 \
libuv1 \
libjpeg-turbo8 \
libicu66 \
libcairo2 \
libgif7 \
libopengl0 \
libpixman-1-0 \
libcurl4 \
librsvg2-2 \
libpango1.0; \
wget -qO- https://deb.nodesource.com/setup_16.x | bash; \
apt-get install -y nodejs; \
apt-get -y remove wget; \
apt-get -y --purge autoremove; \
apt-get clean; \
rm -rf /var/lib/apt/lists/*;
COPY --from=builder /usr/src/app /usr/src/app
COPY . /usr/src/app
VOLUME /data
WORKDIR /data
EXPOSE 80
USER node:node
ENTRYPOINT ["/usr/src/app/docker-entrypoint.sh"]
HEALTHCHECK CMD node /usr/src/app/src/healthcheck.js

View file

@ -1,12 +1,34 @@
FROM node:10-stretch FROM ubuntu:focal
ENV \
NODE_ENV="production" \
CHOKIDAR_USEPOLLING=1 \
CHOKIDAR_INTERVAL=500
RUN set -ex; \
export DEBIAN_FRONTEND=noninteractive; \
groupadd -r node; \
useradd -r -g node node; \
apt-get -qq update; \
apt-get -y --no-install-recommends install \
ca-certificates \
wget; \
wget -qO- https://deb.nodesource.com/setup_16.x | bash; \
apt-get install -y nodejs; \
apt-get -y remove wget; \
apt-get -y --purge autoremove; \
apt-get clean; \
rm -rf /var/lib/apt/lists/*;
ENV NODE_ENV="production"
EXPOSE 80 EXPOSE 80
VOLUME /data VOLUME /data
WORKDIR /data WORKDIR /data
ENTRYPOINT ["node", "/usr/src/app/", "-p", "80"]
HEALTHCHECK CMD node /usr/src/app/src/healthcheck.js ENTRYPOINT ["/usr/src/app/docker-entrypoint.sh"]
RUN mkdir -p /usr/src/app RUN mkdir -p /usr/src/app
COPY / /usr/src/app COPY / /usr/src/app
RUN cd /usr/src/app && npm install --production RUN cd /usr/src/app && npm install --omit=dev
RUN ["chmod", "+x", "/usr/src/app/docker-entrypoint.sh"]
USER node:node
HEALTHCHECK CMD node /usr/src/app/src/healthcheck.js

View file

@ -2,33 +2,43 @@
# Simply run "docker build -f Dockerfile_test ." # Simply run "docker build -f Dockerfile_test ."
# WARNING: sometimes it fails with a core dumped exception # WARNING: sometimes it fails with a core dumped exception
FROM node:10-stretch FROM ubuntu:focal
RUN apt-get -qq update \ ENV NODE_ENV="development"
&& DEBIAN_FRONTEND=noninteractive apt-get -y install \
apt-transport-https \ RUN set -ex; \
curl \ export DEBIAN_FRONTEND=noninteractive; \
unzip \ apt-get -qq update; \
build-essential \ apt-get -y --no-install-recommends install \
python \ unzip \
libcairo2-dev \ build-essential \
libgles2-mesa-dev \ ca-certificates \
libgbm-dev \ wget \
libllvm3.9 \ pkg-config \
libprotobuf-dev \ xvfb \
libxxf86vm-dev \ libglfw3-dev \
xvfb \ libuv1-dev \
&& apt-get clean libjpeg-turbo8 \
libicu66 \
libcairo2-dev \
libpango1.0-dev \
libjpeg-dev \
libgif-dev \
librsvg2-dev \
libcurl4-openssl-dev \
libpixman-1-dev; \
wget -qO- https://deb.nodesource.com/setup_16.x | bash; \
apt-get install -y nodejs; \
apt-get clean;
RUN mkdir -p /usr/src/app RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app WORKDIR /usr/src/app
RUN wget -O test_data.zip https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip RUN wget -O test_data.zip https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip; \
RUN unzip -q test_data.zip -d test_data unzip -q test_data.zip -d test_data
ENV NODE_ENV="test"
COPY package.json . COPY package.json .
RUN npm install RUN npm install
COPY / . COPY / .
RUN xvfb-run --server-args="-screen 0 1024x768x24" npm test RUN xvfb-run --server-args="-screen 0 1024x768x24" npm test

View file

@ -3,4 +3,11 @@
- Update version in `package.json` - Update version in `package.json`
- `git tag vx.x.x` - `git tag vx.x.x`
- `git push --tags` - `git push --tags`
- `node publish.js` (publishes packages to npm) - `docker buildx build --platform linux/amd64 -t maptiler/tileserver-gl:latest -t maptiler/tileserver-gl:[version] .`
- `docker push maptiler/tileserver-gl --all-tags`
- `npm publish --access public` or `node publish.js`
- `node publish.js --no-publish`
- `cd light`
- `docker buildx build --platform linux/amd64 -t maptiler/tileserver-gl-light:latest -t maptiler/tileserver-gl-light:[version] .`
- `docker push maptiler/tileserver-gl-light --all-tags`
- `npm publish --access public`

View file

@ -3,46 +3,87 @@
# TileServer GL # TileServer GL
[![Build Status](https://travis-ci.org/maptiler/tileserver-gl.svg?branch=master)](https://travis-ci.org/maptiler/tileserver-gl) [![Build Status](https://travis-ci.org/maptiler/tileserver-gl.svg?branch=master)](https://travis-ci.org/maptiler/tileserver-gl)
[![Docker Hub](https://img.shields.io/badge/docker-hub-blue.svg)](https://hub.docker.com/r/klokantech/tileserver-gl/) [![Docker Hub](https://img.shields.io/badge/docker-hub-blue.svg)](https://hub.docker.com/r/maptiler/tileserver-gl/)
Vector and raster maps with GL styles. Server side rendering by Mapbox GL Native. Map tile server for Mapbox GL JS, Android, iOS, Leaflet, OpenLayers, GIS via WMTS, etc. Vector and raster maps with GL styles. Server-side rendering by MapLibre GL Native. Map tile server for MapLibre GL JS, Android, iOS, Leaflet, OpenLayers, GIS via WMTS, etc.
## Get Started Download vector tiles from [OpenMapTiles](https://data.maptiler.com/downloads/planet/).
## Getting Started with Node
Make sure you have Node.js version **10** installed (running `node -v` it should output something like `v10.17.0`). Make sure you have Node.js version **14.20.0** or above installed. Node 16 is recommended. (running `node -v` it should output something like `v16.x.x`). Running without docker requires [Native dependencies](https://tileserver.readthedocs.io/en/latest/installation.html#npm) to be installed first.
Install `tileserver-gl` with server-side raster rendering of vector tiles with npm Install `tileserver-gl` with server-side raster rendering of vector tiles with npm.
```bash ```bash
npm install -g tileserver-gl npm install -g tileserver-gl
``` ```
Now download vector tiles from [OpenMapTiles](https://openmaptiles.org/downloads/). Once installed, you can use it like the following examples.
using a mbtiles file
```bash ```bash
curl -o zurich_switzerland.mbtiles https://[GET-YOUR-LINK]/extracts/zurich_switzerland.mbtiles wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/zurich_switzerland.mbtiles
tileserver-gl --mbtiles zurich_switzerland.mbtiles
[in your browser, visit http://[server ip]:8080]
``` ```
Start `tileserver-gl` with the downloaded vector tiles. using a config.json + style + mbtiles file
```bash ```bash
tileserver-gl zurich_switzerland.mbtiles wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip
unzip test_data.zip
tileserver-gl
[in your browser, visit http://[server ip]:8080]
``` ```
Alternatively, you can use the `tileserver-gl-light` package instead, which is pure javascript (does not have any native dependencies) and can run anywhere, but does not contain rasterization on the server side made with MapBox GL Native. Alternatively, you can use the `tileserver-gl-light` npm package instead, which is pure javascript, does not have any native dependencies, and can run anywhere, but does not contain rasterization on the server side made with Maplibre GL Native.
## Using Docker ## Getting Started with Docker
An alternative to npm to start the packed software easier is to install [Docker](http://www.docker.com/) on your computer and then run in the directory with the downloaded MBTiles the command: An alternative to npm to start the packed software easier is to install [Docker](https://www.docker.com/) on your computer and then run from the tileserver-gl directory
Example using a mbtiles file
```bash ```bash
docker run --rm -it -v $(pwd):/data -p 8080:80 klokantech/tileserver-gl wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/zurich_switzerland.mbtiles
docker run --rm -it -v $(pwd):/data -p 8080:80 maptiler/tileserver-gl --mbtiles zurich_switzerland.mbtiles
[in your browser, visit http://[server ip]:8080]
``` ```
This will download and start a ready to use container on your computer and the maps are going to be available in webbrowser on localhost:8080. Example using a config.json + style + mbtiles file
```bash
wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip
unzip test_data.zip
docker run --rm -it -v $(pwd):/data -p 8080:80 maptiler/tileserver-gl
[in your browser, visit http://[server ip]:8080]
```
On laptop you can use [Docker Kitematic](https://kitematic.com/) and search "tileserver-gl" and run it, then drop in the 'data' folder the MBTiles. Example using a different path
```bash
docker run --rm -it -v /your/local/config/path:/data -p 8080:80 maptiler/tileserver-gl
```
replace '/your/local/config/path' with the path to your config file
Alternatively, you can use the `maptiler/tileserver-gl-light` docker image instead, which is pure javascript, does not have any native dependencies, and can run anywhere, but does not contain rasterization on the server side made with Maplibre GL Native.
## Getting Started with Linux cli
Test from command line
```bash
wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip
unzip -q test_data.zip -d test_data
xvfb-run --server-args="-screen 0 1024x768x24" npm test
```
Run from command line
```bash
xvfb-run --server-args="-screen 0 1024x768x24" node .
```
## Documentation ## Documentation
You can read full documentation of this project at http://tileserver.readthedocs.io/. You can read the full documentation of this project at https://tileserver.readthedocs.io/.
## Alternative
Discover MapTiler Server if you need a [map server with easy setup and user-friendly interface](https://www.maptiler.com/server/).

View file

@ -1,6 +1,6 @@
# TileServer GL light # TileServer GL light
[![Build Status](https://travis-ci.org/maptiler/tileserver-gl.svg?branch=master)](https://travis-ci.org/maptiler/tileserver-gl) [![Build Status](https://travis-ci.org/maptiler/tileserver-gl.svg?branch=master)](https://travis-ci.org/maptiler/tileserver-gl)
[![Docker Hub](https://img.shields.io/badge/docker-hub-blue.svg)](https://hub.docker.com/r/klokantech/tileserver-gl/) [![Docker Hub](https://img.shields.io/badge/docker-hub-blue.svg)](https://hub.docker.com/r/maptiler/tileserver-gl/)
Vector maps with GL styles. Map tile server for Mapbox Android, iOS, GL JS, Leaflet, OpenLayers, etc. without server side rendering. Vector maps with GL styles. Map tile server for Mapbox Android, iOS, GL JS, Leaflet, OpenLayers, etc. without server side rendering.
@ -11,7 +11,25 @@ Then you can simply run `tileserver-gl-light zurich_switzerland.mbtiles` to star
See also `tileserver-gl` which contains server side rendering. See also `tileserver-gl` which contains server side rendering.
Prepared vector tiles can be downloaded from [OpenMapTiles](https://openmaptiles.org/downloads/). Prepared vector tiles can be downloaded from [OpenMapTiles.com](https://openmaptiles.com/downloads/planet/).
## Building docker image
You can build TileServer GL light image from source.
```
git clone https://github.com/maptiler/tileserver-gl.git
cd tileserver-gl
node publish.js --no-publish
cd light
docker build -t tileserver-gl-light .
```
[Download from OpenMapTiles.com](https://openmaptiles.com/downloads/planet/) or [create](https://github.com/openmaptiles/openmaptiles) your vector tile, and run following in directory contains your *.mbtiles.
```
docker run --rm -it -v $(pwd):/data -p 8000:80 tileserver-gl-light
```
## Documentation ## Documentation
You can read full documentation of this project at http://tileserver.readthedocs.io/. You can read full documentation of this project at https://tileserver.readthedocs.io/.

35
docker-entrypoint.sh Executable file
View file

@ -0,0 +1,35 @@
#!/bin/sh
set -e
handle() {
SIGNAL=$(( $? - 128 ))
echo "Caught signal ${SIGNAL}, stopping gracefully"
kill -s ${SIGNAL} $(pidof node) 2>/dev/null
}
trap handle INT TERM
refresh() {
SIGNAL=$(( $? - 128 ))
echo "Caught signal ${SIGNAL}, refreshing"
kill -s ${SIGNAL} $(pidof node) 2>/dev/null
}
trap refresh HUP
if ! which -- "${1}"; then
# first arg is not an executable
xvfb-run -a --server-args="-screen 0 1024x768x24" -- node /usr/src/app/ -p 80 "$@" &
# Wait exits immediately on signals which have traps set. Store return value and wait
# again for all jobs to actually complete before continuing.
wait $! || RETVAL=$?
while [ ${RETVAL} = 129 ] ; do
# Refressh signal HUP received. Continue waiting for signals.
wait $! || RETVAL=$?
done
wait
exit ${RETVAL}
fi
exec "$@"

View file

@ -0,0 +1,35 @@
#!/bin/sh
set -e
handle() {
SIGNAL=$(( $? - 128 ))
echo "Caught signal ${SIGNAL}, stopping gracefully"
kill -s ${SIGNAL} $(pidof node) 2>/dev/null
}
trap handle INT TERM
refresh() {
SIGNAL=$(( $? - 128 ))
echo "Caught signal ${SIGNAL}, refreshing"
kill -s ${SIGNAL} $(pidof node) 2>/dev/null
}
trap refresh HUP
if ! which -- "${1}"; then
# first arg is not an executable
node /usr/src/app/ -p 80 "$@" &
# Wait exits immediately on signals which have traps set. Store return value and wait
# again for all jobs to actually complete before continuing.
wait $! || RETVAL=$?
while [ ${RETVAL} = 129 ] ; do
# Refressh signal HUP received. Continue waiting for signals.
wait $! || RETVAL=$?
done
wait
exit ${RETVAL}
fi
exec "$@"

View file

@ -44,7 +44,7 @@ master_doc = 'index'
# General information about the project. # General information about the project.
project = u'TileServer GL' project = u'TileServer GL'
copyright = u'2016, Klokan Technologies GmbH' copyright = u'2022, MapTiler.com'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
@ -197,7 +197,7 @@ latex_elements = {
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
latex_documents = [ latex_documents = [
('index', 'TileServerGL.tex', u'TileServer GL Documentation', ('index', 'TileServerGL.tex', u'TileServer GL Documentation',
u'Klokan Technologies GmbH', 'manual'), u'MapTiler.com', 'manual'),
] ]
# The name of an image file (relative to this directory) to place at the top of # The name of an image file (relative to this directory) to place at the top of
@ -227,7 +227,7 @@ latex_documents = [
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [
('index', 'tileservergl', u'TileServer GL Documentation', ('index', 'tileservergl', u'TileServer GL Documentation',
[u'Klokan Technologies GmbH'], 1) [u'MapTiler.com'], 1)
] ]
# If true, show URL addresses after external links. # If true, show URL addresses after external links.
@ -241,7 +241,7 @@ man_pages = [
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
('index', 'TileServerGL', u'TileServer GL Documentation', ('index', 'TileServerGL', u'TileServer GL Documentation',
u'Klokan Technologies GmbH', 'TileServerGL', 'One line description of project.', u'MapTiler.com', 'TileServerGL', 'One line description of project.',
'Miscellaneous'), 'Miscellaneous'),
] ]

View file

@ -4,7 +4,9 @@ Configuration file
The configuration file defines the behavior of the application. It's a regular JSON file. The configuration file defines the behavior of the application. It's a regular JSON file.
Example:: Example:
.. code-block:: json
{ {
"options": { "options": {
@ -12,6 +14,7 @@ Example::
"root": "", "root": "",
"fonts": "fonts", "fonts": "fonts",
"sprites": "sprites", "sprites": "sprites",
"icons": "icons",
"styles": "styles", "styles": "styles",
"mbtiles": "" "mbtiles": ""
}, },
@ -27,7 +30,9 @@ Example::
"maxSize": 2048, "maxSize": 2048,
"pbfAlias": "pbf", "pbfAlias": "pbf",
"serveAllFonts": false, "serveAllFonts": false,
"serveAllStyles": false,
"serveStaticMaps": true, "serveStaticMaps": true,
"allowRemoteMarkerIcons": true,
"tileMargin": 0 "tileMargin": 0
}, },
"styles": { "styles": {
@ -99,9 +104,9 @@ Default is ``2048``.
``tileMargin`` ``tileMargin``
-------------- --------------
Additional image side length added during tile rendering that is cropped from the delivered tile. This is useful for resolving the issue with cropped labels, Additional image side length added during tile rendering that is cropped from the delivered tile. This is useful for resolving the issue with cropped labels,
but it does come with a performance degradation, because additional, adjacent vector tiles need to be loaded to genenrate a single tile. but it does come with a performance degradation, because additional, adjacent vector tiles need to be loaded to generate a single tile.
Default is ``0`` to disable this processing. Default is ``0`` to disable this processing.
``minRendererPoolSizes`` ``minRendererPoolSizes``
------------------------ ------------------------
@ -124,6 +129,13 @@ If you have plenty of memory, try setting these equal to or slightly above your
If you need to conserve memory, try lower values for scale factors that are less common. If you need to conserve memory, try lower values for scale factors that are less common.
Default is ``[16, 8, 4]``. Default is ``[16, 8, 4]``.
``serveAllStyles``
------------------------
If this option is enabled, all the styles from the ``paths.styles`` will be served. (No recursion, only ``.json`` files are used.)
The process will also watch for changes in this directory and remove/add more styles dynamically.
It is recommended to also use the ``serveAllFonts`` option when using this option.
``watermark`` ``watermark``
----------- -----------
@ -131,6 +143,13 @@ Optional string to be rendered into the raster tiles (and static maps) as waterm
Can be used for hard-coding attributions etc. (can also be specified per-style). Can be used for hard-coding attributions etc. (can also be specified per-style).
Not used by default. Not used by default.
``allowRemoteMarkerIcons``
--------------
Allows the rendering of marker icons fetched via http(s) hyperlinks.
For security reasons only allow this if you can control the origins from where the markers are fetched!
Default is to disallow fetching of icons from remote sources.
``styles`` ``styles``
========== ==========
@ -138,7 +157,7 @@ Each item in this object defines one style (map). It can have the following opti
* ``style`` -- name of the style json file [required] * ``style`` -- name of the style json file [required]
* ``serve_rendered`` -- whether to render the raster tiles for this style or not * ``serve_rendered`` -- whether to render the raster tiles for this style or not
* ``serve_data`` -- whether to allow acces to the original tiles, sprites and required glyphs * ``serve_data`` -- whether to allow access to the original tiles, sprites and required glyphs
* ``tilejson`` -- properties to add to the TileJSON created for the raster data * ``tilejson`` -- properties to add to the TileJSON created for the raster data
* ``format`` and ``bounds`` can be especially useful * ``format`` and ``bounds`` can be especially useful

View file

@ -2,7 +2,7 @@
Deployment Deployment
========== ==========
Typically - you should use nginx/lighttpd/apache on the frontend - and the tileserver-gl server is hidden behind it in production deployment. Typically, you should use nginx, lighttpd or apache on the frontend. The tileserver-gl server is hidden behind it in production deployment.
Caching Caching
======= =======
@ -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-Forwarded-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 ensure the URLs generated inside TileJSON, etc. are using the desired domain and protocol.

View file

@ -38,15 +38,41 @@ Static images
* ``path`` - comma-separated ``lng,lat``, pipe-separated pairs * ``path`` - comma-separated ``lng,lat``, pipe-separated pairs
* e.g. ``5.9,45.8|5.9,47.8|10.5,47.8|10.5,45.8|5.9,45.8`` * e.g. ``5.9,45.8|5.9,47.8|10.5,47.8|10.5,45.8|5.9,45.8``
* can be provided multiple times
* ``latlng`` - indicates the ``path`` coordinates are in ``lat,lng`` order rather than the usual ``lng,lat`` * ``latlng`` - indicates coordinates are in ``lat,lng`` order rather than the usual ``lng,lat``
* ``fill`` - color to use as the fill (e.g. ``red``, ``rgba(255,255,255,0.5)``, ``#0000ff``) * ``fill`` - color to use as the fill (e.g. ``red``, ``rgba(255,255,255,0.5)``, ``#0000ff``)
* ``stroke`` - color of the path stroke * ``stroke`` - color of the path stroke
* ``width`` - width of the stroke * ``width`` - width of the stroke
* ``linecap`` - rendering style for the start and end points of the path
* ``linejoin`` - rendering style for overlapping segments of the path with differing directions
* ``border`` - color of the optional border path stroke
* ``borderwidth`` - width of the border stroke (default 10% of width)
* ``marker`` - Marker in format ``lng,lat|iconPath|option|option|...``
* Will be rendered with the bottom center at the provided location
* ``lng,lat`` and ``iconPath`` are mandatory and icons won't be rendered without them
* ``iconPath`` is either a link to an image served via http(s) or a path to a file relative to the configured icon path
* ``option`` must adhere to the format ``optionName:optionValue`` and supports the following names
* ``scale`` - Factor to scale image by
* e.g. ``0.5`` - Scales the image to half it's original size
* ``offset`` - Image offset as positive or negative pixel value in format ``[offsetX],[offsetY]``
* scales with ``scale`` parameter since image placement is relative to it's size
* e.g. ``2,-4`` - Image will be moved 2 pixel to the right and 4 pixel in the upwards direction from the provided location
* e.g. ``5.9,45.8|marker-start.svg|scale:0.5|offset:2,-4``
* can be provided multiple times
* ``padding`` - "percentage" padding for fitted endpoints (area-based and path autofit) * ``padding`` - "percentage" padding for fitted endpoints (area-based and path autofit)
* value of ``0.1`` means "add 10% size to each side to make sure the area of interest is nicely visible" * value of ``0.1`` means "add 10% size to each side to make sure the area of interest is nicely visible"
* ``maxzoom`` - Maximum zoom level (only for auto endpoint where zoom level is calculated and not provided)
* You can also use (experimental) ``/styles/{id}/static/raw/...`` endpoints with raw spherical mercator coordinates (EPSG:3857) instead of WGS84. * You can also use (experimental) ``/styles/{id}/static/raw/...`` endpoints with raw spherical mercator coordinates (EPSG:3857) instead of WGS84.
* The static images are not available in the ``tileserver-gl-light`` version. * The static images are not available in the ``tileserver-gl-light`` version.

View file

@ -7,12 +7,12 @@ Docker
When running docker image, no special installation is needed -- the docker will automatically download the image if not present. When running docker image, no special installation is needed -- the docker will automatically download the image if not present.
Just run ``docker run --rm -it -v $(pwd):/data -p 8080:80 klokantech/tileserver-gl``. Just run ``docker run --rm -it -v $(pwd):/data -p 8080:80 maptiler/tileserver-gl``.
Additional options (see :doc:`/usage`) can be passed to the TileServer GL by appending them to the end of this command. You can, for example, do the following: Additional options (see :doc:`/usage`) can be passed to the TileServer GL by appending them to the end of this command. You can, for example, do the following:
* ``docker run ... klokantech/tileserver-gl --mbtiles my-tiles.mbtiles`` -- explicitly specify which mbtiles to use (if you have more in the folder) * ``docker run ... maptiler/tileserver-gl --mbtiles my-tiles.mbtiles`` -- explicitly specify which mbtiles to use (if you have more in the folder)
* ``docker run ... klokantech/tileserver-gl --verbose`` -- to see the default config created automatically * ``docker run ... maptiler/tileserver-gl --verbose`` -- to see the default config created automatically
npm npm
=== ===
@ -26,11 +26,30 @@ Native dependencies
There are some native dependencies that you need to make sure are installed if you plan to run the TileServer GL natively without docker. There are some native dependencies that you need to make sure are installed if you plan to run the TileServer GL natively without docker.
The precise package names you need to install may differ on various platforms. The precise package names you need to install may differ on various platforms.
These are required on Debian 9: These are required on Debian 11:
* ``build-essential`` * ``libgles2-mesa``
* ``libcairo2-dev`` * ``libegl1``
* ``libprotobuf-dev`` * ``xvfb``
* ``xauth``
* ``libopengl0``
* ``libcurl4``
* ``curl``
* ``libuv1-dev``
* ``libc6-dev``
* ``http://archive.ubuntu.com/ubuntu/pool/main/libj/libjpeg-turbo/libjpeg-turbo8_2.0.3-0ubuntu1_amd64.deb``
* ``http://archive.ubuntu.com/ubuntu/pool/main/i/icu/libicu66_66.1-2ubuntu2_amd64.deb``
These are required on Ubuntu 20.04:
* ``libcairo2-dev``
* ``libjpeg8-dev``
* ``libpango1.0-dev``
* ``libgif-dev``
* ``build-essential``
* ``g++``
* ``xvfb``
* ``libgles2-mesa-dev``
* ``libgbm-dev``
* ``libxxf86vm-dev``
``tileserver-gl-light`` on npm ``tileserver-gl-light`` on npm
============================== ==============================

View file

@ -17,13 +17,22 @@ Getting started
-b, --bind <address> Bind address -b, --bind <address> Bind address
-p, --port <port> Port [8080] -p, --port <port> Port [8080]
-C|--no-cors Disable Cross-origin resource sharing headers -C|--no-cors Disable Cross-origin resource sharing headers
-u|--public_url <url> Enable exposing the server on subpaths, not necessarily the root of the domain
-V, --verbose More verbose output -V, --verbose More verbose output
-s, --silent Less verbose output -s, --silent Less verbose output
-v, --version Version info -v, --version Version info
Default styles and configuration Default preview style and configuration
====== ======
- If no configuration file is specified, the default styles (compatible with openmaptiles) are used. - If no configuration file is specified, a default preview style (compatible with openmaptiles) is used.
- If no mbtiles file is specified (and is not found in the current working directory), an extract is downloaded directly from https://openmaptiles.org/ - If no mbtiles file is specified (and is not found in the current working directory), a sample file is downloaded (showing the Zurich area)
Reloading the configuration
======
It is possible to reload the configuration file without restarting the whole process by sending a SIGHUP signal to the node process.
- The `docker kill -s HUP tileserver-gl` command can be used when running the tileserver-gl docker container.
- The `docker-compose kill -s HUP tileserver-gl-service-name` can be used when tileserver-gl is run as a docker-compose service.

9131
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,46 +1,50 @@
{ {
"name": "tileserver-gl", "name": "tileserver-gl",
"version": "3.0.0", "version": "4.1.2",
"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",
"type": "module",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/maptiler/tileserver-gl.git" "url": "git+https://github.com/maptiler/tileserver-gl.git"
}, },
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=10 <11" "node": ">=14.15.0 <17"
}, },
"scripts": { "scripts": {
"test": "mocha test/**.js --timeout 10000", "test": "mocha test/**.js --timeout 10000",
"docker": "docker build -f Dockerfile . && docker run --rm -i -p 8080:80 $(docker build -q .)" "docker": "docker build -f Dockerfile . && docker run --rm -i -p 8080:80 $(docker build -q .)"
}, },
"dependencies": { "dependencies": {
"@mapbox/mapbox-gl-native": "5.0.2", "@mapbox/glyph-pbf-composite": "0.0.3",
"@mapbox/mbtiles": "0.11.0", "@mapbox/mbtiles": "0.12.1",
"@mapbox/sphericalmercator": "1.1.0", "@mapbox/sphericalmercator": "1.2.0",
"@mapbox/vector-tile": "1.3.1", "@mapbox/vector-tile": "1.3.1",
"@maplibre/maplibre-gl-native": "5.0.1-pre.6",
"@maplibre/maplibre-gl-style-spec": "17.0.1",
"advanced-pool": "0.3.3", "advanced-pool": "0.3.3",
"canvas": "2.6.1", "canvas": "2.10.1",
"chokidar": "3.5.3",
"clone": "2.1.2", "clone": "2.1.2",
"color": "3.1.2", "color": "4.2.3",
"commander": "4.0.1", "commander": "9.4.0",
"cors": "2.8.5", "cors": "2.8.5",
"express": "4.17.1", "express": "4.18.1",
"glyph-pbf-composite": "0.0.2", "handlebars": "4.7.7",
"handlebars": "4.5.3", "http-shutdown": "1.2.2",
"http-shutdown": "1.2.1", "morgan": "1.10.0",
"morgan": "1.9.1",
"pbf": "3.2.1", "pbf": "3.2.1",
"proj4": "2.6.0", "proj4": "2.8.0",
"request": "2.88.0", "request": "2.88.2",
"sharp": "0.23.4", "sharp": "0.31.0",
"tileserver-gl-styles": "2.0.0" "tileserver-gl-styles": "2.0.0",
"sanitize-filename": "1.6.3"
}, },
"devDependencies": { "devDependencies": {
"mocha": "^6.2.2", "chai": "4.3.6",
"should": "^13.2.3", "mocha": "^10.0.0",
"supertest": "^4.0.2" "supertest": "^6.2.4"
} }
} }

View file

@ -0,0 +1,243 @@
// @class TileLayer
L.TileLayer.mergeOptions({
// @option keepBuffer
// The amount of tiles outside the visible map area to be kept in the stitched
// `TileLayer`.
// @option dumpToCanvas: Boolean = true
// Whether to dump loaded tiles to a `<canvas>` to prevent some rendering
// artifacts. (Disabled by default in IE)
dumpToCanvas: L.Browser.canvas && !L.Browser.ie,
});
L.TileLayer.include({
_onUpdateLevel: function(z, zoom) {
if (this.options.dumpToCanvas) {
this._levels[z].canvas.style.zIndex =
this.options.maxZoom - Math.abs(zoom - z);
}
},
_onRemoveLevel: function(z) {
if (this.options.dumpToCanvas) {
L.DomUtil.remove(this._levels[z].canvas);
}
},
_onCreateLevel: function(level) {
if (this.options.dumpToCanvas) {
level.canvas = L.DomUtil.create(
"canvas",
"leaflet-tile-container leaflet-zoom-animated",
this._container
);
level.ctx = level.canvas.getContext("2d");
this._resetCanvasSize(level);
}
},
_removeTile: function(key) {
if (this.options.dumpToCanvas) {
var tile = this._tiles[key];
var level = this._levels[tile.coords.z];
var tileSize = this.getTileSize();
if (level) {
// Where in the canvas should this tile go?
var offset = L.point(tile.coords.x, tile.coords.y)
.subtract(level.canvasRange.min)
.scaleBy(this.getTileSize());
level.ctx.clearRect(offset.x, offset.y, tileSize.x, tileSize.y);
}
}
L.GridLayer.prototype._removeTile.call(this, key);
},
_resetCanvasSize: function(level) {
var buff = this.options.keepBuffer,
pixelBounds = this._getTiledPixelBounds(this._map.getCenter()),
tileRange = this._pxBoundsToTileRange(pixelBounds),
tileSize = this.getTileSize();
tileRange.min = tileRange.min.subtract([buff, buff]); // This adds the no-prune buffer
tileRange.max = tileRange.max.add([buff + 1, buff + 1]);
var pixelRange = L.bounds(
tileRange.min.scaleBy(tileSize),
tileRange.max.add([1, 1]).scaleBy(tileSize) // This prevents an off-by-one when checking if tiles are inside
),
mustRepositionCanvas = false,
neededSize = pixelRange.max.subtract(pixelRange.min);
// Resize the canvas, if needed, and only to make it bigger.
if (
neededSize.x > level.canvas.width ||
neededSize.y > level.canvas.height
) {
// Resizing canvases erases the currently drawn content, I'm afraid.
// To keep it, dump the pixels to another canvas, then display it on
// top. This could be done with getImageData/putImageData, but that
// would break for tainted canvases (in non-CORS tilesets)
var oldSize = { x: level.canvas.width, y: level.canvas.height };
// console.info('Resizing canvas from ', oldSize, 'to ', neededSize);
var tmpCanvas = L.DomUtil.create("canvas");
tmpCanvas.style.width = (tmpCanvas.width = oldSize.x) + "px";
tmpCanvas.style.height = (tmpCanvas.height = oldSize.y) + "px";
tmpCanvas.getContext("2d").drawImage(level.canvas, 0, 0);
// var data = level.ctx.getImageData(0, 0, oldSize.x, oldSize.y);
level.canvas.style.width = (level.canvas.width = neededSize.x) + "px";
level.canvas.style.height = (level.canvas.height = neededSize.y) + "px";
level.ctx.drawImage(tmpCanvas, 0, 0);
// level.ctx.putImageData(data, 0, 0, 0, 0, oldSize.x, oldSize.y);
}
// Translate the canvas contents if it's moved around
if (level.canvasRange) {
var offset = level.canvasRange.min
.subtract(tileRange.min)
.scaleBy(this.getTileSize());
// console.info('Offsetting by ', offset);
if (!L.Browser.safari) {
// By default, canvases copy things "on top of" existing pixels, but we want
// this to *replace* the existing pixels when doing a drawImage() call.
// This will also clear the sides, so no clearRect() calls are needed to make room
// for the new tiles.
level.ctx.globalCompositeOperation = "copy";
level.ctx.drawImage(level.canvas, offset.x, offset.y);
level.ctx.globalCompositeOperation = "source-over";
} else {
// Safari clears the canvas when copying from itself :-(
if (!this._tmpCanvas) {
var t = (this._tmpCanvas = L.DomUtil.create("canvas"));
t.width = level.canvas.width;
t.height = level.canvas.height;
this._tmpContext = t.getContext("2d");
}
this._tmpContext.clearRect(
0,
0,
level.canvas.width,
level.canvas.height
);
this._tmpContext.drawImage(level.canvas, 0, 0);
level.ctx.clearRect(0, 0, level.canvas.width, level.canvas.height);
level.ctx.drawImage(this._tmpCanvas, offset.x, offset.y);
}
mustRepositionCanvas = true; // Wait until new props are set
}
level.canvasRange = tileRange;
level.canvasPxRange = pixelRange;
level.canvasOrigin = pixelRange.min;
// console.log('Canvas tile range: ', level, tileRange.min, tileRange.max );
// console.log('Canvas pixel range: ', pixelRange.min, pixelRange.max );
// console.log('Level origin: ', level.origin );
if (mustRepositionCanvas) {
this._setCanvasZoomTransform(
level,
this._map.getCenter(),
this._map.getZoom()
);
}
},
/// set transform/position of canvas, in addition to the transform/position of the individual tile container
_setZoomTransform: function(level, center, zoom) {
L.GridLayer.prototype._setZoomTransform.call(this, level, center, zoom);
if (this.options.dumpToCanvas) {
this._setCanvasZoomTransform(level, center, zoom);
}
},
// This will get called twice:
// * From _setZoomTransform
// * When the canvas has shifted due to a new tile being loaded
_setCanvasZoomTransform: function(level, center, zoom) {
// console.log('_setCanvasZoomTransform', level, center, zoom);
if (!level.canvasOrigin) {
return;
}
var scale = this._map.getZoomScale(zoom, level.zoom),
translate = level.canvasOrigin
.multiplyBy(scale)
.subtract(this._map._getNewPixelOrigin(center, zoom))
.round();
if (L.Browser.any3d) {
L.DomUtil.setTransform(level.canvas, translate, scale);
} else {
L.DomUtil.setPosition(level.canvas, translate);
}
},
_onOpaqueTile: function(tile) {
if (!this.options.dumpToCanvas) {
return;
}
// Guard against an NS_ERROR_NOT_AVAILABLE (or similar) exception
// when a non-image-tile has been loaded (e.g. a WMS error).
// Checking for tile.el.complete is not enough, as it has been
// already marked as loaded and ready somehow.
try {
this.dumpPixels(tile.coords, tile.el);
} catch (ex) {
return this.fire("tileerror", {
error: "Could not copy tile pixels: " + ex,
tile: tile,
coods: tile.coords,
});
}
// If dumping the pixels was successful, then hide the tile.
// Do not remove the tile itself, as it is needed to check if the whole
// level (and its canvas) should be removed (via level.el.children.length)
tile.el.style.display = "none";
},
// @section Extension methods
// @uninheritable
// @method dumpPixels(coords: Object, imageSource: CanvasImageSource): this
// Dumps pixels from the given `CanvasImageSource` into the layer, into
// the space for the tile represented by the `coords` tile coordinates (an object
// like `{x: Number, y: Number, z: Number}`; the image source must have the
// same size as the `tileSize` option for the layer. Has no effect if `dumpToCanvas`
// is `false`.
dumpPixels: function(coords, imageSource) {
var level = this._levels[coords.z],
tileSize = this.getTileSize();
if (!level.canvasRange || !this.options.dumpToCanvas) {
return;
}
// Check if the tile is inside the currently visible map bounds
// There is a possible race condition when tiles are loaded after they
// have been panned outside of the map.
if (!level.canvasRange.contains(coords)) {
this._resetCanvasSize(level);
}
// Where in the canvas should this tile go?
var offset = L.point(coords.x, coords.y)
.subtract(level.canvasRange.min)
.scaleBy(this.getTileSize());
level.ctx.drawImage(imageSource, offset.x, offset.y, tileSize.x, tileSize.y);
// TODO: Clear the pixels of other levels' canvases where they overlap
// this newly dumped tile.
return this;
},
});

View file

@ -0,0 +1,657 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-control-attribution svg {
display: inline !important;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
overflow: hidden;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: #fff;
background: rgba(255, 255, 255, 0.5);
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
border-bottom: 1px solid #ddd;
border-top: 1px solid #ddd;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,40 +0,0 @@
.mapbox-gl-inspect_popup {
color: #333;
display: table;
}
.mapbox-gl-inspect_feature:not(:last-child) {
border-bottom: 1px solid #ccc;
}
.mapbox-gl-inspect_layer:before {
content: '#';
}
.mapbox-gl-inspect_layer {
display: block;
font-weight: bold;
}
.mapbox-gl-inspect_property {
display: table-row;
}
.mapbox-gl-inspect_property-value {
display: table-cell;
}
.mapbox-gl-inspect_property-name {
display: table-cell;
padding-right: 10px;
}
.mapboxgl-ctrl-inspect {
background-image: url('data:image/svg+xml;charset=utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="#333333%22%20preserveAspectRatio=%22xMidYMid%20meet%22%20viewBox=%22-10%20-10%2060%2060%22%3E%3Cg%3E%3Cpath%20d=%22m15%2021.6q0-2%201.5-3.5t3.5-1.5%203.5%201.5%201.5%203.5-1.5%203.6-3.5%201.4-3.5-1.4-1.5-3.6z%20m18.4%2011.1l-6.4-6.5q1.4-2.1%201.4-4.6%200-3.4-2.5-5.8t-5.9-2.4-5.9%202.4-2.5%205.8%202.5%205.9%205.9%202.5q2.4%200%204.6-1.4l7.4%207.4q-0.9%200.6-2%200.6h-20q-1.3%200-2.3-0.9t-1.1-2.3l0.1-26.8q0-1.3%201-2.3t2.3-0.9h13.4l10%2010v19.3z%22%3E%3C/path%3E%3C/g%3E%3C/svg%3E');
}
.mapboxgl-ctrl-map {
background-image: url('data:image/svg+xml;charset=utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="#333333%22%20viewBox=%22-10%20-10%2060%2060%22%20preserveAspectRatio=%22xMidYMid%20meet%22%3E%3Cg%3E%3Cpath%20d=%22m25%2031.640000000000004v-19.766666666666673l-10-3.511666666666663v19.766666666666666z%20m9.140000000000008-26.640000000000004q0.8599999999999923%200%200.8599999999999923%200.8600000000000003v25.156666666666666q0%200.625-0.625%200.783333333333335l-9.375%203.1999999999999993-10-3.5133333333333354-8.906666666666668%203.4383333333333326-0.2333333333333334%200.07833333333333314q-0.8616666666666664%200-0.8616666666666664-0.8599999999999994v-25.156666666666663q0-0.625%200.6233333333333331-0.7833333333333332l9.378333333333334-3.198333333333334%2010%203.5133333333333336%208.905000000000001-3.4383333333333344z%22%3E%3C/path%3E%3C/g%3E%3C/svg%3E');
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"version":3,"file":"maplibre-gl-compat.js","sources":["../rollup/maplibregl.js"],"sourcesContent":["//\n// Our custom intro provides a specialized \"define()\" function, called by the\n// AMD modules below, that sets up the worker blob URL and then executes the\n// main module, storing its exported value as 'maplibregl'\n\n// The three \"chunks\" imported here are produced by a first Rollup pass,\n// which outputs them as AMD modules.\n\n// Shared dependencies, i.e.:\n/*\ndefine(['exports'], function (exports) {\n // Code for all common dependencies\n // Each module's exports are attached attached to 'exports' (with\n // names rewritten to avoid collisions, etc.)\n})\n*/\nimport './build/maplibregl/shared';\n\n// Worker and its unique dependencies, i.e.:\n/*\ndefine(['./shared.js'], function (__shared__js) {\n // Code for worker script and its unique dependencies.\n // Expects the output of 'shared' module to be passed in as an argument,\n // since all references to common deps look like, e.g.,\n // __shared__js.shapeText().\n});\n*/\n// When this wrapper function is passed to our custom define() above,\n// it gets stringified, together with the shared wrapper (using\n// Function.toString()), and the resulting string of code is made into a\n// Blob URL that gets used by the main module to create the web workers.\nimport './build/maplibregl/worker';\n\n// Main module and its unique dependencies\n/*\ndefine(['./shared.js'], function (__shared__js) {\n // Code for main GL JS module and its unique dependencies.\n // Expects the output of 'shared' module to be passed in as an argument,\n // since all references to common deps look like, e.g.,\n // __shared__js.shapeText().\n //\n // Returns the actual maplibregl (i.e. src/index.js)\n});\n*/\nimport './build/maplibregl/index';\n\nexport default maplibregl;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AA6CA;AACA,mBAAe,UAAU;;;;;;;;"}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,40 @@
.maplibregl-inspect_popup {
color: #333;
display: table;
}
.maplibregl-inspect_feature:not(:last-child) {
border-bottom: 1px solid #ccc;
}
.maplibregl-inspect_layer:before {
content: '#';
}
.maplibregl-inspect_layer {
display: block;
font-weight: bold;
}
.maplibregl-inspect_property {
display: table-row;
}
.maplibregl-inspect_property-value {
display: table-cell;
word-break: break-all;
}
.maplibregl-inspect_property-name {
display: table-cell;
padding-right: 10px;
word-break: break-all;
}
.maplibregl-ctrl-inspect {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23333333' preserveAspectRatio='xMidYMid meet' viewBox='-10 -10 60 60'%3E%3Cg%3E%3Cpath d='m15 21.6q0-2 1.5-3.5t3.5-1.5 3.5 1.5 1.5 3.5-1.5 3.6-3.5 1.4-3.5-1.4-1.5-3.6z m18.4 11.1l-6.4-6.5q1.4-2.1 1.4-4.6 0-3.4-2.5-5.8t-5.9-2.4-5.9 2.4-2.5 5.8 2.5 5.9 5.9 2.5q2.4 0 4.6-1.4l7.4 7.4q-0.9 0.6-2 0.6h-20q-1.3 0-2.3-0.9t-1.1-2.3l0.1-26.8q0-1.3 1-2.3t2.3-0.9h13.4l10 10v19.3z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
}
.maplibregl-ctrl-map {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23333333' viewBox='-10 -10 60 60' preserveAspectRatio='xMidYMid meet'%3E%3Cg%3E%3Cpath d='m25 31.640000000000004v-19.766666666666673l-10-3.511666666666663v19.766666666666666z m9.140000000000008-26.640000000000004q0.8599999999999923 0 0.8599999999999923 0.8600000000000003v25.156666666666666q0 0.625-0.625 0.783333333333335l-9.375 3.1999999999999993-10-3.5133333333333354-8.906666666666668 3.4383333333333326-0.2333333333333334 0.07833333333333314q-0.8616666666666664 0-0.8616666666666664-0.8599999999999994v-25.156666666666663q0-0.625 0.6233333333333331-0.7833333333333332l9.378333333333334-3.198333333333334 10 3.5133333333333336 8.905000000000001-3.4383333333333344z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"version":3,"file":"maplibre-gl.js","sources":["../rollup/maplibregl.js"],"sourcesContent":["//\n// Our custom intro provides a specialized \"define()\" function, called by the\n// AMD modules below, that sets up the worker blob URL and then executes the\n// main module, storing its exported value as 'maplibregl'\n\n// The three \"chunks\" imported here are produced by a first Rollup pass,\n// which outputs them as AMD modules.\n\n// Shared dependencies, i.e.:\n/*\ndefine(['exports'], function (exports) {\n // Code for all common dependencies\n // Each module's exports are attached attached to 'exports' (with\n // names rewritten to avoid collisions, etc.)\n})\n*/\nimport './build/maplibregl/shared';\n\n// Worker and its unique dependencies, i.e.:\n/*\ndefine(['./shared.js'], function (__shared__js) {\n // Code for worker script and its unique dependencies.\n // Expects the output of 'shared' module to be passed in as an argument,\n // since all references to common deps look like, e.g.,\n // __shared__js.shapeText().\n});\n*/\n// When this wrapper function is passed to our custom define() above,\n// it gets stringified, together with the shared wrapper (using\n// Function.toString()), and the resulting string of code is made into a\n// Blob URL that gets used by the main module to create the web workers.\nimport './build/maplibregl/worker';\n\n// Main module and its unique dependencies\n/*\ndefine(['./shared.js'], function (__shared__js) {\n // Code for main GL JS module and its unique dependencies.\n // Expects the output of 'shared' module to be passed in as an argument,\n // since all references to common deps look like, e.g.,\n // __shared__js.shapeText().\n //\n // Returns the actual maplibregl (i.e. src/index.js)\n});\n*/\nimport './build/maplibregl/index';\n\nexport default maplibregl;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AA6CA;AACA,mBAAe,UAAU;;;;;;;;"}

View file

@ -5,10 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{name}} - TileServer GL</title> <title>{{name}} - TileServer GL</title>
{{#is_vector}} {{#is_vector}}
<link rel="stylesheet" type="text/css" href="{{public_url}}mapbox-gl.css{{&key_query}}" /> <link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl.css{{&key_query}}" />
<link rel="stylesheet" type="text/css" href="{{public_url}}mapbox-gl-inspect.css{{&key_query}}" /> <link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl-inspect.css{{&key_query}}" />
<script src="{{public_url}}mapbox-gl.js{{&key_query}}"></script> <script>if (typeof Symbol !== 'undefined') { document.write('<script src="{{public_url}}maplibre-gl.js{{&key_query}}"><\/script>'); } else { document.write('<script src="{{public_url}}maplibre-gl-compat.js{{&key_query}}"><\/script>'); }</script>
<script src="{{public_url}}mapbox-gl-inspect.min.js{{&key_query}}"></script> <script>if (typeof Symbol !== 'undefined') { document.write('<script src="{{public_url}}maplibre-gl-inspect.min.js{{&key_query}}"><\/script>'); } else { document.write('<script src="{{public_url}}maplibre-gl-inspect-compat.min.js{{&key_query}}"><\/script>'); }</script>
<style> <style>
body {background:#fff;color:#333;font-family:Arial, sans-serif;} body {background:#fff;color:#333;font-family:Arial, sans-serif;}
#map {position:absolute;top:0;left:0;right:250px;bottom:0;} #map {position:absolute;top:0;left:0;right:250px;bottom:0;}
@ -18,9 +18,10 @@
</style> </style>
{{/is_vector}} {{/is_vector}}
{{^is_vector}} {{^is_vector}}
<link rel="stylesheet" type="text/css" href="{{public_url}}mapbox.css{{&key_query}}" /> <link rel="stylesheet" type="text/css" href="{{public_url}}leaflet.css{{&key_query}}" />
<script src="{{public_url}}mapbox.js{{&key_query}}"></script> <script src="{{public_url}}leaflet.js{{&key_query}}"></script>
<script src="{{public_url}}leaflet-hash.js{{&key_query}}"></script> <script src="{{public_url}}leaflet-hash.js{{&key_query}}"></script>
<script src="{{public_url}}L.TileLayer.NoGap.js{{&key_query}}"></script>
<style> <style>
body { margin:0; padding:0; } body { margin:0; padding:0; }
#map { position:absolute; top:0; bottom:0; width:100%; } #map { position:absolute; top:0; bottom:0; width:100%; }
@ -34,9 +35,10 @@
<div id="layerList"></div> <div id="layerList"></div>
<pre id="propertyList"></pre> <pre id="propertyList"></pre>
<script> <script>
var map = new mapboxgl.Map({ var map = new maplibregl.Map({
container: 'map', container: 'map',
hash: true, hash: true,
maplibreLogo: true,
style: { style: {
version: 8, version: 8,
sources: { sources: {
@ -48,8 +50,8 @@
layers: [] layers: []
} }
}); });
map.addControl(new mapboxgl.NavigationControl()); map.addControl(new maplibregl.NavigationControl());
var inspect = new MapboxInspect({ var inspect = new MaplibreInspect({
showInspectMap: true, showInspectMap: true,
showInspectButton: false showInspectButton: false
}); });
@ -74,12 +76,49 @@
<h1 style="display:none;">{{name}}</h1> <h1 style="display:none;">{{name}}</h1>
<div id='map'></div> <div id='map'></div>
<script> <script>
var map = L.mapbox.map('map', '{{public_url}}data/{{id}}.json{{&key_query}}', { zoomControl: false }); var map = L.map('map', { zoomControl: false });
map.eachLayer(function(layer) { new L.Control.Zoom({ position: 'topright' }).addTo(map);
// do not add scale prefix even if retina display is detected
layer.scalePrefix = '.'; var tile_urls = [], tile_attribution, tile_minzoom, tile_maxzoom;
}); var url = '{{public_url}}data/{{id}}.json{{&key_query}}';
new L.Control.Zoom({ position: 'topright' }).addTo(map); var req = new XMLHttpRequest();
req.overrideMimeType("application/json");
req.open('GET', url, true);
req.onload = function() {
var jsonResponse = JSON.parse(req.responseText);
for (key in jsonResponse) {
var keyl = key.toLowerCase();
switch(keyl) {
case "tiles":
tile_urls = jsonResponse[key];
break;
case "attribution":
tile_attribution = jsonResponse[key];
break;
case "minzoom":
tile_minzoom = jsonResponse[key];
break;
case "maxzoom":
tile_maxzoom = jsonResponse[key];
break;
}
}
for (tile_url in tile_urls) {
L.tileLayer(tile_urls[tile_url], {
minZoom: tile_minzoom,
maxZoom: tile_maxzoom,
attribution: tile_attribution
}).addTo(map);
}
map.eachLayer(function(layer) {
// do not add scale prefix even if retina display is detected
layer.scalePrefix = '.';
});
};
req.send(null);
setTimeout(function() { setTimeout(function() {
new L.Hash(map); new L.Hash(map);
}, 0); }, 0);

View file

@ -41,7 +41,7 @@
{{#if serving_data}}| {{/if}}<a href="{{public_url}}styles/{{@key}}.json{{&../key_query}}">TileJSON</a> {{#if serving_data}}| {{/if}}<a href="{{public_url}}styles/{{@key}}.json{{&../key_query}}">TileJSON</a>
{{/if}} {{/if}}
{{#if serving_rendered}} {{#if serving_rendered}}
| <a href="/styles/{{@key}}/wmts.xml{{&../key_query}}">WMTS</a> | <a href="{{public_url}}styles/{{@key}}/wmts.xml{{&../key_query}}">WMTS</a>
{{/if}} {{/if}}
{{#if xyz_link}} {{#if xyz_link}}
| <a href="#" onclick="return toggle_xyz('xyz_style_{{@key}}');">XYZ</a> | <a href="#" onclick="return toggle_xyz('xyz_style_{{@key}}');">XYZ</a>

View file

@ -4,11 +4,14 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{name}} - TileServer GL</title> <title>{{name}} - TileServer GL</title>
<link rel="stylesheet" type="text/css" href="{{public_url}}mapbox-gl.css{{&key_query}}" /> <link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl.css{{&key_query}}" />
<script src="{{public_url}}mapbox-gl.js{{&key_query}}"></script> <link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl-inspect.css{{&key_query}}" />
<link rel="stylesheet" type="text/css" href="{{public_url}}mapbox.css{{&key_query}}" /> <link rel="stylesheet" type="text/css" href="{{public_url}}leaflet.css{{&key_query}}" />
<script src="{{public_url}}mapbox.js{{&key_query}}"></script> <script>if (typeof Symbol !== 'undefined') { document.write('<script src="{{public_url}}maplibre-gl.js{{&key_query}}"><\/script>'); } else { document.write('<script src="{{public_url}}maplibre-gl-compat.js{{&key_query}}"><\/script>'); }</script>
<script>if (typeof Symbol !== 'undefined') { document.write('<script src="{{public_url}}maplibre-gl-inspect.min.js{{&key_query}}"><\/script>'); } else { document.write('<script src="{{public_url}}maplibre-gl-inspect-compat.min.js{{&key_query}}"><\/script>'); }</script>
<script src="{{public_url}}leaflet.js{{&key_query}}"></script>
<script src="{{public_url}}leaflet-hash.js{{&key_query}}"></script> <script src="{{public_url}}leaflet-hash.js{{&key_query}}"></script>
<script src="{{public_url}}L.TileLayer.NoGap.js{{&key_query}}"></script>
<style> <style>
body { margin:0; padding:0; } body { margin:0; padding:0; }
#map { position:absolute; top:0; bottom:0; width:100%; } #map { position:absolute; top:0; bottom:0; width:100%; }
@ -22,21 +25,72 @@
var preference = var preference =
q.indexOf('vector') >= 0 ? 'vector' : q.indexOf('vector') >= 0 ? 'vector' :
(q.indexOf('raster') >= 0 ? 'raster' : (q.indexOf('raster') >= 0 ? 'raster' :
(mapboxgl.supported() ? 'vector' : 'raster')); (maplibregl.supported() ? 'vector' : 'raster'));
if (preference == 'vector') { if (preference == 'vector') {
mapboxgl.setRTLTextPlugin('{{public_url}}mapbox-gl-rtl-text.js{{&key_query}}'); maplibregl.setRTLTextPlugin('{{public_url}}mapbox-gl-rtl-text.js{{&key_query}}');
var map = new mapboxgl.Map({ var map = new maplibregl.Map({
container: 'map', container: 'map',
style: '{{public_url}}styles/{{id}}/style.json{{&key_query}}', style: '{{public_url}}styles/{{id}}/style.json{{&key_query}}',
hash: true hash: true,
maplibreLogo: true
}); });
map.addControl(new mapboxgl.NavigationControl()); map.addControl(new maplibregl.NavigationControl({
visualizePitch: true,
showZoom: true,
showCompass: true
}));
map.addControl(new MaplibreInspect({
showMapPopupOnHover: false,
showInspectMapPopupOnHover: false,
selectThreshold: 5
}));
} else { } else {
var map = L.mapbox.map('map', '{{public_url}}styles/{{id}}.json{{&key_query}}', { zoomControl: false }); var map = L.map('map', { zoomControl: false });
new L.Control.Zoom({ position: 'topright' }).addTo(map); new L.Control.Zoom({ position: 'topright' }).addTo(map);
setTimeout(function() {
new L.Hash(map); var tile_urls = [], tile_attribution, tile_minzoom, tile_maxzoom;
}, 0); var url = '{{public_url}}styles/{{id}}.json{{&key_query}}';
var req = new XMLHttpRequest();
req.overrideMimeType("application/json");
req.open('GET', url, true);
req.onload = function() {
var jsonResponse = JSON.parse(req.responseText);
for (key in jsonResponse) {
var keyl = key.toLowerCase();
switch(keyl) {
case "tiles":
tile_urls = jsonResponse[key];
break;
case "attribution":
tile_attribution = jsonResponse[key];
break;
case "minzoom":
tile_minzoom = jsonResponse[key];
break;
case "maxzoom":
tile_maxzoom = jsonResponse[key];
break;
}
}
for (tile_url in tile_urls) {
L.tileLayer(tile_urls[tile_url], {
minZoom: tile_minzoom,
maxZoom: tile_maxzoom,
attribution: tile_attribution
}).addTo(map);
}
map.eachLayer(function(layer) {
// do not add scale prefix even if retina display is detected
layer.scalePrefix = '.';
});
};
req.send(null);
setTimeout(function() {
new L.Hash(map);
}, 0);
} }
</script> </script>
</body> </body>

View file

@ -11,7 +11,7 @@
<ows:Operation name="GetCapabilities"> <ows:Operation name="GetCapabilities">
<ows:DCP> <ows:DCP>
<ows:HTTP> <ows:HTTP>
<ows:Get xlink:href="{{baseUrl}}/wmts/{{id}}/"> <ows:Get xlink:href="{{baseUrl}}wmts/{{id}}/">
<ows:Constraint name="GetEncoding"> <ows:Constraint name="GetEncoding">
<ows:AllowedValues> <ows:AllowedValues>
<ows:Value>RESTful</ows:Value> <ows:Value>RESTful</ows:Value>
@ -24,7 +24,7 @@
<ows:Operation name="GetTile"> <ows:Operation name="GetTile">
<ows:DCP> <ows:DCP>
<ows:HTTP> <ows:HTTP>
<ows:Get xlink:href="{{baseUrl}}/styles/"> <ows:Get xlink:href="{{baseUrl}}styles/">
<ows:Constraint name="GetEncoding"> <ows:Constraint name="GetEncoding">
<ows:AllowedValues> <ows:AllowedValues>
<ows:Value>RESTful</ows:Value> <ows:Value>RESTful</ows:Value>
@ -50,7 +50,7 @@
<TileMatrixSetLink> <TileMatrixSetLink>
<TileMatrixSet>GoogleMapsCompatible</TileMatrixSet> <TileMatrixSet>GoogleMapsCompatible</TileMatrixSet>
</TileMatrixSetLink> </TileMatrixSetLink>
<ResourceURL format="image/png" resourceType="tile" template="{{baseUrl}}/styles/{{id}}/{TileMatrix}/{TileCol}/{TileRow}.png{{key_query}}"/> <ResourceURL format="image/png" resourceType="tile" template="{{baseUrl}}styles/{{id}}/{TileMatrix}/{TileCol}/{TileRow}.png{{key_query}}"/>
</Layer><TileMatrixSet> </Layer><TileMatrixSet>
<ows:Title>GoogleMapsCompatible</ows:Title> <ows:Title>GoogleMapsCompatible</ows:Title>
<ows:Abstract>GoogleMapsCompatible EPSG:3857</ows:Abstract> <ows:Abstract>GoogleMapsCompatible EPSG:3857</ows:Abstract>
@ -395,7 +395,7 @@
</TileMatrix> </TileMatrix>
<TileMatrix> <TileMatrix>
<ows:Identifier>18</ows:Identifier> <ows:Identifier>18</ows:Identifier>
<ScaleDenominator></ScaleDenominator> <ScaleDenominator>1066.3647919249</ScaleDenominator>
<TopLeftCorner>90 -180</TopLeftCorner> <TopLeftCorner>90 -180</TopLeftCorner>
<TileWidth>256</TileWidth> <TileWidth>256</TileWidth>
<TileHeight>256</TileHeight> <TileHeight>256</TileHeight>
@ -403,5 +403,5 @@
<MatrixHeight>262144</MatrixHeight> <MatrixHeight>262144</MatrixHeight>
</TileMatrix></TileMatrixSet> </TileMatrix></TileMatrixSet>
</Contents> </Contents>
<ServiceMetadataURL xlink:href="{{baseUrl}}/wmts/{{id}}/"/> <ServiceMetadataURL xlink:href="{{baseUrl}}wmts/{{id}}/"/>
</Capabilities> </Capabilities>

View file

@ -11,38 +11,50 @@
/* CREATE tileserver-gl-light */ /* CREATE tileserver-gl-light */
// SYNC THE `light` FOLDER // SYNC THE `light` FOLDER
require('child_process').execSync('rsync -av --exclude="light" --exclude=".git" --exclude="node_modules" --delete . light', {
import child_process from 'child_process'
child_process.execSync('rsync -av --exclude="light" --exclude=".git" --exclude="node_modules" --delete . light', {
stdio: 'inherit' stdio: 'inherit'
}); });
// PATCH `package.json` // PATCH `package.json`
var fs = require('fs'); import fs from 'fs';
var packageJson = require('./package'); import path from 'path';
import {fileURLToPath} from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageJson = JSON.parse(fs.readFileSync(__dirname + '/package.json', 'utf8'))
packageJson.name += '-light'; packageJson.name += '-light';
packageJson.description = 'Map tile server for JSON GL styles - serving vector tiles'; packageJson.description = 'Map tile server for JSON GL styles - serving vector tiles';
delete packageJson.dependencies['canvas']; delete packageJson.dependencies['canvas'];
delete packageJson.dependencies['@mapbox/mapbox-gl-native']; delete packageJson.dependencies['@maplibre/maplibre-gl-native'];
delete packageJson.dependencies['sharp']; delete packageJson.dependencies['sharp'];
delete packageJson.optionalDependencies; delete packageJson.optionalDependencies;
delete packageJson.devDependencies; delete packageJson.devDependencies;
packageJson.engines.node = '>= 10'; packageJson.engines.node = '>= 14.15.0';
var str = JSON.stringify(packageJson, undefined, 2); const str = JSON.stringify(packageJson, undefined, 2);
fs.writeFileSync('light/package.json', str); fs.writeFileSync('light/package.json', str);
fs.renameSync('light/README_light.md', 'light/README.md'); fs.renameSync('light/README_light.md', 'light/README.md');
fs.renameSync('light/Dockerfile_light', 'light/Dockerfile'); fs.renameSync('light/Dockerfile_light', 'light/Dockerfile');
fs.renameSync('light/docker-entrypoint_light.sh', 'light/docker-entrypoint.sh');
// for Build tileserver-gl-light docker image, don't publish
if (process.argv.length > 2 && process.argv[2] == '--no-publish') {
process.exit(0);
}
/* PUBLISH */ /* PUBLISH */
// tileserver-gl // tileserver-gl
require('child_process').execSync('npm publish .', { child_process.execSync('npm publish . --access public', {
stdio: 'inherit' stdio: 'inherit'
}); });
// tileserver-gl-light // tileserver-gl-light
require('child_process').execSync('npm publish light', { child_process.execSync('npm publish ./light --access public', {
stdio: 'inherit' stdio: 'inherit'
}); });

4
run.sh
View file

@ -8,7 +8,7 @@ _term() {
trap _term SIGTERM trap _term SIGTERM
trap _term SIGINT trap _term SIGINT
xvfbMaxStartWaitTime=5 xvfbMaxStartWaitTime=60
displayNumber=99 displayNumber=99
screenNumber=0 screenNumber=0
@ -18,7 +18,7 @@ rm -rf /tmp/.X11-unix /tmp/.X${displayNumber}-lock ~/xvfb.pid
echo "Starting Xvfb on display ${displayNumber}" echo "Starting Xvfb on display ${displayNumber}"
start-stop-daemon --start --pidfile ~/xvfb.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :${displayNumber} -screen ${screenNumber} 1024x768x24 -ac +extension GLX +render -noreset start-stop-daemon --start --pidfile ~/xvfb.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :${displayNumber} -screen ${screenNumber} 1024x768x24 -ac +extension GLX +render -noreset
# Wait to be able to connect to the port. This will exit if it cannot in 15 minutes. # Wait to be able to connect to the port. This will exit if it cannot in 1 minute.
timeout ${xvfbMaxStartWaitTime} bash -c "while ! xdpyinfo -display :${displayNumber} >/dev/null; do sleep 0.5; done" timeout ${xvfbMaxStartWaitTime} bash -c "while ! xdpyinfo -display :${displayNumber} >/dev/null; do sleep 0.5; done"
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "Could not connect to display ${displayNumber} in ${xvfbMaxStartWaitTime} seconds time." echo "Could not connect to display ${displayNumber} in ${xvfbMaxStartWaitTime} seconds time."

View file

@ -2,20 +2,25 @@
'use strict'; 'use strict';
const fs = require('fs'); import fs from 'node:fs';
const path = require('path'); import path from 'path';
const request = require('request'); import {fileURLToPath} from 'url';
import request from 'request';
import {server} from './server.js';
const MBTiles = require('@mapbox/mbtiles'); import MBTiles from '@mapbox/mbtiles';
const packageJson = require('../package'); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageJson = JSON.parse(fs.readFileSync(__dirname + '/../package.json', 'utf8'));
const args = process.argv; const args = process.argv;
if (args.length >= 3 && args[2][0] !== '-') { if (args.length >= 3 && args[2][0] !== '-') {
args.splice(2, 0, '--mbtiles'); args.splice(2, 0, '--mbtiles');
} }
const opts = require('commander') import {program} from 'commander';
program
.description('tileserver-gl startup options') .description('tileserver-gl startup options')
.usage('tileserver-gl [mbtiles] [options]') .usage('tileserver-gl [mbtiles] [options]')
.option( .option(
@ -66,7 +71,8 @@ const opts = require('commander')
packageJson.version, packageJson.version,
'-v, --version' '-v, --version'
) )
.parse(args); program.parse(process.argv);
const opts = program.opts();
console.log(`Starting ${packageJson.name} v${packageJson.version}`); console.log(`Starting ${packageJson.name} v${packageJson.version}`);
@ -75,7 +81,7 @@ const startServer = (configPath, config) => {
if (publicUrl && publicUrl.lastIndexOf('/') !== publicUrl.length - 1) { if (publicUrl && publicUrl.lastIndexOf('/') !== publicUrl.length - 1) {
publicUrl += '/'; publicUrl += '/';
} }
return require('./server')({ return server({
configPath: configPath, configPath: configPath,
config: config, config: config,
bind: opts.bind, bind: opts.bind,
@ -101,7 +107,7 @@ const startWithMBTiles = (mbtilesFile) => {
console.log(`ERROR: Not valid MBTiles file: ${mbtilesFile}`); console.log(`ERROR: Not valid MBTiles file: ${mbtilesFile}`);
process.exit(1); process.exit(1);
} }
const instance = new MBTiles(mbtilesFile, (err) => { const instance = new MBTiles(mbtilesFile + '?mode=ro', (err) => {
if (err) { if (err) {
console.log('ERROR: Unable to open MBTiles.'); console.log('ERROR: Unable to open MBTiles.');
console.log(` Make sure ${path.basename(mbtilesFile)} is valid MBTiles.`); console.log(` Make sure ${path.basename(mbtilesFile)} is valid MBTiles.`);
@ -116,38 +122,37 @@ const startWithMBTiles = (mbtilesFile) => {
} }
const bounds = info.bounds; const bounds = info.bounds;
const styleDir = path.resolve(__dirname, "../node_modules/tileserver-gl-styles/"); const styleDir = path.resolve(__dirname, '../node_modules/tileserver-gl-styles/');
const config = { const config = {
"options": { 'options': {
"paths": { 'paths': {
"root": styleDir, 'root': styleDir,
"fonts": "fonts", 'fonts': 'fonts',
"styles": "styles", 'styles': 'styles',
"mbtiles": path.dirname(mbtilesFile) 'mbtiles': path.dirname(mbtilesFile)
} }
}, },
"styles": {}, 'styles': {},
"data": {} 'data': {}
}; };
if (info.format === 'pbf' && if (info.format === 'pbf' &&
info.name.toLowerCase().indexOf('openmaptiles') > -1) { info.name.toLowerCase().indexOf('openmaptiles') > -1) {
config['data'][`v3`] = { config['data'][`v3`] = {
"mbtiles": path.basename(mbtilesFile) 'mbtiles': path.basename(mbtilesFile)
}; };
const styles = fs.readdirSync(path.resolve(styleDir, 'styles')); const styles = fs.readdirSync(path.resolve(styleDir, 'styles'));
for (let styleName of styles) { for (const styleName of styles) {
const styleFileRel = styleName + '/style.json'; const styleFileRel = styleName + '/style.json';
const styleFile = path.resolve(styleDir, 'styles', styleFileRel); const styleFile = path.resolve(styleDir, 'styles', styleFileRel);
if (fs.existsSync(styleFile)) { if (fs.existsSync(styleFile)) {
config['styles'][styleName] = { config['styles'][styleName] = {
"style": styleFileRel, 'style': styleFileRel,
"tilejson": { 'tilejson': {
"bounds": bounds 'bounds': bounds
} }
}; };
} }
@ -155,10 +160,10 @@ const startWithMBTiles = (mbtilesFile) => {
} else { } else {
console.log(`WARN: MBTiles not in "openmaptiles" format. Serving raw data only...`); console.log(`WARN: MBTiles not in "openmaptiles" format. Serving raw data only...`);
config['data'][(info.id || 'mbtiles') config['data'][(info.id || 'mbtiles')
.replace(/\//g, '_') .replace(/\//g, '_')
.replace(/:/g, '_') .replace(/:/g, '_')
.replace(/\?/g, '_')] = { .replace(/\?/g, '_')] = {
"mbtiles": path.basename(mbtilesFile) 'mbtiles': path.basename(mbtilesFile)
}; };
} }
@ -179,7 +184,7 @@ fs.stat(path.resolve(opts.config), (err, stats) => {
if (!mbtiles) { if (!mbtiles) {
// try to find in the cwd // try to find in the cwd
const files = fs.readdirSync(process.cwd()); const files = fs.readdirSync(process.cwd());
for (let filename of files) { for (const filename of files) {
if (filename.endsWith('.mbtiles')) { if (filename.endsWith('.mbtiles')) {
const mbTilesStats = fs.statSync(filename); const mbTilesStats = fs.statSync(filename);
if (mbTilesStats.isFile() && mbTilesStats.size > 0) { if (mbTilesStats.isFile() && mbTilesStats.size > 0) {
@ -192,7 +197,7 @@ fs.stat(path.resolve(opts.config), (err, stats) => {
console.log(`No MBTiles specified, using ${mbtiles}`); console.log(`No MBTiles specified, using ${mbtiles}`);
return startWithMBTiles(mbtiles); return startWithMBTiles(mbtiles);
} else { } else {
const url = 'https://github.com/klokantech/tileserver-gl/releases/download/v1.3.0/zurich_switzerland.mbtiles'; const url = 'https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/zurich_switzerland.mbtiles';
const filename = 'zurich_switzerland.mbtiles'; const filename = 'zurich_switzerland.mbtiles';
const stream = fs.createWriteStream(filename); const stream = fs.createWriteStream(filename);
console.log(`No MBTiles found`); console.log(`No MBTiles found`);

View file

@ -1,155 +1,171 @@
'use strict'; 'use strict';
const fs = require('fs'); import fs from 'node:fs';
const path = require('path'); import path from 'path';
const zlib = require('zlib'); import zlib from 'zlib';
const clone = require('clone'); import clone from 'clone';
const express = require('express'); import express from 'express';
const MBTiles = require('@mapbox/mbtiles'); import MBTiles from '@mapbox/mbtiles';
const Pbf = require('pbf'); import Pbf from 'pbf';
const VectorTile = require('@mapbox/vector-tile').VectorTile; import VectorTile from '@mapbox/vector-tile';
const utils = require('./utils'); import {getTileUrls, fixTileJSONCenter} from './utils.js';
module.exports = (options, repo, params, id, styles, publicUrl) => { export const serve_data = {
const app = express().disable('x-powered-by'); init: (options, repo) => {
const app = express().disable('x-powered-by');
const mbtilesFile = path.resolve(options.paths.mbtiles, params.mbtiles); app.get('/:id/:z(\\d+)/:x(\\d+)/:y(\\d+).:format([\\w.]+)', (req, res, next) => {
let tileJSON = { const item = repo[req.params.id];
'tiles': params.domains || options.domains if (!item) {
}; return res.sendStatus(404);
repo[id] = tileJSON;
const mbtilesFileStats = fs.statSync(mbtilesFile);
if (!mbtilesFileStats.isFile() || mbtilesFileStats.size === 0) {
throw Error(`Not valid MBTiles file: ${mbtilesFile}`);
}
let source;
const sourceInfoPromise = new Promise((resolve, reject) => {
source = new MBTiles(mbtilesFile, err => {
if (err) {
reject(err);
return;
} }
source.getInfo((err, info) => { const tileJSONFormat = item.tileJSON.format;
const z = req.params.z | 0;
const x = req.params.x | 0;
const y = req.params.y | 0;
let format = req.params.format;
if (format === options.pbfAlias) {
format = 'pbf';
}
if (format !== tileJSONFormat &&
!(format === 'geojson' && tileJSONFormat === 'pbf')) {
return res.status(404).send('Invalid format');
}
if (z < item.tileJSON.minzoom || 0 || x < 0 || y < 0 ||
z > item.tileJSON.maxzoom ||
x >= Math.pow(2, z) || y >= Math.pow(2, z)) {
return res.status(404).send('Out of bounds');
}
item.source.getTile(z, x, y, (err, data, headers) => {
let isGzipped;
if (err) { if (err) {
reject(err); if (/does not exist/.test(err.message)) {
return; return res.status(204).send();
} } else {
tileJSON['name'] = id; return res.status(500).send(err.message);
tileJSON['format'] = 'pbf'; }
Object.assign(tileJSON, info);
tileJSON['tilejson'] = '2.0.0';
delete tileJSON['filesize'];
delete tileJSON['mtime'];
delete tileJSON['scheme'];
Object.assign(tileJSON, params.tilejson || {});
utils.fixTileJSONCenter(tileJSON);
if (options.dataDecoratorFunc) {
tileJSON = options.dataDecoratorFunc(id, 'tilejson', tileJSON);
}
resolve();
});
});
});
const tilePattern = `/${id}/:z(\\d+)/:x(\\d+)/:y(\\d+).:format([\\w.]+)`;
app.get(tilePattern, (req, res, next) => {
const z = req.params.z | 0;
const x = req.params.x | 0;
const y = req.params.y | 0;
let format = req.params.format;
if (format === options.pbfAlias) {
format = 'pbf';
}
if (format !== tileJSON.format &&
!(format === 'geojson' && tileJSON.format === 'pbf')) {
return res.status(404).send('Invalid format');
}
if (z < tileJSON.minzoom || 0 || x < 0 || y < 0 ||
z > tileJSON.maxzoom ||
x >= Math.pow(2, z) || y >= Math.pow(2, z)) {
return res.status(404).send('Out of bounds');
}
source.getTile(z, x, y, (err, data, headers) => {
let isGzipped;
if (err) {
if (/does not exist/.test(err.message)) {
return res.status(204).send();
} else { } else {
return res.status(500).send(err.message); if (data == null) {
} return res.status(404).send('Not found');
} else { } else {
if (data == null) { if (tileJSONFormat === 'pbf') {
return res.status(404).send('Not found'); isGzipped = data.slice(0, 2).indexOf(
} else { Buffer.from([0x1f, 0x8b])) === 0;
if (tileJSON['format'] === 'pbf') { if (options.dataDecoratorFunc) {
isGzipped = data.slice(0, 2).indexOf( if (isGzipped) {
Buffer.from([0x1f, 0x8b])) === 0; data = zlib.unzipSync(data);
if (options.dataDecoratorFunc) { isGzipped = false;
}
data = options.dataDecoratorFunc(id, 'data', data, z, x, y);
}
}
if (format === 'pbf') {
headers['Content-Type'] = 'application/x-protobuf';
} else if (format === 'geojson') {
headers['Content-Type'] = 'application/json';
if (isGzipped) { if (isGzipped) {
data = zlib.unzipSync(data); data = zlib.unzipSync(data);
isGzipped = false; isGzipped = false;
} }
data = options.dataDecoratorFunc(id, 'data', data, z, x, y);
}
}
if (format === 'pbf') {
headers['Content-Type'] = 'application/x-protobuf';
} else if (format === 'geojson') {
headers['Content-Type'] = 'application/json';
if (isGzipped) { const tile = new VectorTile(new Pbf(data));
data = zlib.unzipSync(data); const geojson = {
isGzipped = false; 'type': 'FeatureCollection',
} 'features': []
};
const tile = new VectorTile(new Pbf(data)); for (const layerName in tile.layers) {
const geojson = { const layer = tile.layers[layerName];
"type": "FeatureCollection", for (let i = 0; i < layer.length; i++) {
"features": [] const feature = layer.feature(i);
}; const featureGeoJSON = feature.toGeoJSON(x, y, z);
for (let layerName in tile.layers) { featureGeoJSON.properties.layer = layerName;
const layer = tile.layers[layerName]; geojson.features.push(featureGeoJSON);
for (let i = 0; i < layer.length; i++) { }
const feature = layer.feature(i);
const featureGeoJSON = feature.toGeoJSON(x, y, z);
featureGeoJSON.properties.layer = layerName;
geojson.features.push(featureGeoJSON);
} }
data = JSON.stringify(geojson);
} }
data = JSON.stringify(geojson); delete headers['ETag']; // do not trust the tile ETag -- regenerate
} headers['Content-Encoding'] = 'gzip';
delete headers['ETag']; // do not trust the tile ETag -- regenerate res.set(headers);
headers['Content-Encoding'] = 'gzip';
res.set(headers);
if (!isGzipped) { if (!isGzipped) {
data = zlib.gzipSync(data); data = zlib.gzipSync(data);
isGzipped = true; isGzipped = true;
} }
return res.status(200).send(data); return res.status(200).send(data);
}
} }
} });
}); });
});
app.get(`/${id}.json`, (req, res, next) => { app.get('/:id.json', (req, res, next) => {
const info = clone(tileJSON); const item = repo[req.params.id];
info.tiles = utils.getTileUrls(req, info.tiles, if (!item) {
`data/${id}`, info.format, publicUrl, { return res.sendStatus(404);
'pbf': options.pbfAlias }
}); const info = clone(item.tileJSON);
return res.send(info); info.tiles = getTileUrls(req, info.tiles,
}); `data/${req.params.id}`, info.format, item.publicUrl, {
'pbf': options.pbfAlias
});
return res.send(info);
});
return sourceInfoPromise.then(() => app); return app;
},
add: (options, repo, params, id, publicUrl) => {
const mbtilesFile = path.resolve(options.paths.mbtiles, params.mbtiles);
let tileJSON = {
'tiles': params.domains || options.domains
};
const mbtilesFileStats = fs.statSync(mbtilesFile);
if (!mbtilesFileStats.isFile() || mbtilesFileStats.size === 0) {
throw Error(`Not valid MBTiles file: ${mbtilesFile}`);
}
let source;
const sourceInfoPromise = new Promise((resolve, reject) => {
source = new MBTiles(mbtilesFile + '?mode=ro', err => {
if (err) {
reject(err);
return;
}
source.getInfo((err, info) => {
if (err) {
reject(err);
return;
}
tileJSON['name'] = id;
tileJSON['format'] = 'pbf';
Object.assign(tileJSON, info);
tileJSON['tilejson'] = '2.0.0';
delete tileJSON['filesize'];
delete tileJSON['mtime'];
delete tileJSON['scheme'];
Object.assign(tileJSON, params.tilejson || {});
fixTileJSONCenter(tileJSON);
if (options.dataDecoratorFunc) {
tileJSON = options.dataDecoratorFunc(id, 'tilejson', tileJSON);
}
resolve();
});
});
});
return sourceInfoPromise.then(() => {
repo[id] = {
tileJSON,
publicUrl,
source
};
});
}
}; };

View file

@ -1,12 +1,12 @@
'use strict'; 'use strict';
const express = require('express'); import express from 'express';
const fs = require('fs'); import fs from 'node:fs';
const path = require('path'); import path from 'path';
const utils = require('./utils'); import {getFontsPbf} from './utils.js';
module.exports = (options, allowedFonts) => { export const serve_font = (options, allowedFonts) => {
const app = express().disable('x-powered-by'); const app = express().disable('x-powered-by');
const lastModified = new Date().toUTCString(); const lastModified = new Date().toUTCString();
@ -40,19 +40,19 @@ module.exports = (options, allowedFonts) => {
const fontstack = decodeURI(req.params.fontstack); const fontstack = decodeURI(req.params.fontstack);
const range = req.params.range; const range = req.params.range;
utils.getFontsPbf(options.serveAllFonts ? null : allowedFonts, getFontsPbf(options.serveAllFonts ? null : allowedFonts,
fontPath, fontstack, range, existingFonts).then(concated => { fontPath, fontstack, range, existingFonts).then((concated) => {
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);
}, err => res.status(400).send(err) }, (err) => res.status(400).send(err)
); );
}); });
app.get('/fonts.json', (req, res, next) => { app.get('/fonts.json', (req, res, next) => {
res.header('Content-type', 'application/json'); res.header('Content-type', 'application/json');
return res.send( return res.send(
Object.keys(options.serveAllFonts ? existingFonts : allowedFonts).sort() Object.keys(options.serveAllFonts ? existingFonts : allowedFonts).sort()
); );
}); });

10
src/serve_light.js Normal file
View file

@ -0,0 +1,10 @@
'use strict';
export const serve_rendered = {
init: (options, repo) => {
},
add: (options, repo, params, id, publicUrl, dataResolver) => {
},
remove: (repo, id) => {
}
};

File diff suppressed because it is too large Load diff

View file

@ -1,119 +1,158 @@
'use strict'; 'use strict';
const path = require('path'); import path from 'path';
const fs = require('fs'); import fs from 'node:fs';
const clone = require('clone'); import clone from 'clone';
const express = require('express'); import express from 'express';
import {validate} from '@maplibre/maplibre-gl-style-spec';
const utils = require('./utils'); import {getPublicUrl} from './utils.js';
module.exports = (options, repo, params, id, publicUrl, reportTiles, reportFont) => { const httpTester = /^(http(s)?:)?\/\//;
const app = express().disable('x-powered-by');
const styleFile = path.resolve(options.paths.styles, params.style); const fixUrl = (req, url, publicUrl, opt_nokey) => {
if (!url || (typeof url !== 'string') || url.indexOf('local://') !== 0) {
return url;
}
const queryParams = [];
if (!opt_nokey && req.query.key) {
queryParams.unshift(`key=${encodeURIComponent(req.query.key)}`);
}
let query = '';
if (queryParams.length) {
query = `?${queryParams.join('&')}`;
}
return url.replace(
'local://', getPublicUrl(publicUrl, req)) + query;
};
const styleJSON = clone(require(styleFile)); export const serve_style = {
for (const name of Object.keys(styleJSON.sources)) { init: (options, repo) => {
const source = styleJSON.sources[name]; const app = express().disable('x-powered-by');
const url = source.url;
if (url && url.lastIndexOf('mbtiles:', 0) === 0) {
let mbtilesFile = url.substring('mbtiles://'.length);
const fromData = mbtilesFile[0] === '{' &&
mbtilesFile[mbtilesFile.length - 1] === '}';
if (fromData) { app.get('/:id/style.json', (req, res, next) => {
mbtilesFile = mbtilesFile.substr(1, mbtilesFile.length - 2); const item = repo[req.params.id];
const mapsTo = (params.mapping || {})[mbtilesFile]; if (!item) {
if (mapsTo) { return res.sendStatus(404);
mbtilesFile = mapsTo; }
const styleJSON_ = clone(item.styleJSON);
for (const name of Object.keys(styleJSON_.sources)) {
const source = styleJSON_.sources[name];
source.url = fixUrl(req, source.url, item.publicUrl);
}
// mapbox-gl-js viewer cannot handle sprite urls with query
if (styleJSON_.sprite) {
styleJSON_.sprite = fixUrl(req, styleJSON_.sprite, item.publicUrl, false);
}
if (styleJSON_.glyphs) {
styleJSON_.glyphs = fixUrl(req, styleJSON_.glyphs, item.publicUrl, false);
}
return res.send(styleJSON_);
});
app.get('/:id/sprite:scale(@[23]x)?.:format([\\w]+)', (req, res, next) => {
const item = repo[req.params.id];
if (!item || !item.spritePath) {
return res.sendStatus(404);
}
const scale = req.params.scale;
const format = req.params.format;
const filename = `${item.spritePath + (scale || '')}.${format}`;
return fs.readFile(filename, (err, data) => {
if (err) {
console.log('Sprite load error:', filename);
return res.sendStatus(404);
} else {
if (format === 'json') res.header('Content-type', 'application/json');
if (format === 'png') res.header('Content-type', 'image/png');
return res.send(data);
}
});
});
return app;
},
remove: (repo, id) => {
delete repo[id];
},
add: (options, repo, params, id, publicUrl, reportTiles, reportFont) => {
const styleFile = path.resolve(options.paths.styles, params.style);
let styleFileData;
try {
styleFileData = fs.readFileSync(styleFile);
} catch (e) {
console.log('Error reading style file');
return false;
}
const validationErrors = validate(styleFileData);
if (validationErrors.length > 0) {
console.log(`The file "${params.style}" is not valid a valid style file:`);
for (const err of validationErrors) {
console.log(`${err.line}: ${err.message}`);
}
return false;
}
const styleJSON = JSON.parse(styleFileData);
for (const name of Object.keys(styleJSON.sources)) {
const source = styleJSON.sources[name];
const url = source.url;
if (url && url.lastIndexOf('mbtiles:', 0) === 0) {
let mbtilesFile = url.substring('mbtiles://'.length);
const fromData = mbtilesFile[0] === '{' &&
mbtilesFile[mbtilesFile.length - 1] === '}';
if (fromData) {
mbtilesFile = mbtilesFile.substr(1, mbtilesFile.length - 2);
const mapsTo = (params.mapping || {})[mbtilesFile];
if (mapsTo) {
mbtilesFile = mapsTo;
}
}
const identifier = reportTiles(mbtilesFile, fromData);
if (!identifier) {
return false;
}
source.url = `local://data/${identifier}.json`;
}
}
for (const obj of styleJSON.layers) {
if (obj['type'] === 'symbol') {
const fonts = (obj['layout'] || {})['text-font'];
if (fonts && fonts.length) {
fonts.forEach(reportFont);
} else {
reportFont('Open Sans Regular');
reportFont('Arial Unicode MS Regular');
} }
} }
const identifier = reportTiles(mbtilesFile, fromData);
source.url = `local://data/${identifier}.json`;
} }
}
for(let obj of styleJSON.layers) { let spritePath;
if (obj['type'] === 'symbol') {
const fonts = (obj['layout'] || {})['text-font']; if (styleJSON.sprite && !httpTester.test(styleJSON.sprite)) {
if (fonts && fonts.length) { spritePath = path.join(options.paths.sprites,
fonts.forEach(reportFont); styleJSON.sprite
} else { .replace('{style}', path.basename(styleFile, '.json'))
reportFont('Open Sans Regular'); .replace('{styleJsonFolder}', path.relative(options.paths.sprites, path.dirname(styleFile)))
reportFont('Arial Unicode MS Regular'); );
} styleJSON.sprite = `local://styles/${id}/sprite`;
}
if (styleJSON.glyphs && !httpTester.test(styleJSON.glyphs)) {
styleJSON.glyphs = 'local://fonts/{fontstack}/{range}.pbf';
} }
}
let spritePath; repo[id] = {
styleJSON,
const httpTester = /^(http(s)?:)?\/\//; spritePath,
if (styleJSON.sprite && !httpTester.test(styleJSON.sprite)) { publicUrl,
spritePath = path.join(options.paths.sprites, name: styleJSON.name
styleJSON.sprite
.replace('{style}', path.basename(styleFile, '.json'))
.replace('{styleJsonFolder}', path.relative(options.paths.sprites, path.dirname(styleFile)))
);
styleJSON.sprite = `local://styles/${id}/sprite`;
}
if (styleJSON.glyphs && !httpTester.test(styleJSON.glyphs)) {
styleJSON.glyphs = 'local://fonts/{fontstack}/{range}.pbf';
}
repo[id] = styleJSON;
app.get(`/${id}/style.json`, (req, res, next) => {
const fixUrl = (url, opt_nokey) => {
if (!url || (typeof url !== 'string') || url.indexOf('local://') !== 0) {
return url;
}
const queryParams = [];
if (!opt_nokey && req.query.key) {
queryParams.unshift(`key=${req.query.key}`);
}
let query = '';
if (queryParams.length) {
query = `?${queryParams.join('&')}`;
}
return url.replace(
'local://', utils.getPublicUrl(publicUrl, req)) + query;
}; };
const styleJSON_ = clone(styleJSON); return true;
for (const name of Object.keys(styleJSON_.sources)) { }
const source = styleJSON_.sources[name];
source.url = fixUrl(source.url);
}
// mapbox-gl-js viewer cannot handle sprite urls with query
if (styleJSON_.sprite) {
styleJSON_.sprite = fixUrl(styleJSON_.sprite, true);
}
if (styleJSON_.glyphs) {
styleJSON_.glyphs = fixUrl(styleJSON_.glyphs, false);
}
return res.send(styleJSON_);
});
app.get(`/${id}/sprite:scale(@[23]x)?.:format([\\w]+)`,
(req, res, next) => {
if (!spritePath) {
return res.status(404).send('File not found');
}
const scale = req.params.scale,
format = req.params.format;
const filename = `${spritePath + (scale || '')}.${format}`;
return fs.readFile(filename, (err, data) => {
if (err) {
console.log('Sprite load error:', filename);
return res.status(404).send('File not found');
} else {
if (format === 'json') res.header('Content-type', 'application/json');
if (format === 'png') res.header('Content-type', 'image/png');
return res.send(data);
}
});
});
return Promise.resolve(app);
}; };

View file

@ -1,43 +1,45 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict'; 'use strict';
import os from 'os';
process.env.UV_THREADPOOL_SIZE = process.env.UV_THREADPOOL_SIZE =
Math.ceil(Math.max(4, require('os').cpus().length * 1.5)); Math.ceil(Math.max(4, os.cpus().length * 1.5));
const fs = require('fs'); import fs from 'node:fs';
const path = require('path'); import path from 'path';
const clone = require('clone'); import chokidar from 'chokidar';
const cors = require('cors'); import clone from 'clone';
const enableShutdown = require('http-shutdown'); import cors from 'cors';
const express = require('express'); import enableShutdown from 'http-shutdown';
const handlebars = require('handlebars'); import express from 'express';
const mercator = new (require('@mapbox/sphericalmercator'))(); import handlebars from 'handlebars';
const morgan = require('morgan'); import SphericalMercator from '@mapbox/sphericalmercator';
const mercator = new SphericalMercator();
import morgan from 'morgan';
import {serve_data} from './serve_data.js';
import {serve_style} from './serve_style.js';
import {serve_font} from './serve_font.js';
import {getTileUrls, getPublicUrl} from './utils.js';
const packageJson = require('../package'); import {fileURLToPath} from 'url';
const serve_font = require('./serve_font'); const __filename = fileURLToPath(import.meta.url);
const serve_style = require('./serve_style'); const __dirname = path.dirname(__filename);
const serve_data = require('./serve_data'); const packageJson = JSON.parse(fs.readFileSync(__dirname + '/../package.json', 'utf8'));
const utils = require('./utils');
let serve_rendered = null;
const isLight = packageJson.name.slice(-6) === '-light'; const isLight = packageJson.name.slice(-6) === '-light';
if (!isLight) { const serve_rendered = (await import(`${!isLight ? `./serve_rendered.js` : `./serve_light.js`}`)).serve_rendered;
// do not require `serve_rendered` in the light package
serve_rendered = require('./serve_rendered');
}
function start(opts) { function start(opts) {
console.log('Starting server'); console.log('Starting server');
const app = express().disable('x-powered-by'), const app = express().disable('x-powered-by');
serving = { const serving = {
styles: {}, styles: {},
rendered: {}, rendered: {},
data: {}, data: {},
fonts: {} fonts: {}
}; };
app.enable('trust proxy'); app.enable('trust proxy');
@ -45,7 +47,7 @@ function start(opts) {
const defaultLogFormat = process.env.NODE_ENV === 'production' ? 'tiny' : 'dev'; const defaultLogFormat = process.env.NODE_ENV === 'production' ? 'tiny' : 'dev';
const logFormat = opts.logFormat || defaultLogFormat; const logFormat = opts.logFormat || defaultLogFormat;
app.use(morgan(logFormat, { app.use(morgan(logFormat, {
stream: opts.logFile ? fs.createWriteStream(opts.logFile, { flags: 'a' }) : process.stdout, stream: opts.logFile ? fs.createWriteStream(opts.logFile, {flags: 'a'}) : process.stdout,
skip: (req, res) => opts.silent && (res.statusCode === 200 || res.statusCode === 304) skip: (req, res) => opts.silent && (res.statusCode === 200 || res.statusCode === 304)
})); }));
} }
@ -55,7 +57,7 @@ function start(opts) {
if (opts.configPath) { if (opts.configPath) {
configPath = path.resolve(opts.configPath); configPath = path.resolve(opts.configPath);
try { try {
config = clone(require(configPath)); config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
} catch (e) { } catch (e) {
console.log('ERROR: Config file not found or invalid!'); console.log('ERROR: Config file not found or invalid!');
console.log(' See README.md for instructions and sample data.'); console.log(' See README.md for instructions and sample data.');
@ -77,10 +79,11 @@ function start(opts) {
paths.fonts = path.resolve(paths.root, paths.fonts || ''); paths.fonts = path.resolve(paths.root, paths.fonts || '');
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 || '');
paths.icons = path.resolve(paths.root, paths.icons || '');
const startupPromises = []; const startupPromises = [];
const checkPath = type => { const checkPath = (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]}).`);
process.exit(1); process.exit(1);
@ -90,6 +93,36 @@ function start(opts) {
checkPath('fonts'); checkPath('fonts');
checkPath('sprites'); checkPath('sprites');
checkPath('mbtiles'); checkPath('mbtiles');
checkPath('icons');
/**
* Recursively get all files within a directory.
* Inspired by https://stackoverflow.com/a/45130990/10133863
* @param {String} directory Absolute path to a directory to get files from.
*/
const getFiles = async (directory) => {
// Fetch all entries of the directory and attach type information
const dirEntries = await fs.promises.readdir(directory, { withFileTypes: true });
// Iterate through entries and return the relative file-path to the icon directory if it is not a directory
// otherwise initiate a recursive call
const files = await Promise.all(dirEntries.map((dirEntry) => {
const entryPath = path.resolve(directory, dirEntry.name);
return dirEntry.isDirectory() ?
getFiles(entryPath) : entryPath.replace(paths.icons + path.sep, "");
}));
// Flatten the list of files to a single array
return files.flat();
}
// Load all available icons into a settings object
startupPromises.push(new Promise(resolve => {
getFiles(paths.icons).then((files) => {
paths.availableIcons = files;
resolve();
});
}));
if (options.dataDecorator) { if (options.dataDecorator) {
try { try {
@ -103,52 +136,59 @@ function start(opts) {
app.use(cors()); app.use(cors());
} }
for (const id of Object.keys(config.styles || {})) { app.use('/data/', serve_data.init(options, serving.data));
const item = config.styles[id]; app.use('/styles/', serve_style.init(options, serving.styles));
if (!item.style || item.style.length === 0) { if (!isLight) {
console.log(`Missing "style" property for ${id}`); startupPromises.push(
continue; serve_rendered.init(options, serving.rendered)
} .then((sub) => {
app.use('/styles/', sub);
})
);
}
const addStyle = (id, item, allowMoreData, reportFonts) => {
let success = true;
if (item.serve_data !== false) { if (item.serve_data !== false) {
startupPromises.push(serve_style(options, serving.styles, item, id, opts.publicUrl, success = serve_style.add(options, serving.styles, item, id, opts.publicUrl,
(mbtiles, fromData) => { (mbtiles, fromData) => {
let dataItemId; let dataItemId;
for (const id of Object.keys(data)) { for (const id of Object.keys(data)) {
if (fromData) { if (fromData) {
if (id === mbtiles) { if (id === mbtiles) {
dataItemId = id; dataItemId = id;
} }
} else { } else {
if (data[id].mbtiles === mbtiles) { if (data[id].mbtiles === mbtiles) {
dataItemId = id; dataItemId = id;
}
} }
} }
} if (dataItemId) { // mbtiles exist in the data config
if (dataItemId) { // mbtiles exist in the data config return dataItemId;
return dataItemId; } else {
} else if (fromData) { if (fromData || !allowMoreData) {
console.log(`ERROR: data "${mbtiles}" not found!`); console.log(`ERROR: style "${item.style}" using unknown mbtiles "${mbtiles}"! Skipping...`);
process.exit(1); return undefined;
} else { } else {
let id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles; let id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles;
while (data[id]) id += '_'; while (data[id]) id += '_';
data[id] = { data[id] = {
'mbtiles': mbtiles 'mbtiles': mbtiles
}; };
return id; return id;
} }
}, font => { }
serving.fonts[font] = true; }, (font) => {
}).then(sub => { if (reportFonts) {
app.use('/styles/', sub); serving.fonts[font] = true;
})); }
});
} }
if (item.serve_rendered !== false) { if (success && item.serve_rendered !== false) {
if (serve_rendered) { if (!isLight) {
startupPromises.push( startupPromises.push(serve_rendered.add(options, serving.rendered, item, id, opts.publicUrl,
serve_rendered(options, serving.rendered, item, id, opts.publicUrl, (mbtiles) => {
mbtiles => {
let mbtilesFile; let mbtilesFile;
for (const id of Object.keys(data)) { for (const id of Object.keys(data)) {
if (id === mbtiles) { if (id === mbtiles) {
@ -157,20 +197,27 @@ function start(opts) {
} }
return mbtilesFile; return mbtilesFile;
} }
).then(sub => { ));
app.use('/styles/', sub);
})
);
} else { } else {
item.serve_rendered = false; item.serve_rendered = false;
} }
} }
};
for (const id of Object.keys(config.styles || {})) {
const item = config.styles[id];
if (!item.style || item.style.length === 0) {
console.log(`Missing "style" property for ${id}`);
continue;
}
addStyle(id, item, true, true);
} }
startupPromises.push( startupPromises.push(
serve_font(options, serving.fonts).then(sub => { serve_font(options, serving.fonts).then((sub) => {
app.use('/', sub); app.use('/', sub);
}) })
); );
for (const id of Object.keys(data)) { for (const id of Object.keys(data)) {
@ -181,22 +228,61 @@ function start(opts) {
} }
startupPromises.push( startupPromises.push(
serve_data(options, serving.data, item, id, serving.styles, opts.publicUrl).then(sub => { serve_data.add(options, serving.data, item, id, opts.publicUrl)
app.use('/data/', sub);
})
); );
} }
if (options.serveAllStyles) {
fs.readdir(options.paths.styles, {withFileTypes: true}, (err, files) => {
if (err) {
return;
}
for (const file of files) {
if (file.isFile() &&
path.extname(file.name).toLowerCase() == '.json') {
const id = path.basename(file.name, '.json');
const item = {
style: file.name
};
addStyle(id, item, false, false);
}
}
});
const watcher = chokidar.watch(path.join(options.paths.styles, '*.json'),
{
});
watcher.on('all',
(eventType, filename) => {
if (filename) {
const id = path.basename(filename, '.json');
console.log(`Style "${id}" changed, updating...`);
serve_style.remove(serving.styles, id);
if (!isLight) {
serve_rendered.remove(serving.rendered, id);
}
if (eventType == 'add' || eventType == 'change') {
const item = {
style: filename
};
addStyle(id, item, false, false);
}
}
});
}
app.get('/styles.json', (req, res, next) => { app.get('/styles.json', (req, res, next) => {
const result = []; const result = [];
const query = req.query.key ? (`?key=${req.query.key}`) : ''; const query = req.query.key ? (`?key=${encodeURIComponent(req.query.key)}`) : '';
for (const id of Object.keys(serving.styles)) { for (const id of Object.keys(serving.styles)) {
const styleJSON = serving.styles[id]; const styleJSON = serving.styles[id].styleJSON;
result.push({ result.push({
version: styleJSON.version, version: styleJSON.version,
name: styleJSON.name, name: styleJSON.name,
id: id, id: id,
url: `${utils.getPublicUrl(opts.publicUrl, req)}styles/${id}/style.json${query}` url: `${getPublicUrl(opts.publicUrl, req)}styles/${id}/style.json${query}`
}); });
} }
res.send(result); res.send(result);
@ -204,14 +290,14 @@ function start(opts) {
const addTileJSONs = (arr, req, type) => { const addTileJSONs = (arr, req, type) => {
for (const id of Object.keys(serving[type])) { for (const id of Object.keys(serving[type])) {
const info = clone(serving[type][id]); const info = clone(serving[type][id].tileJSON);
let path = ''; let path = '';
if (type === 'rendered') { if (type === 'rendered') {
path = `styles/${id}`; path = `styles/${id}`;
} else { } else {
path = `${type}/${id}`; path = `${type}/${id}`;
} }
info.tiles = utils.getTileUrls(req, info.tiles, path, info.format, opts.publicUrl, { info.tiles = getTileUrls(req, info.tiles, path, info.format, opts.publicUrl, {
'pbf': options.pbfAlias 'pbf': options.pbfAlias
}); });
arr.push(info); arr.push(info);
@ -229,7 +315,7 @@ function start(opts) {
res.send(addTileJSONs(addTileJSONs([], req, 'rendered'), req, 'data')); res.send(addTileJSONs(addTileJSONs([], req, 'rendered'), req, 'data'));
}); });
//------------------------------------ // ------------------------------------
// serve web presentations // serve web presentations
app.use('/', express.static(path.join(__dirname, '../public/resources'))); app.use('/', express.static(path.join(__dirname, '../public/resources')));
@ -265,8 +351,8 @@ function start(opts) {
data['public_url'] = opts.publicUrl || '/'; data['public_url'] = opts.publicUrl || '/';
data['is_light'] = isLight; data['is_light'] = isLight;
data['key_query_part'] = data['key_query_part'] =
req.query.key ? `key=${req.query.key}&amp;` : ''; req.query.key ? `key=${encodeURIComponent(req.query.key)}&amp;` : '';
data['key_query'] = req.query.key ? `?key=${req.query.key}` : ''; data['key_query'] = req.query.key ? `?key=${encodeURIComponent(req.query.key)}` : '';
if (template === 'wmts') res.set('Content-Type', 'text/xml'); if (template === 'wmts') res.set('Content-Type', 'text/xml');
return res.status(200).send(compiled(data)); return res.status(200).send(compiled(data));
}); });
@ -275,15 +361,15 @@ function start(opts) {
})); }));
}; };
serveTemplate('/$', 'index', req => { serveTemplate('/$', 'index', (req) => {
const styles = clone(config.styles || {}); const styles = clone(serving.styles || {});
for (const id of Object.keys(styles)) { for (const id of Object.keys(styles)) {
const style = styles[id]; const style = styles[id];
style.name = (serving.styles[id] || serving.rendered[id] || {}).name; style.name = (serving.styles[id] || serving.rendered[id] || {}).name;
style.serving_data = serving.styles[id]; style.serving_data = serving.styles[id];
style.serving_rendered = serving.rendered[id]; style.serving_rendered = serving.rendered[id];
if (style.serving_rendered) { if (style.serving_rendered) {
const center = style.serving_rendered.center; const center = style.serving_rendered.tileJSON.center;
if (center) { if (center) {
style.viewer_hash = `#${center[2]}/${center[1].toFixed(5)}/${center[0].toFixed(5)}`; style.viewer_hash = `#${center[2]}/${center[1].toFixed(5)}/${center[0].toFixed(5)}`;
@ -291,29 +377,30 @@ function start(opts) {
style.thumbnail = `${center[2]}/${Math.floor(centerPx[0] / 256)}/${Math.floor(centerPx[1] / 256)}.png`; style.thumbnail = `${center[2]}/${Math.floor(centerPx[0] / 256)}/${Math.floor(centerPx[1] / 256)}.png`;
} }
style.xyz_link = utils.getTileUrls( style.xyz_link = getTileUrls(
req, style.serving_rendered.tiles, req, style.serving_rendered.tileJSON.tiles,
`styles/${id}`, style.serving_rendered.format, opts.publicUrl)[0]; `styles/${id}`, style.serving_rendered.tileJSON.format, opts.publicUrl)[0];
} }
} }
const data = clone(serving.data || {}); const data = clone(serving.data || {});
for (const id of Object.keys(data)) { for (const id of Object.keys(data)) {
const data_ = data[id]; const data_ = data[id];
const center = data_.center; const tilejson = data[id].tileJSON;
const center = tilejson.center;
if (center) { if (center) {
data_.viewer_hash = `#${center[2]}/${center[1].toFixed(5)}/${center[0].toFixed(5)}`; data_.viewer_hash = `#${center[2]}/${center[1].toFixed(5)}/${center[0].toFixed(5)}`;
} }
data_.is_vector = data_.format === 'pbf'; data_.is_vector = tilejson.format === 'pbf';
if (!data_.is_vector) { if (!data_.is_vector) {
if (center) { if (center) {
const centerPx = mercator.px([center[0], center[1]], center[2]); const centerPx = mercator.px([center[0], center[1]], center[2]);
data_.thumbnail = `${center[2]}/${Math.floor(centerPx[0] / 256)}/${Math.floor(centerPx[1] / 256)}.${data_.format}`; data_.thumbnail = `${center[2]}/${Math.floor(centerPx[0] / 256)}/${Math.floor(centerPx[1] / 256)}.${data_.tileJSON.format}`;
} }
data_.xyz_link = utils.getTileUrls( data_.xyz_link = getTileUrls(
req, data_.tiles, `data/${id}`, data_.format, opts.publicUrl, { req, tilejson.tiles, `data/${id}`, tilejson.format, opts.publicUrl, {
'pbf': options.pbfAlias 'pbf': options.pbfAlias
})[0]; })[0];
} }
if (data_.filesize) { if (data_.filesize) {
let suffix = 'kB'; let suffix = 'kB';
@ -335,9 +422,9 @@ function start(opts) {
}; };
}); });
serveTemplate('/styles/:id/$', 'viewer', req => { serveTemplate('/styles/:id/$', 'viewer', (req) => {
const id = req.params.id; const id = req.params.id;
const style = clone((config.styles || {})[id]); const style = clone(((serving.styles || {})[id] || {}).styleJSON);
if (!style) { if (!style) {
return null; return null;
} }
@ -353,29 +440,34 @@ function start(opts) {
return res.redirect(301, '/styles/' + req.params.id + '/'); return res.redirect(301, '/styles/' + req.params.id + '/');
}); });
*/ */
serveTemplate('/styles/:id/wmts.xml', 'wmts', req => { serveTemplate('/styles/:id/wmts.xml', 'wmts', (req) => {
const id = req.params.id; const id = req.params.id;
const wmts = clone((config.styles || {})[id]); const wmts = clone((serving.styles || {})[id]);
if (!wmts) { if (!wmts) {
return null; return null;
} }
if (wmts.hasOwnProperty("serve_rendered") && !wmts.serve_rendered) { if (wmts.hasOwnProperty('serve_rendered') && !wmts.serve_rendered) {
return null; return null;
} }
wmts.id = id; wmts.id = id;
wmts.name = (serving.styles[id] || serving.rendered[id]).name; wmts.name = (serving.styles[id] || serving.rendered[id]).name;
wmts.baseUrl = `${req.get('X-Forwarded-Protocol') ? req.get('X-Forwarded-Protocol') : req.protocol}://${req.get('host')}`; if (opts.publicUrl) {
wmts.baseUrl = opts.publicUrl;
}
else {
wmts.baseUrl = `${req.get('X-Forwarded-Protocol') ? req.get('X-Forwarded-Protocol') : req.protocol}://${req.get('host')}/`;
}
return wmts; return wmts;
}); });
serveTemplate('/data/:id/$', 'data', req => { serveTemplate('/data/:id/$', 'data', (req) => {
const id = req.params.id; const id = req.params.id;
const data = clone(serving.data[id]); const data = clone(serving.data[id]);
if (!data) { if (!data) {
return null; return null;
} }
data.id = id; data.id = id;
data.is_vector = data.format === 'pbf'; data.is_vector = data.tileJSON.format === 'pbf';
return data; return data;
}); });
@ -392,7 +484,7 @@ function start(opts) {
} }
}); });
const server = app.listen(process.env.PORT || opts.port, process.env.BIND || opts.bind, function () { const server = app.listen(process.env.PORT || opts.port, process.env.BIND || opts.bind, function() {
let address = this.address().address; let address = this.address().address;
if (address.indexOf('::') === 0) { if (address.indexOf('::') === 0) {
address = `[${address}]`; // literal IPv6 address address = `[${address}]`; // literal IPv6 address
@ -410,10 +502,10 @@ function start(opts) {
}; };
} }
module.exports = opts => { export function server(opts) {
const running = start(opts); const running = start(opts);
running.startupPromise.catch(err => { running.startupPromise.catch((err) => {
console.error(err.message); console.error(err.message);
process.exit(1); process.exit(1);
}); });
@ -426,10 +518,6 @@ module.exports = opts => {
console.log('Stopping server and reloading config'); console.log('Stopping server and reloading config');
running.server.shutdown(() => { running.server.shutdown(() => {
for (const key in require.cache) {
delete require.cache[key];
}
const restarted = start(opts); const restarted = start(opts);
running.server = restarted.server; running.server = restarted.server;
running.app = restarted.app; running.app = restarted.app;

View file

@ -1,16 +1,15 @@
'use strict'; 'use strict';
const path = require('path'); import path from 'path';
const fs = require('fs'); import fs from 'node:fs';
const clone = require('clone'); import clone from 'clone';
const glyphCompose = require('glyph-pbf-composite'); import glyphCompose from '@mapbox/glyph-pbf-composite';
module.exports.getPublicUrl = (publicUrl, req) => publicUrl || `${req.protocol}://${req.headers.host}/`; export const getPublicUrl = (publicUrl, req) => publicUrl || `${req.protocol}://${req.headers.host}/`;
module.exports.getTileUrls = (req, domains, path, format, publicUrl, aliases) => {
export const getTileUrls = (req, domains, path, format, publicUrl, aliases) => {
if (domains) { if (domains) {
if (domains.constructor === String && domains.length > 0) { if (domains.constructor === String && domains.length > 0) {
domains = domains.split(','); domains = domains.split(',');
@ -37,13 +36,12 @@ module.exports.getTileUrls = (req, domains, path, format, publicUrl, aliases) =>
domains = [req.headers.host]; domains = [req.headers.host];
} }
const key = req.query.key;
const queryParams = []; const queryParams = [];
if (req.query.key) { if (req.query.key) {
queryParams.push(`key=${req.query.key}`); queryParams.push(`key=${encodeURIComponent(req.query.key)}`);
} }
if (req.query.style) { if (req.query.style) {
queryParams.push(`style=${req.query.style}`); queryParams.push(`style=${encodeURIComponent(req.query.style)}`);
} }
const query = queryParams.length > 0 ? (`?${queryParams.join('&')}`) : ''; const query = queryParams.length > 0 ? (`?${queryParams.join('&')}`) : '';
@ -57,13 +55,13 @@ module.exports.getTileUrls = (req, domains, path, format, publicUrl, aliases) =>
uris.push(`${req.protocol}://${domain}/${path}/{z}/{x}/{y}.${format}${query}`); uris.push(`${req.protocol}://${domain}/${path}/{z}/{x}/{y}.${format}${query}`);
} }
} else { } else {
uris.push(`${publicUrl}${path}/{z}/{x}/{y}.${format}${query}`) uris.push(`${publicUrl}${path}/{z}/{x}/{y}.${format}${query}`);
} }
return uris; return uris;
}; };
module.exports.fixTileJSONCenter = tileJSON => { export const fixTileJSONCenter = (tileJSON) => {
if (tileJSON.bounds && !tileJSON.center) { if (tileJSON.bounds && !tileJSON.center) {
const fitWidth = 1024; const fitWidth = 1024;
const tiles = fitWidth / 256; const tiles = fitWidth / 256;
@ -71,8 +69,8 @@ module.exports.fixTileJSONCenter = tileJSON => {
(tileJSON.bounds[0] + tileJSON.bounds[2]) / 2, (tileJSON.bounds[0] + tileJSON.bounds[2]) / 2,
(tileJSON.bounds[1] + tileJSON.bounds[3]) / 2, (tileJSON.bounds[1] + tileJSON.bounds[3]) / 2,
Math.round( Math.round(
-Math.log((tileJSON.bounds[2] - tileJSON.bounds[0]) / 360 / tiles) / -Math.log((tileJSON.bounds[2] - tileJSON.bounds[0]) / 360 / tiles) /
Math.LN2 Math.LN2
) )
]; ];
} }
@ -118,14 +116,14 @@ const getFontPbf = (allowedFonts, fontPath, name, range, fallbacks) => new Promi
} }
}); });
module.exports.getFontsPbf = (allowedFonts, fontPath, names, range, fallbacks) => { export const getFontsPbf = (allowedFonts, fontPath, names, range, fallbacks) => {
const fonts = names.split(','); const fonts = names.split(',');
const queue = []; const queue = [];
for (const font of fonts) { for (const font of fonts) {
queue.push( queue.push(
getFontPbf(allowedFonts, fontPath, font, range, clone(allowedFonts || fallbacks)) getFontPbf(allowedFonts, fontPath, font, range, clone(allowedFonts || fallbacks))
); );
} }
return Promise.all(queue).then(values => glyphCompose.combine(values)); return Promise.all(queue).then((values) => glyphCompose.combine(values));
}; };

View file

@ -1,38 +1,38 @@
var testTileJSONArray = function(url) { const testTileJSONArray = function(url) {
describe(url + ' is array of TileJSONs', function() { describe(url + ' is array of TileJSONs', function() {
it('is json', function(done) { it('is json', function(done) {
supertest(app) supertest(app)
.get(url) .get(url)
.expect(200) .expect(200)
.expect('Content-Type', /application\/json/, done); .expect('Content-Type', /application\/json/, done);
}); });
it('is non-empty array', function(done) { it('is non-empty array', function(done) {
supertest(app) supertest(app)
.get(url) .get(url)
.expect(function(res) { .expect(function(res) {
res.body.should.be.Array(); expect(res.body).to.be.a('array');
res.body.length.should.be.greaterThan(0); expect(res.body.length).to.be.greaterThan(0);
}).end(done); }).end(done);
}); });
}); });
}; };
var testTileJSON = function(url) { const testTileJSON = function(url) {
describe(url + ' is TileJSON', function() { describe(url + ' is TileJSON', function() {
it('is json', function(done) { it('is json', function(done) {
supertest(app) supertest(app)
.get(url) .get(url)
.expect(200) .expect(200)
.expect('Content-Type', /application\/json/, done); .expect('Content-Type', /application\/json/, done);
}); });
it('has valid tiles', function(done) { it('has valid tiles', function(done) {
supertest(app) supertest(app)
.get(url) .get(url)
.expect(function(res) { .expect(function(res) {
res.body.tiles.length.should.be.greaterThan(0); expect(res.body.tiles.length).to.be.greaterThan(0);
}).end(done); }).end(done);
}); });
}); });
}; };
@ -41,8 +41,8 @@ describe('Metadata', function() {
describe('/health', function() { describe('/health', function() {
it('returns 200', function(done) { it('returns 200', function(done) {
supertest(app) supertest(app)
.get('/health') .get('/health')
.expect(200, done); .expect(200, done);
}); });
}); });
@ -53,21 +53,21 @@ describe('Metadata', function() {
describe('/styles.json is valid array', function() { describe('/styles.json is valid array', function() {
it('is json', function(done) { it('is json', function(done) {
supertest(app) supertest(app)
.get('/styles.json') .get('/styles.json')
.expect(200) .expect(200)
.expect('Content-Type', /application\/json/, done); .expect('Content-Type', /application\/json/, done);
}); });
it('contains valid item', function(done) { it('contains valid item', function(done) {
supertest(app) supertest(app)
.get('/styles.json') .get('/styles.json')
.expect(function(res) { .expect(function(res) {
res.body.should.be.Array(); expect(res.body).to.be.a('array');
res.body.length.should.be.greaterThan(0); expect(res.body.length).to.be.greaterThan(0);
res.body[0].version.should.equal(8); expect(res.body[0].version).to.be.equal(8);
res.body[0].id.should.be.String(); expect(res.body[0].id).to.be.a('string');
res.body[0].name.should.be.String(); expect(res.body[0].name).to.be.a('string');
}).end(done); }).end(done);
}); });
}); });

View file

@ -1,12 +1,16 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
global.should = require('should'); import {expect} from 'chai';
global.supertest = require('supertest'); import supertest from 'supertest';
import {server} from '../src/server.js';
global.expect = expect;
global.supertest = supertest;
before(function() { before(function() {
console.log('global setup'); console.log('global setup');
process.chdir('test_data'); process.chdir('test_data');
var running = require('../src/server')({ const running = server({
configPath: 'config.json', configPath: 'config.json',
port: 8888, port: 8888,
publicUrl: '/test/' publicUrl: '/test/'
@ -18,5 +22,7 @@ before(function() {
after(function() { after(function() {
console.log('global teardown'); console.log('global teardown');
global.server.close(function() { console.log('Done'); process.exit(); }); global.server.close(function() {
console.log('Done'); process.exit();
});
}); });

View file

@ -1,18 +1,18 @@
var testStatic = function(prefix, q, format, status, scale, type, query) { const testStatic = function(prefix, q, format, status, scale, type, query) {
if (scale) q += '@' + scale + 'x'; if (scale) q += '@' + scale + 'x';
var path = '/styles/' + prefix + '/static/' + q + '.' + format; let path = '/styles/' + prefix + '/static/' + q + '.' + format;
if (query) { if (query) {
path += query; path += query;
} }
it(path + ' returns ' + status, function(done) { it(path + ' returns ' + status, function(done) {
var test = supertest(app).get(path); const test = supertest(app).get(path);
if (status) test.expect(status); if (status) test.expect(status);
if (type) test.expect('Content-Type', type); if (type) test.expect('Content-Type', type);
test.end(done); test.end(done);
}); });
}; };
var prefix = 'test-style'; const prefix = 'test-style';
describe('Static endpoints', function() { describe('Static endpoints', function() {
describe('center-based', function() { describe('center-based', function() {
@ -95,7 +95,7 @@ describe('Static endpoints', function() {
describe('invalid requests return 4xx', function() { describe('invalid requests return 4xx', function() {
testStatic(prefix, 'auto/256x256', 'png', 400); testStatic(prefix, 'auto/256x256', 'png', 400);
testStatic(prefix, 'auto/256x256', 'png', 400, undefined, undefined, '?path=10,10'); testStatic(prefix, 'auto/256x256', 'png', 400, undefined, undefined, '?path=invalid');
testStatic(prefix, 'auto/2560x2560', 'png', 400, undefined, undefined, '?path=10,10|20,20'); testStatic(prefix, 'auto/2560x2560', 'png', 400, undefined, undefined, '?path=10,10|20,20');
}); });
}); });

View file

@ -1,14 +1,14 @@
var testIs = function(url, type, status) { const testIs = function(url, type, status) {
it(url + ' return ' + (status || 200) + ' and is ' + type.toString(), it(url + ' return ' + (status || 200) + ' and is ' + type.toString(),
function(done) { function(done) {
supertest(app) supertest(app)
.get(url) .get(url)
.expect(status || 200) .expect(status || 200)
.expect('Content-Type', type, done); .expect('Content-Type', type, done);
}); });
}; };
var prefix = 'test-style'; const prefix = 'test-style';
describe('Styles', function() { describe('Styles', function() {
describe('/styles/' + prefix + '/style.json is valid style', function() { describe('/styles/' + prefix + '/style.json is valid style', function() {
@ -16,16 +16,16 @@ describe('Styles', function() {
it('contains expected properties', function(done) { it('contains expected properties', function(done) {
supertest(app) supertest(app)
.get('/styles/' + prefix + '/style.json') .get('/styles/' + prefix + '/style.json')
.expect(function(res) { .expect(function(res) {
res.body.version.should.equal(8); expect(res.body.version).to.be.equal(8);
res.body.name.should.be.String(); expect(res.body.name).to.be.a('string');
res.body.sources.should.be.Object(); expect(res.body.sources).to.be.a('object');
res.body.glyphs.should.be.String(); expect(res.body.glyphs).to.be.a('string');
res.body.sprite.should.be.String(); expect(res.body.sprite).to.be.a('string');
res.body.sprite.should.equal('/test/styles/test-style/sprite'); expect(res.body.sprite).to.be.equal('/test/styles/test-style/sprite');
res.body.layers.should.be.Array(); expect(res.body.layers).to.be.a('array');
}).end(done); }).end(done);
}); });
}); });
describe('/styles/streets/style.json is not served', function() { describe('/styles/streets/style.json is not served', function() {
@ -44,7 +44,7 @@ describe('Fonts', function() {
testIs('/fonts/Open Sans Bold/0-255.pbf', /application\/x-protobuf/); testIs('/fonts/Open Sans Bold/0-255.pbf', /application\/x-protobuf/);
testIs('/fonts/Open Sans Regular/65280-65535.pbf', /application\/x-protobuf/); testIs('/fonts/Open Sans Regular/65280-65535.pbf', /application\/x-protobuf/);
testIs('/fonts/Open Sans Bold,Open Sans Regular/0-255.pbf', testIs('/fonts/Open Sans Bold,Open Sans Regular/0-255.pbf',
/application\/x-protobuf/); /application\/x-protobuf/);
testIs('/fonts/Nonsense,Open Sans Bold/0-255.pbf', /./, 400); testIs('/fonts/Nonsense,Open Sans Bold/0-255.pbf', /./, 400);
testIs('/fonts/Nonsense/0-255.pbf', /./, 400); testIs('/fonts/Nonsense/0-255.pbf', /./, 400);

View file

@ -1,14 +1,14 @@
var testTile = function(prefix, z, x, y, status) { const testTile = function(prefix, z, x, y, status) {
var path = '/data/' + prefix + '/' + z + '/' + x + '/' + y + '.pbf'; const path = '/data/' + prefix + '/' + z + '/' + x + '/' + y + '.pbf';
it(path + ' returns ' + status, function(done) { it(path + ' returns ' + status, function(done) {
var test = supertest(app).get(path); const test = supertest(app).get(path);
if (status) test.expect(status); if (status) test.expect(status);
if (status == 200) test.expect('Content-Type', /application\/x-protobuf/); if (status == 200) test.expect('Content-Type', /application\/x-protobuf/);
test.end(done); test.end(done);
}); });
}; };
var prefix = 'openmaptiles'; const prefix = 'openmaptiles';
describe('Vector tiles', function() { describe('Vector tiles', function() {
describe('existing tiles', function() { describe('existing tiles', function() {

View file

@ -1,15 +1,15 @@
var testTile = function(prefix, z, x, y, format, status, scale, type) { const testTile = function(prefix, z, x, y, format, status, scale, type) {
if (scale) y += '@' + scale + 'x'; if (scale) y += '@' + scale + 'x';
var path = '/styles/' + prefix + '/' + z + '/' + x + '/' + y + '.' + format; const 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); const test = supertest(app).get(path);
test.expect(status); test.expect(status);
if (type) test.expect('Content-Type', type); if (type) test.expect('Content-Type', type);
test.end(done); test.end(done);
}); });
}; };
var prefix = 'test-style'; const prefix = 'test-style';
describe('Raster tiles', function() { describe('Raster tiles', function() {
describe('valid requests', function() { describe('valid requests', function() {
@ -41,6 +41,6 @@ describe('Raster tiles', function() {
testTile(prefix, 0, 0, 0, 'png', 404, 1); testTile(prefix, 0, 0, 0, 'png', 404, 1);
testTile(prefix, 0, 0, 0, 'png', 404, 5); testTile(prefix, 0, 0, 0, 'png', 404, 5);
//testTile('hybrid', 0, 0, 0, 'png', 404); //TODO: test this // testTile('hybrid', 0, 0, 0, 'png', 404); //TODO: test this
}); });
}); });