Merge remote-tracking branch 'upstream/master' into publish_workflow

This commit is contained in:
acalcutt 2022-12-09 13:22:59 -05:00
commit a1af3631ca
40 changed files with 17957 additions and 841 deletions

32
.eslintrc.cjs Normal file
View file

@ -0,0 +1,32 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
es6: true,
},
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 2020,
sourceType: 'module',
lib: ['es2020'],
ecmaFeatures: {
jsx: true,
tsx: true,
},
},
plugins: ['prettier', 'jsdoc', 'security'],
extends: [
'prettier',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:prettier/recommended',
'plugin:jsdoc/recommended',
'plugin:security/recommended',
],
// add your custom rules here
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
},
};

11
.gitattributes vendored Normal file
View file

@ -0,0 +1,11 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# behavior for Unix scripts
#
# Unix scripts are treated as binary by default.
###############################################################################
*.sh eol=lf

19
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,19 @@
version: 2
updates:
- package-ecosystem: npm
versioning-strategy: increase
directory: '/'
schedule:
interval: daily
commit-message:
prefix: fix
prefix-development: chore
include: scope
- package-ecosystem: github-actions
directory: '/'
schedule:
interval: weekly
commit-message:
prefix: fix
prefix-development: chore
include: scope

35
.github/workflows/codeql.yml vendored Normal file
View file

@ -0,0 +1,35 @@
on:
push:
branches:
- master
pull_request:
branches:
- master
schedule:
- cron: '45 23 * * 2'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [javascript]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
queries: +security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: '/language:${{ matrix.language }}'

57
.github/workflows/ct.yml vendored Normal file
View file

@ -0,0 +1,57 @@
name: 'Continuous Testing'
on:
push:
branches:
- master
pull_request:
branches:
- master
permissions:
checks: write
contents: read
jobs:
ct:
runs-on: ubuntu-20.04
steps:
- name: Check out repository ✨ (non-dependabot)
if: ${{ github.actor != 'dependabot[bot]' }}
uses: actions/checkout@v3
- name: Check out repository 🎉 (dependabot)
if: ${{ github.actor == 'dependabot[bot]' }}
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Update apt-get 🚀
run: sudo apt-get update -qq
- name: Install dependencies (Ubuntu) 🚀
run: >-
sudo apt-get install -qq libcairo2-dev libjpeg8-dev libpango1.0-dev
libgif-dev build-essential g++ xvfb libgles2-mesa-dev libgbm-dev
libxxf86vm-dev
- name: Setup node env 📦
uses: actions/setup-node@v3
with:
node-version-file: 'package.json'
check-latest: true
cache: 'npm'
- name: Install dependencies 🚀
run: npm ci --prefer-offline --no-audit --omit=optional
- name: Pull test data 📦
run: >-
wget -O test_data.zip
https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip
- name: Prepare test data 📦
run: unzip -q test_data.zip -d test_data
- name: Run tests 🧪
run: xvfb-run --server-args="-screen 0 1024x768x24" npm test

103
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,103 @@
name: "Build, Test, Release"
on:
workflow_dispatch:
inputs:
docker_user:
description: 'Docker Username'
required: true
docker_token:
description: 'Docker Token'
required: true
npm_token:
description: 'NPM Token'
required: true
jobs:
release:
name: "Build, Test, Publish"
runs-on: ubuntu-20.04
steps:
- name: Check out repository ✨
uses: actions/checkout@v3
- name: Update apt-get 🚀
run: sudo apt-get update -qq
- name: Install dependencies (Ubuntu) 🚀
run: >-
sudo apt-get install -qq libcairo2-dev libjpeg8-dev libpango1.0-dev
libgif-dev build-essential g++ xvfb libgles2-mesa-dev libgbm-dev
libxxf86vm-dev
- name: Setup node env 📦
uses: actions/setup-node@v3
with:
node-version-file: 'package.json'
check-latest: true
cache: 'npm'
- name: Install dependencies 🚀
run: npm ci --prefer-offline --no-audit --omit=optional
- name: Pull test data 📦
run: >-
wget -O test_data.zip
https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip
- name: Prepare test data 📦
run: unzip -q test_data.zip -d test_data
- name: Run tests 🧪
run: xvfb-run --server-args="-screen 0 1024x768x24" npm test
- name: Remove Test Data
run: rm -R test_data*
- name: Publish to Full Version NPM
run: |
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
npm publish --access public
env:
NPM_TOKEN: ${{ github.event.inputs.npm_token }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ github.event.inputs.docker_user }}
password: ${{ github.event.inputs.docker_token }}
- name: Build and publish Full Version to Docker Hub
uses: docker/build-push-action@v3
with:
context: .
push: true
tags: maptiler/tileserver-gl:latest, maptiler/tileserver-gl:${{ env.PACKAGE_VERSION }}
platforms: linux/amd64
- name: Create Tileserver Light Directory
run: node publish.js --no-publish
- name: Install node dependencies
run: npm install
working-directory: ./light
- name: Publish to Light Version NPM
working-directory: ./light
run: |
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
npm publish --access public
env:
NPM_TOKEN: ${{ github.event.inputs.npm_token }}
- name: Build and publish Light Version to Docker Hub
uses: docker/build-push-action@v3
with:
context: ./light
file: ./light/Dockerfile
push: true
tags: maptiler/tileserver-gl-light:latest, maptiler/tileserver-gl-light:${{ env.PACKAGE_VERSION }}
platforms: linux/amd64

21
.husky/commit-msg Executable file
View file

@ -0,0 +1,21 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
NAME=$(git config user.name)
EMAIL=$(git config user.email)
if [ -z "$NAME" ]; then
echo "empty git config user.name"
exit 1
fi
if [ -z "$EMAIL" ]; then
echo "empty git config user.email"
exit 1
fi
git interpret-trailers --if-exists doNothing --trailer \
"Signed-off-by: $NAME <$EMAIL>" \
--in-place "$1"
npm exec --no -- commitlint --edit $1

4
.husky/pre-push Executable file
View file

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm exec --no -- lint-staged --no-stash

View file

@ -1,21 +0,0 @@
language: node_js
node_js:
- "10"
env:
- CXX=g++-4.8
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8
before_install:
- sudo apt-get update -qq
- sudo apt-get install -qq libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev build-essential g++
- sudo apt-get install -qq xvfb libgles2-mesa-dev libgbm-dev libxxf86vm-dev
install:
- npm install
- wget -O test_data.zip https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip
- unzip -q test_data.zip -d test_data
script:
- xvfb-run --server-args="-screen 0 1024x768x24" npm test

View file

@ -30,9 +30,9 @@ RUN set -ex; \
rm -rf /var/lib/apt/lists/*; rm -rf /var/lib/apt/lists/*;
RUN mkdir -p /usr/src/app RUN mkdir -p /usr/src/app
COPY package.json /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 FROM ubuntu:focal AS final
@ -72,11 +72,14 @@ COPY --from=builder /usr/src/app /usr/src/app
COPY . /usr/src/app COPY . /usr/src/app
RUN mkdir -p /data && chown node:node /data
VOLUME /data VOLUME /data
WORKDIR /data WORKDIR /data
EXPOSE 80 EXPOSE 8080
USER node:node USER node:node
ENTRYPOINT ["/usr/src/app/docker-entrypoint.sh"] ENTRYPOINT ["/usr/src/app/docker-entrypoint.sh"]
HEALTHCHECK CMD node /usr/src/app/src/healthcheck.js

View file

@ -20,13 +20,16 @@ RUN set -ex; \
apt-get clean; \ apt-get clean; \
rm -rf /var/lib/apt/lists/*; rm -rf /var/lib/apt/lists/*;
EXPOSE 80 EXPOSE 8080
RUN mkdir -p /data && chown node:node /data
VOLUME /data VOLUME /data
WORKDIR /data WORKDIR /data
ENTRYPOINT ["/usr/src/app/docker-entrypoint.sh"] 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"] RUN ["chmod", "+x", "/usr/src/app/docker-entrypoint.sh"]
USER node:node USER node:node
HEALTHCHECK CMD node /usr/src/app/src/healthcheck.js

View file

@ -44,7 +44,7 @@ An alternative to npm to start the packed software easier is to install [Docker]
Example using a mbtiles file Example using a mbtiles file
```bash ```bash
wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/zurich_switzerland.mbtiles 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 docker run --rm -it -v $(pwd):/data -p 8080:8080 maptiler/tileserver-gl --mbtiles zurich_switzerland.mbtiles
[in your browser, visit http://[server ip]:8080] [in your browser, visit http://[server ip]:8080]
``` ```
@ -52,13 +52,13 @@ Example using a config.json + style + mbtiles file
```bash ```bash
wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip
unzip test_data.zip unzip test_data.zip
docker run --rm -it -v $(pwd):/data -p 8080:80 maptiler/tileserver-gl docker run --rm -it -v $(pwd):/data -p 8080:8080 maptiler/tileserver-gl
[in your browser, visit http://[server ip]:8080] [in your browser, visit http://[server ip]:8080]
``` ```
Example using a different path Example using a different path
```bash ```bash
docker run --rm -it -v /your/local/config/path:/data -p 8080:80 maptiler/tileserver-gl docker run --rm -it -v /your/local/config/path:/data -p 8080:8080 maptiler/tileserver-gl
``` ```
replace '/your/local/config/path' with the path to your config file replace '/your/local/config/path' with the path to your config file

3
commitlint.config.cjs Normal file
View file

@ -0,0 +1,3 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
};

View file

@ -20,7 +20,7 @@ trap refresh HUP
if ! which -- "${1}"; then if ! which -- "${1}"; then
# first arg is not an executable # first arg is not an executable
xvfb-run -a --server-args="-screen 0 1024x768x24" -- node /usr/src/app/ -p 80 "$@" & xvfb-run -a --server-args="-screen 0 1024x768x24" -- node /usr/src/app/ "$@" &
# Wait exits immediately on signals which have traps set. Store return value and wait # Wait exits immediately on signals which have traps set. Store return value and wait
# again for all jobs to actually complete before continuing. # again for all jobs to actually complete before continuing.
wait $! || RETVAL=$? wait $! || RETVAL=$?

View file

@ -20,7 +20,7 @@ trap refresh HUP
if ! which -- "${1}"; then if ! which -- "${1}"; then
# first arg is not an executable # first arg is not an executable
node /usr/src/app/ -p 80 "$@" & node /usr/src/app/ "$@" &
# Wait exits immediately on signals which have traps set. Store return value and wait # Wait exits immediately on signals which have traps set. Store return value and wait
# again for all jobs to actually complete before continuing. # again for all jobs to actually complete before continuing.
wait $! || RETVAL=$? wait $! || RETVAL=$?

View file

@ -14,6 +14,7 @@ Example:
"root": "", "root": "",
"fonts": "fonts", "fonts": "fonts",
"sprites": "sprites", "sprites": "sprites",
"icons": "icons",
"styles": "styles", "styles": "styles",
"mbtiles": "" "mbtiles": ""
}, },
@ -31,6 +32,7 @@ Example:
"serveAllFonts": false, "serveAllFonts": false,
"serveAllStyles": false, "serveAllStyles": false,
"serveStaticMaps": true, "serveStaticMaps": true,
"allowRemoteMarkerIcons": true,
"tileMargin": 0 "tileMargin": 0
}, },
"styles": { "styles": {
@ -141,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``
========== ==========

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,7 +7,7 @@ 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 maptiler/tileserver-gl``. Just run ``docker run --rm -it -v $(pwd):/data -p 8080:8080 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:

View file

@ -36,3 +36,9 @@ It is possible to reload the configuration file without restarting the whole pro
- The `docker kill -s HUP tileserver-gl` command can be used when running the tileserver-gl docker container. - 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. - The `docker-compose kill -s HUP tileserver-gl-service-name` can be used when tileserver-gl is run as a docker-compose service.
Docker and `--port`
======
When running tileserver-gl in a Docker container, using the `--port` option would make the container incorrectly seem unhealthy.
Instead, it is advised to use Docker's port mapping and map the default port 8080 to the desired external port.

4
lint-staged.config.cjs Normal file
View file

@ -0,0 +1,4 @@
module.exports = {
'*.{js,ts}': 'npm run lint:js',
'*.{yml}': 'npm run lint:yml',
};

15852
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,21 +1,21 @@
{ {
"name": "tileserver-gl", "name": "tileserver-gl",
"version": "4.1.1", "version": "4.2.1",
"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", "type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/maptiler/tileserver-gl.git"
},
"license": "BSD-2-Clause",
"engines": {
"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 .)" "lint:yml": "yamllint --schema=CORE_SCHEMA *.{yml,yaml}",
"lint:js": "npm run lint:eslint && npm run lint:prettier",
"lint:js:fix": "npm run lint:eslint:fix && npm run lint:prettier:fix",
"lint:eslint": "eslint \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs}\" --ignore-path .gitignore",
"lint:eslint:fix": "eslint --fix \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs}\" --ignore-path .gitignore",
"lint:prettier": "prettier --check \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs,json}\" --ignore-path .gitignore",
"lint:prettier:fix": "prettier --write \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs,json}\" --ignore-path .gitignore",
"docker": "docker build -f Dockerfile . && docker run --rm -i -p 8080:8080 $(docker build -q .)",
"prepare": "node -e \"if (process.env.NODE_ENV !== 'production'){ process.exit(1) } \" || husky install"
}, },
"dependencies": { "dependencies": {
"@mapbox/glyph-pbf-composite": "0.0.3", "@mapbox/glyph-pbf-composite": "0.0.3",
@ -29,9 +29,9 @@
"chokidar": "3.5.3", "chokidar": "3.5.3",
"clone": "2.1.2", "clone": "2.1.2",
"color": "4.2.3", "color": "4.2.3",
"commander": "9.4.0", "commander": "9.4.1",
"cors": "2.8.5", "cors": "2.8.5",
"express": "4.18.1", "express": "4.18.2",
"handlebars": "4.7.7", "handlebars": "4.7.7",
"http-shutdown": "1.2.2", "http-shutdown": "1.2.2",
"morgan": "1.10.0", "morgan": "1.10.0",
@ -39,11 +39,44 @@
"proj4": "2.8.0", "proj4": "2.8.0",
"request": "2.88.2", "request": "2.88.2",
"sharp": "0.31.0", "sharp": "0.31.0",
"tileserver-gl-styles": "2.0.0" "tileserver-gl-styles": "2.0.0",
"sanitize-filename": "1.6.3"
}, },
"devDependencies": { "devDependencies": {
"chai": "4.3.6", "@commitlint/cli": "^17.3.0",
"@commitlint/config-conventional": "^17.3.0",
"@typescript-eslint/eslint-plugin": "^5.46.0",
"@typescript-eslint/parser": "^5.38.0",
"chai": "4.3.7",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-jsdoc": "^39.6.4",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-security": "^1.5.0",
"husky": "^8.0.1",
"lint-staged": "^13.1.0",
"mocha": "^10.0.0", "mocha": "^10.0.0",
"supertest": "^6.2.4" "prettier": "^2.8.1",
} "should": "^13.2.3",
"supertest": "^6.3.3",
"yaml-lint": "^1.7.0"
},
"keywords": [
"maptiler",
"tileserver-gl",
"maplibre-gl",
"tileserver"
],
"license": "BSD-2-Clause",
"engines": {
"node": ">=14.15.0 <17"
},
"repository": {
"url": "git+https://github.com/maptiler/tileserver-gl.git",
"type": "git"
},
"bugs": {
"url": "https://github.com/maptiler/tileserver-gl/issues"
},
"homepage": "https://github.com/maptiler/tileserver-gl#readme"
} }

13
prettier.config.cjs Normal file
View file

@ -0,0 +1,13 @@
module.exports = {
$schema: 'http://json.schemastore.org/prettierrc',
semi: true,
arrowParens: 'always',
singleQuote: true,
trailingComma: 'all',
bracketSpacing: true,
htmlWhitespaceSensitivity: 'css',
insertPragma: false,
tabWidth: 2,
useTabs: false,
endOfLine: 'lf',
};

View file

@ -12,25 +12,33 @@
// SYNC THE `light` FOLDER // SYNC THE `light` FOLDER
import child_process from 'child_process' import child_process from 'child_process';
child_process.execSync('rsync -av --exclude="light" --exclude=".git" --exclude="node_modules" --delete . light', { child_process.execSync(
stdio: 'inherit' 'rsync -av --exclude="light" --exclude=".git" --exclude="node_modules" --delete . light',
}); {
stdio: 'inherit',
},
);
// PATCH `package.json` // PATCH `package.json`
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import {fileURLToPath} from 'url'; import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const packageJson = JSON.parse(fs.readFileSync(__dirname + '/package.json', 'utf8')) 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['@maplibre/maplibre-gl-native']; delete packageJson.dependencies['@maplibre/maplibre-gl-native'];
delete packageJson.dependencies['sharp']; delete packageJson.dependencies['sharp'];
delete packageJson.scripts['prepare'];
delete packageJson.optionalDependencies; delete packageJson.optionalDependencies;
delete packageJson.devDependencies; delete packageJson.devDependencies;
@ -51,10 +59,10 @@ if (process.argv.length > 2 && process.argv[2] == '--no-publish') {
// tileserver-gl // tileserver-gl
child_process.execSync('npm publish . --access public', { child_process.execSync('npm publish . --access public', {
stdio: 'inherit' stdio: 'inherit',
}); });
// tileserver-gl-light // tileserver-gl-light
child_process.execSync('npm publish ./light --access public', { child_process.execSync('npm publish ./light --access public', {
stdio: 'inherit' stdio: 'inherit',
}); });

2
run.sh
View file

@ -29,7 +29,7 @@ export DISPLAY=:${displayNumber}.${screenNumber}
echo echo
cd /data cd /data
node /usr/src/app/ -p 80 "$@" & node /usr/src/app/ "$@" &
child=$! child=$!
wait "$child" wait "$child"

18
src/healthcheck.js Normal file
View file

@ -0,0 +1,18 @@
import * as http from 'http';
var options = {
timeout: 2000,
};
var url = 'http://localhost:8080/health';
var request = http.request(url, options, (res) => {
console.log(`STATUS: ${res.statusCode}`);
if (res.statusCode == 200) {
process.exit(0);
} else {
process.exit(1);
}
});
request.on('error', function (err) {
console.log('ERROR');
process.exit(1);
});
request.end();

View file

@ -4,73 +4,52 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'path'; import path from 'path';
import {fileURLToPath} from 'url'; import { fileURLToPath } from 'url';
import request from 'request'; import request from 'request';
import {server} from './server.js'; import { server } from './server.js';
import MBTiles from '@mapbox/mbtiles'; import MBTiles from '@mapbox/mbtiles';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const packageJson = JSON.parse(fs.readFileSync(__dirname + '/../package.json', 'utf8')); 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');
} }
import {program} from 'commander'; import { program } from 'commander';
program program
.description('tileserver-gl startup options') .description('tileserver-gl startup options')
.usage('tileserver-gl [mbtiles] [options]') .usage('tileserver-gl [mbtiles] [options]')
.option( .option(
'--mbtiles <file>', '--mbtiles <file>',
'MBTiles file (uses demo configuration);\n' + 'MBTiles file (uses demo configuration);\n' +
'\t ignored if the configuration file is also specified' '\t ignored if the configuration file is also specified',
) )
.option( .option(
'-c, --config <file>', '-c, --config <file>',
'Configuration file [config.json]', 'Configuration file [config.json]',
'config.json' 'config.json',
)
.option(
'-b, --bind <address>',
'Bind address'
)
.option(
'-p, --port <port>',
'Port [8080]',
8080,
parseInt
)
.option(
'-C|--no-cors',
'Disable Cross-origin resource sharing headers'
) )
.option('-b, --bind <address>', 'Bind address')
.option('-p, --port <port>', 'Port [8080]', 8080, parseInt)
.option('-C|--no-cors', 'Disable Cross-origin resource sharing headers')
.option( .option(
'-u|--public_url <url>', '-u|--public_url <url>',
'Enable exposing the server on subpaths, not necessarily the root of the domain' 'Enable exposing the server on subpaths, not necessarily the root of the domain',
)
.option(
'-V, --verbose',
'More verbose output'
)
.option(
'-s, --silent',
'Less verbose output'
)
.option(
'-l|--log_file <file>',
'output log file (defaults to standard out)'
) )
.option('-V, --verbose', 'More verbose output')
.option('-s, --silent', 'Less verbose output')
.option('-l|--log_file <file>', 'output log file (defaults to standard out)')
.option( .option(
'-f|--log_format <format>', '-f|--log_format <format>',
'define the log format: https://github.com/expressjs/morgan#morganformat-options' 'define the log format: https://github.com/expressjs/morgan#morganformat-options',
)
.version(
packageJson.version,
'-v, --version'
) )
.version(packageJson.version, '-v, --version');
program.parse(process.argv); program.parse(process.argv);
const opts = program.opts(); const opts = program.opts();
@ -91,14 +70,16 @@ const startServer = (configPath, config) => {
silent: opts.silent, silent: opts.silent,
logFile: opts.log_file, logFile: opts.log_file,
logFormat: opts.log_format, logFormat: opts.log_format,
publicUrl: publicUrl publicUrl: publicUrl,
}); });
}; };
const startWithMBTiles = (mbtilesFile) => { const startWithMBTiles = (mbtilesFile) => {
console.log(`[INFO] Automatically creating config file for ${mbtilesFile}`); console.log(`[INFO] Automatically creating config file for ${mbtilesFile}`);
console.log(`[INFO] Only a basic preview style will be used.`); console.log(`[INFO] Only a basic preview style will be used.`);
console.log(`[INFO] See documentation to learn how to create config.json file.`); console.log(
`[INFO] See documentation to learn how to create config.json file.`,
);
mbtilesFile = path.resolve(process.cwd(), mbtilesFile); mbtilesFile = path.resolve(process.cwd(), mbtilesFile);
@ -110,60 +91,70 @@ const startWithMBTiles = (mbtilesFile) => {
const instance = new MBTiles(mbtilesFile + '?mode=ro', (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.`);
process.exit(1); process.exit(1);
} }
instance.getInfo((err, info) => { instance.getInfo((err, info) => {
if (err || !info) { if (err || !info) {
console.log('ERROR: Metadata missing in the MBTiles.'); console.log('ERROR: Metadata missing in the MBTiles.');
console.log(` Make sure ${path.basename(mbtilesFile)} is valid MBTiles.`); console.log(
`Make sure ${path.basename(mbtilesFile)} is valid MBTiles.`,
);
process.exit(1); process.exit(1);
} }
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': {}, },
'data': {} styles: {},
data: {},
}; };
if (info.format === 'pbf' && if (
info.name.toLowerCase().indexOf('openmaptiles') > -1) { info.format === 'pbf' &&
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 (const 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,
} },
}; };
} }
} }
} else { } else {
console.log(`WARN: MBTiles not in "openmaptiles" format. Serving raw data only...`); console.log(
config['data'][(info.id || 'mbtiles') `WARN: MBTiles not in "openmaptiles" format. Serving raw data only...`,
);
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),
}; };
} }
@ -197,7 +188,8 @@ 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/maptiler/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

@ -10,13 +10,15 @@ import MBTiles from '@mapbox/mbtiles';
import Pbf from 'pbf'; import Pbf from 'pbf';
import VectorTile from '@mapbox/vector-tile'; import VectorTile from '@mapbox/vector-tile';
import {getTileUrls, fixTileJSONCenter} from './utils.js'; import { getTileUrls, fixTileJSONCenter } from './utils.js';
export const serve_data = { export const serve_data = {
init: (options, repo) => { init: (options, repo) => {
const app = express().disable('x-powered-by'); const app = express().disable('x-powered-by');
app.get('/:id/:z(\\d+)/:x(\\d+)/:y(\\d+).:format([\\w.]+)', (req, res, next) => { app.get(
'/:id/:z(\\d+)/:x(\\d+)/:y(\\d+).:format([\\w.]+)',
(req, res, next) => {
const item = repo[req.params.id]; const item = repo[req.params.id];
if (!item) { if (!item) {
return res.sendStatus(404); return res.sendStatus(404);
@ -29,13 +31,21 @@ export const serve_data = {
if (format === options.pbfAlias) { if (format === options.pbfAlias) {
format = 'pbf'; format = 'pbf';
} }
if (format !== tileJSONFormat && if (
!(format === 'geojson' && tileJSONFormat === 'pbf')) { format !== tileJSONFormat &&
!(format === 'geojson' && tileJSONFormat === 'pbf')
) {
return res.status(404).send('Invalid format'); return res.status(404).send('Invalid format');
} }
if (z < item.tileJSON.minzoom || 0 || x < 0 || y < 0 || if (
z < item.tileJSON.minzoom ||
0 ||
x < 0 ||
y < 0 ||
z > item.tileJSON.maxzoom || z > item.tileJSON.maxzoom ||
x >= Math.pow(2, z) || y >= Math.pow(2, z)) { x >= Math.pow(2, z) ||
y >= Math.pow(2, z)
) {
return res.status(404).send('Out of bounds'); return res.status(404).send('Out of bounds');
} }
item.source.getTile(z, x, y, (err, data, headers) => { item.source.getTile(z, x, y, (err, data, headers) => {
@ -44,15 +54,18 @@ export const serve_data = {
if (/does not exist/.test(err.message)) { if (/does not exist/.test(err.message)) {
return res.status(204).send(); return res.status(204).send();
} else { } else {
return res.status(500).send(err.message); return res
.status(500)
.header('Content-Type', 'text/plain')
.send(err.message);
} }
} else { } else {
if (data == null) { if (data == null) {
return res.status(404).send('Not found'); return res.status(404).send('Not found');
} else { } else {
if (tileJSONFormat === 'pbf') { if (tileJSONFormat === 'pbf') {
isGzipped = data.slice(0, 2).indexOf( isGzipped =
Buffer.from([0x1f, 0x8b])) === 0; data.slice(0, 2).indexOf(Buffer.from([0x1f, 0x8b])) === 0;
if (options.dataDecoratorFunc) { if (options.dataDecoratorFunc) {
if (isGzipped) { if (isGzipped) {
data = zlib.unzipSync(data); data = zlib.unzipSync(data);
@ -73,8 +86,8 @@ export const serve_data = {
const tile = new VectorTile(new Pbf(data)); const tile = new VectorTile(new Pbf(data));
const geojson = { const geojson = {
'type': 'FeatureCollection', type: 'FeatureCollection',
'features': [] features: [],
}; };
for (const layerName in tile.layers) { for (const layerName in tile.layers) {
const layer = tile.layers[layerName]; const layer = tile.layers[layerName];
@ -100,7 +113,8 @@ export const serve_data = {
} }
} }
}); });
}); },
);
app.get('/:id.json', (req, res, next) => { app.get('/:id.json', (req, res, next) => {
const item = repo[req.params.id]; const item = repo[req.params.id];
@ -108,10 +122,16 @@ export const serve_data = {
return res.sendStatus(404); return res.sendStatus(404);
} }
const info = clone(item.tileJSON); const info = clone(item.tileJSON);
info.tiles = getTileUrls(req, info.tiles, info.tiles = getTileUrls(
`data/${req.params.id}`, info.format, item.publicUrl, { req,
'pbf': options.pbfAlias info.tiles,
}); `data/${req.params.id}`,
info.format,
item.publicUrl,
{
pbf: options.pbfAlias,
},
);
return res.send(info); return res.send(info);
}); });
@ -120,7 +140,7 @@ export const serve_data = {
add: (options, repo, params, id, publicUrl) => { add: (options, repo, params, id, publicUrl) => {
const mbtilesFile = path.resolve(options.paths.mbtiles, params.mbtiles); const mbtilesFile = path.resolve(options.paths.mbtiles, params.mbtiles);
let tileJSON = { let tileJSON = {
'tiles': params.domains || options.domains tiles: params.domains || options.domains,
}; };
const mbtilesFileStats = fs.statSync(mbtilesFile); const mbtilesFileStats = fs.statSync(mbtilesFile);
@ -129,7 +149,7 @@ export const serve_data = {
} }
let source; let source;
const sourceInfoPromise = new Promise((resolve, reject) => { const sourceInfoPromise = new Promise((resolve, reject) => {
source = new MBTiles(mbtilesFile + '?mode=ro', err => { source = new MBTiles(mbtilesFile + '?mode=ro', (err) => {
if (err) { if (err) {
reject(err); reject(err);
return; return;
@ -164,8 +184,8 @@ export const serve_data = {
repo[id] = { repo[id] = {
tileJSON, tileJSON,
publicUrl, publicUrl,
source source,
}; };
}); });
} },
}; };

View file

@ -4,7 +4,7 @@ import express from 'express';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'path'; import path from 'path';
import {getFontsPbf} from './utils.js'; import { getFontsPbf } from './utils.js';
export const serve_font = (options, allowedFonts) => { export const serve_font = (options, allowedFonts) => {
const app = express().disable('x-powered-by'); const app = express().disable('x-powered-by');
@ -26,8 +26,10 @@ export const serve_font = (options, allowedFonts) => {
reject(err); reject(err);
return; return;
} }
if (stats.isDirectory() && if (
fs.existsSync(path.join(fontPath, file, '0-255.pbf'))) { stats.isDirectory() &&
fs.existsSync(path.join(fontPath, file, '0-255.pbf'))
) {
existingFonts[path.basename(file)] = true; existingFonts[path.basename(file)] = true;
} }
}); });
@ -40,19 +42,26 @@ export const serve_font = (options, allowedFonts) => {
const fontstack = decodeURI(req.params.fontstack); const fontstack = decodeURI(req.params.fontstack);
const range = req.params.range; const range = req.params.range;
getFontsPbf(options.serveAllFonts ? null : allowedFonts, getFontsPbf(
fontPath, fontstack, range, existingFonts).then((concated) => { options.serveAllFonts ? null : allowedFonts,
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).header('Content-Type', 'text/plain').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(),
); );
}); });

View file

@ -1,10 +1,9 @@
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/no-unused-vars */
'use strict'; 'use strict';
export const serve_rendered = { export const serve_rendered = {
init: (options, repo) => { init: (options, repo) => {},
}, add: (options, repo, params, id, publicUrl, dataResolver) => {},
add: (options, repo, params, id, publicUrl, dataResolver) => { remove: (repo, id) => {},
},
remove: (repo, id) => {
}
}; };

File diff suppressed because it is too large Load diff

View file

@ -5,14 +5,14 @@ import fs from 'node:fs';
import clone from 'clone'; import clone from 'clone';
import express from 'express'; import express from 'express';
import {validate} from '@maplibre/maplibre-gl-style-spec'; import { validate } from '@maplibre/maplibre-gl-style-spec';
import {getPublicUrl} from './utils.js'; import { getPublicUrl } from './utils.js';
const httpTester = /^(http(s)?:)?\/\//; const httpTester = /^(http(s)?:)?\/\//;
const fixUrl = (req, url, publicUrl, opt_nokey) => { const fixUrl = (req, url, publicUrl, opt_nokey) => {
if (!url || (typeof url !== 'string') || url.indexOf('local://') !== 0) { if (!url || typeof url !== 'string' || url.indexOf('local://') !== 0) {
return url; return url;
} }
const queryParams = []; const queryParams = [];
@ -23,8 +23,7 @@ const fixUrl = (req, url, publicUrl, opt_nokey) => {
if (queryParams.length) { if (queryParams.length) {
query = `?${queryParams.join('&')}`; query = `?${queryParams.join('&')}`;
} }
return url.replace( return url.replace('local://', getPublicUrl(publicUrl, req)) + query;
'local://', getPublicUrl(publicUrl, req)) + query;
}; };
export const serve_style = { export const serve_style = {
@ -43,10 +42,20 @@ export const serve_style = {
} }
// mapbox-gl-js viewer cannot handle sprite urls with query // mapbox-gl-js viewer cannot handle sprite urls with query
if (styleJSON_.sprite) { if (styleJSON_.sprite) {
styleJSON_.sprite = fixUrl(req, styleJSON_.sprite, item.publicUrl, false); styleJSON_.sprite = fixUrl(
req,
styleJSON_.sprite,
item.publicUrl,
false,
);
} }
if (styleJSON_.glyphs) { if (styleJSON_.glyphs) {
styleJSON_.glyphs = fixUrl(req, styleJSON_.glyphs, item.publicUrl, false); styleJSON_.glyphs = fixUrl(
req,
styleJSON_.glyphs,
item.publicUrl,
false,
);
} }
return res.send(styleJSON_); return res.send(styleJSON_);
}); });
@ -89,7 +98,9 @@ export const serve_style = {
const validationErrors = validate(styleFileData); const validationErrors = validate(styleFileData);
if (validationErrors.length > 0) { if (validationErrors.length > 0) {
console.log(`The file "${params.style}" is not valid a valid style file:`); console.log(
`The file "${params.style}" is not valid a valid style file:`,
);
for (const err of validationErrors) { for (const err of validationErrors) {
console.log(`${err.line}: ${err.message}`); console.log(`${err.line}: ${err.message}`);
} }
@ -102,8 +113,8 @@ export const serve_style = {
const url = source.url; const url = source.url;
if (url && url.lastIndexOf('mbtiles:', 0) === 0) { if (url && url.lastIndexOf('mbtiles:', 0) === 0) {
let mbtilesFile = url.substring('mbtiles://'.length); let mbtilesFile = url.substring('mbtiles://'.length);
const fromData = mbtilesFile[0] === '{' && const fromData =
mbtilesFile[mbtilesFile.length - 1] === '}'; mbtilesFile[0] === '{' && mbtilesFile[mbtilesFile.length - 1] === '}';
if (fromData) { if (fromData) {
mbtilesFile = mbtilesFile.substr(1, mbtilesFile.length - 2); mbtilesFile = mbtilesFile.substr(1, mbtilesFile.length - 2);
@ -135,10 +146,14 @@ export const serve_style = {
let spritePath; let spritePath;
if (styleJSON.sprite && !httpTester.test(styleJSON.sprite)) { if (styleJSON.sprite && !httpTester.test(styleJSON.sprite)) {
spritePath = path.join(options.paths.sprites, spritePath = path.join(
options.paths.sprites,
styleJSON.sprite styleJSON.sprite
.replace('{style}', path.basename(styleFile, '.json')) .replace('{style}', path.basename(styleFile, '.json'))
.replace('{styleJsonFolder}', path.relative(options.paths.sprites, path.dirname(styleFile))) .replace(
'{styleJsonFolder}',
path.relative(options.paths.sprites, path.dirname(styleFile)),
),
); );
styleJSON.sprite = `local://styles/${id}/sprite`; styleJSON.sprite = `local://styles/${id}/sprite`;
} }
@ -150,9 +165,9 @@ export const serve_style = {
styleJSON, styleJSON,
spritePath, spritePath,
publicUrl, publicUrl,
name: styleJSON.name name: styleJSON.name,
}; };
return true; return true;
} },
}; };

View file

@ -2,8 +2,7 @@
'use strict'; 'use strict';
import os from 'os'; import os from 'os';
process.env.UV_THREADPOOL_SIZE = process.env.UV_THREADPOOL_SIZE = Math.ceil(Math.max(4, os.cpus().length * 1.5));
Math.ceil(Math.max(4, os.cpus().length * 1.5));
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'path'; import path from 'path';
@ -17,20 +16,28 @@ import handlebars from 'handlebars';
import SphericalMercator from '@mapbox/sphericalmercator'; import SphericalMercator from '@mapbox/sphericalmercator';
const mercator = new SphericalMercator(); const mercator = new SphericalMercator();
import morgan from 'morgan'; import morgan from 'morgan';
import {serve_data} from './serve_data.js'; import { serve_data } from './serve_data.js';
import {serve_style} from './serve_style.js'; import { serve_style } from './serve_style.js';
import {serve_font} from './serve_font.js'; import { serve_font } from './serve_font.js';
import {getTileUrls, getPublicUrl} from './utils.js'; import { getTileUrls, getPublicUrl } from './utils.js';
import {fileURLToPath} from 'url'; import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const packageJson = JSON.parse(fs.readFileSync(__dirname + '/../package.json', 'utf8')); const packageJson = JSON.parse(
fs.readFileSync(__dirname + '/../package.json', 'utf8'),
);
const isLight = packageJson.name.slice(-6) === '-light'; const isLight = packageJson.name.slice(-6) === '-light';
const serve_rendered = (await import(`${!isLight ? `./serve_rendered.js` : `./serve_light.js`}`)).serve_rendered; const serve_rendered = (
await import(`${!isLight ? `./serve_rendered.js` : `./serve_light.js`}`)
).serve_rendered;
export function server(opts) { /**
*
* @param 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');
@ -38,18 +45,24 @@ export function server(opts) {
styles: {}, styles: {},
rendered: {}, rendered: {},
data: {}, data: {},
fonts: {} fonts: {},
}; };
app.enable('trust proxy'); app.enable('trust proxy');
if (process.env.NODE_ENV !== 'test') { if (process.env.NODE_ENV !== 'test') {
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(
stream: opts.logFile ? fs.createWriteStream(opts.logFile, {flags: 'a'}) : process.stdout, morgan(logFormat, {
skip: (req, res) => opts.silent && (res.statusCode === 200 || res.statusCode === 304) stream: opts.logFile
})); ? fs.createWriteStream(opts.logFile, { flags: 'a' })
: process.stdout,
skip: (req, res) =>
opts.silent && (res.statusCode === 200 || res.statusCode === 304),
}),
);
} }
let config = opts.config || null; let config = opts.config || null;
@ -74,17 +87,21 @@ export function server(opts) {
options.paths = paths; options.paths = paths;
paths.root = path.resolve( paths.root = path.resolve(
configPath ? path.dirname(configPath) : process.cwd(), configPath ? path.dirname(configPath) : process.cwd(),
paths.root || ''); paths.root || '',
);
paths.styles = path.resolve(paths.root, paths.styles || ''); paths.styles = path.resolve(paths.root, paths.styles || '');
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);
} }
}; };
@ -92,10 +109,51 @@ export function server(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 {
options.dataDecoratorFunc = require(path.resolve(paths.root, options.dataDecorator)); options.dataDecoratorFunc = require(path.resolve(
paths.root,
options.dataDecorator,
));
} catch (e) {} } catch (e) {}
} }
@ -109,17 +167,21 @@ export function server(opts) {
app.use('/styles/', serve_style.init(options, serving.styles)); app.use('/styles/', serve_style.init(options, serving.styles));
if (!isLight) { if (!isLight) {
startupPromises.push( startupPromises.push(
serve_rendered.init(options, serving.rendered) serve_rendered.init(options, serving.rendered).then((sub) => {
.then((sub) => {
app.use('/styles/', sub); app.use('/styles/', sub);
}) }),
); );
} }
const addStyle = (id, item, allowMoreData, reportFonts) => { const addStyle = (id, item, allowMoreData, reportFonts) => {
let success = true; let success = true;
if (item.serve_data !== false) { if (item.serve_data !== false) {
success = serve_style.add(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)) {
@ -133,30 +195,41 @@ export function server(opts) {
} }
} }
} }
if (dataItemId) { // mbtiles exist in the data config if (dataItemId) {
// mbtiles exist in the data config
return dataItemId; return dataItemId;
} else { } else {
if (fromData || !allowMoreData) { if (fromData || !allowMoreData) {
console.log(`ERROR: style "${item.style}" using unknown mbtiles "${mbtiles}"! Skipping...`); console.log(
`ERROR: style "${item.style}" using unknown mbtiles "${mbtiles}"! Skipping...`,
);
return undefined; 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) => { },
(font) => {
if (reportFonts) { if (reportFonts) {
serving.fonts[font] = true; serving.fonts[font] = true;
} }
}); },
);
} }
if (success && item.serve_rendered !== false) { if (success && item.serve_rendered !== false) {
if (!isLight) { if (!isLight) {
startupPromises.push(serve_rendered.add(options, serving.rendered, item, id, opts.publicUrl, startupPromises.push(
serve_rendered.add(
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)) {
@ -165,8 +238,9 @@ export function server(opts) {
} }
} }
return mbtilesFile; return mbtilesFile;
} },
)); ),
);
} else { } else {
item.serve_rendered = false; item.serve_rendered = false;
} }
@ -186,7 +260,7 @@ export function server(opts) {
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)) {
@ -197,32 +271,31 @@ export function server(opts) {
} }
startupPromises.push( startupPromises.push(
serve_data.add(options, serving.data, item, id, opts.publicUrl) serve_data.add(options, serving.data, item, id, opts.publicUrl),
); );
} }
if (options.serveAllStyles) { if (options.serveAllStyles) {
fs.readdir(options.paths.styles, {withFileTypes: true}, (err, files) => { fs.readdir(options.paths.styles, { withFileTypes: true }, (err, files) => {
if (err) { if (err) {
return; return;
} }
for (const file of files) { for (const file of files) {
if (file.isFile() && if (file.isFile() && path.extname(file.name).toLowerCase() == '.json') {
path.extname(file.name).toLowerCase() == '.json') {
const id = path.basename(file.name, '.json'); const id = path.basename(file.name, '.json');
const item = { const item = {
style: file.name style: file.name,
}; };
addStyle(id, item, false, false); addStyle(id, item, false, false);
} }
} }
}); });
const watcher = chokidar.watch(path.join(options.paths.styles, '*.json'), const watcher = chokidar.watch(
{ path.join(options.paths.styles, '*.json'),
}); {},
watcher.on('all', );
(eventType, filename) => { watcher.on('all', (eventType, filename) => {
if (filename) { if (filename) {
const id = path.basename(filename, '.json'); const id = path.basename(filename, '.json');
console.log(`Style "${id}" changed, updating...`); console.log(`Style "${id}" changed, updating...`);
@ -234,7 +307,7 @@ export function server(opts) {
if (eventType == 'add' || eventType == 'change') { if (eventType == 'add' || eventType == 'change') {
const item = { const item = {
style: filename style: filename,
}; };
addStyle(id, item, false, false); addStyle(id, item, false, false);
} }
@ -244,14 +317,19 @@ export function server(opts) {
app.get('/styles.json', (req, res, next) => { app.get('/styles.json', (req, res, next) => {
const result = []; const result = [];
const query = req.query.key ? (`?key=${encodeURIComponent(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].styleJSON; 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: `${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);
@ -266,9 +344,16 @@ export function server(opts) {
} else { } else {
path = `${type}/${id}`; path = `${type}/${id}`;
} }
info.tiles = getTileUrls(req, info.tiles, path, info.format, opts.publicUrl, { info.tiles = getTileUrls(
'pbf': options.pbfAlias req,
}); info.tiles,
path,
info.format,
opts.publicUrl,
{
pbf: options.pbfAlias,
},
);
arr.push(info); arr.push(info);
} }
return arr; return arr;
@ -294,12 +379,15 @@ export function server(opts) {
if (template === 'index') { if (template === 'index') {
if (options.frontPage === false) { if (options.frontPage === false) {
return; return;
} else if (options.frontPage && } else if (
options.frontPage.constructor === String) { options.frontPage &&
options.frontPage.constructor === String
) {
templateFile = path.resolve(paths.root, options.frontPage); templateFile = path.resolve(paths.root, options.frontPage);
} }
} }
startupPromises.push(new Promise((resolve, reject) => { startupPromises.push(
new Promise((resolve, reject) => {
fs.readFile(templateFile, (err, content) => { fs.readFile(templateFile, (err, content) => {
if (err) { if (err) {
err = new Error(`Template not found: ${err.message}`); err = new Error(`Template not found: ${err.message}`);
@ -316,18 +404,24 @@ export function server(opts) {
return res.status(404).send('Not found'); return res.status(404).send('Not found');
} }
} }
data['server_version'] = `${packageJson.name} v${packageJson.version}`; data[
'server_version'
] = `${packageJson.name} v${packageJson.version}`;
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
req.query.key ? `key=${encodeURIComponent(req.query.key)}&amp;` : ''; ? `key=${encodeURIComponent(req.query.key)}&amp;`
data['key_query'] = req.query.key ? `?key=${encodeURIComponent(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));
}); });
resolve(); resolve();
}); });
})); }),
);
}; };
serveTemplate('/$', 'index', (req) => { serveTemplate('/$', 'index', (req) => {
@ -340,15 +434,23 @@ export function server(opts) {
if (style.serving_rendered) { if (style.serving_rendered) {
const center = style.serving_rendered.tileJSON.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)}`;
const centerPx = mercator.px([center[0], center[1]], center[2]); const centerPx = mercator.px([center[0], center[1]], center[2]);
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 = getTileUrls( style.xyz_link = getTileUrls(
req, style.serving_rendered.tileJSON.tiles, req,
`styles/${id}`, style.serving_rendered.tileJSON.format, opts.publicUrl)[0]; style.serving_rendered.tileJSON.tiles,
`styles/${id}`,
style.serving_rendered.tileJSON.format,
opts.publicUrl,
)[0];
} }
} }
const data = clone(serving.data || {}); const data = clone(serving.data || {});
@ -357,19 +459,29 @@ export function server(opts) {
const tilejson = data[id].tileJSON; const tilejson = data[id].tileJSON;
const center = tilejson.center; 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 = tilejson.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_.tileJSON.format}`; data_.thumbnail = `${center[2]}/${Math.floor(
centerPx[0] / 256,
)}/${Math.floor(centerPx[1] / 256)}.${data_.tileJSON.format}`;
} }
data_.xyz_link = getTileUrls( data_.xyz_link = getTileUrls(
req, tilejson.tiles, `data/${id}`, tilejson.format, opts.publicUrl, { req,
'pbf': options.pbfAlias tilejson.tiles,
})[0]; `data/${id}`,
tilejson.format,
opts.publicUrl,
{
pbf: options.pbfAlias,
},
)[0];
} }
if (data_.filesize) { if (data_.filesize) {
let suffix = 'kB'; let suffix = 'kB';
@ -387,7 +499,7 @@ export function server(opts) {
} }
return { return {
styles: Object.keys(styles).length ? styles : null, styles: Object.keys(styles).length ? styles : null,
data: Object.keys(data).length ? data : null data: Object.keys(data).length ? data : null,
}; };
}); });
@ -422,9 +534,12 @@ export function server(opts) {
wmts.name = (serving.styles[id] || serving.rendered[id]).name; wmts.name = (serving.styles[id] || serving.rendered[id]).name;
if (opts.publicUrl) { if (opts.publicUrl) {
wmts.baseUrl = opts.publicUrl; wmts.baseUrl = opts.publicUrl;
} } else {
else { wmts.baseUrl = `${
wmts.baseUrl = `${req.get('X-Forwarded-Protocol') ? req.get('X-Forwarded-Protocol') : req.protocol}://${req.get('host')}/`; req.get('X-Forwarded-Protocol')
? req.get('X-Forwarded-Protocol')
: req.protocol
}://${req.get('host')}/`;
} }
return wmts; return wmts;
}); });
@ -453,13 +568,17 @@ export function server(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
} }
console.log(`Listening at http://${address}:${this.address().port}/`); console.log(`Listening at http://${address}:${this.address().port}/`);
}); },
);
// add server.shutdown() to gracefully stop serving // add server.shutdown() to gracefully stop serving
enableShutdown(server); enableShutdown(server);
@ -467,11 +586,15 @@ export function server(opts) {
return { return {
app: app, app: app,
server: server, server: server,
startupPromise: startupPromise startupPromise: startupPromise,
}; };
} }
export const exports = (opts) => { /**
*
* @param opts
*/
export function server(opts) {
const running = start(opts); const running = start(opts);
running.startupPromise.catch((err) => { running.startupPromise.catch((err) => {
@ -487,10 +610,6 @@ export const 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;
@ -498,4 +617,4 @@ export const exports = (opts) => {
}); });
return running; return running;
}; }

View file

@ -6,8 +6,8 @@ import fs from 'node:fs';
import clone from 'clone'; import clone from 'clone';
import glyphCompose from '@mapbox/glyph-pbf-composite'; import glyphCompose from '@mapbox/glyph-pbf-composite';
export const getPublicUrl = (publicUrl, req) =>
export const getPublicUrl = (publicUrl, req) => publicUrl || `${req.protocol}://${req.headers.host}/`; publicUrl || `${req.protocol}://${req.headers.host}/`;
export const getTileUrls = (req, domains, path, format, publicUrl, aliases) => { export const getTileUrls = (req, domains, path, format, publicUrl, aliases) => {
if (domains) { if (domains) {
@ -16,7 +16,8 @@ export const getTileUrls = (req, domains, path, format, publicUrl, aliases) => {
} }
const host = req.headers.host; const host = req.headers.host;
const hostParts = host.split('.'); const hostParts = host.split('.');
const relativeSubdomainsUsable = hostParts.length > 1 && const relativeSubdomainsUsable =
hostParts.length > 1 &&
!/^([0-9]{1,3}\.){3}[0-9]{1,3}(\:[0-9]+)?$/.test(host); !/^([0-9]{1,3}\.){3}[0-9]{1,3}(\:[0-9]+)?$/.test(host);
const newDomains = []; const newDomains = [];
for (const domain of domains) { for (const domain of domains) {
@ -43,7 +44,7 @@ export const getTileUrls = (req, domains, path, format, publicUrl, aliases) => {
if (req.query.style) { if (req.query.style) {
queryParams.push(`style=${encodeURIComponent(req.query.style)}`); queryParams.push(`style=${encodeURIComponent(req.query.style)}`);
} }
const query = queryParams.length > 0 ? (`?${queryParams.join('&')}`) : ''; const query = queryParams.length > 0 ? `?${queryParams.join('&')}` : '';
if (aliases && aliases[format]) { if (aliases && aliases[format]) {
format = aliases[format]; format = aliases[format];
@ -52,7 +53,9 @@ export const getTileUrls = (req, domains, path, format, publicUrl, aliases) => {
const uris = []; const uris = [];
if (!publicUrl) { if (!publicUrl) {
for (const domain of domains) { for (const domain of domains) {
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}`);
@ -70,13 +73,14 @@ export const fixTileJSONCenter = (tileJSON) => {
(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,
) ),
]; ];
} }
}; };
const getFontPbf = (allowedFonts, fontPath, name, range, fallbacks) => new Promise((resolve, reject) => { const getFontPbf = (allowedFonts, fontPath, name, range, fallbacks) =>
new Promise((resolve, reject) => {
if (!allowedFonts || (allowedFonts[name] && fallbacks)) { if (!allowedFonts || (allowedFonts[name] && fallbacks)) {
const filename = path.join(fontPath, name, `${range}.pbf`); const filename = path.join(fontPath, name, `${range}.pbf`);
if (!fallbacks) { if (!fallbacks) {
@ -103,7 +107,10 @@ const getFontPbf = (allowedFonts, fontPath, name, range, fallbacks) => new Promi
console.error(`ERROR: Trying to use ${fallbackName} as a fallback`); console.error(`ERROR: Trying to use ${fallbackName} as a fallback`);
delete fallbacks[fallbackName]; delete fallbacks[fallbackName];
getFontPbf(null, fontPath, fallbackName, range, fallbacks).then(resolve, reject); getFontPbf(null, fontPath, fallbackName, range, fallbacks).then(
resolve,
reject,
);
} else { } else {
reject(`Font load error: ${name}`); reject(`Font load error: ${name}`);
} }
@ -114,14 +121,26 @@ const getFontPbf = (allowedFonts, fontPath, name, range, fallbacks) => new Promi
} else { } else {
reject(`Font not allowed: ${name}`); reject(`Font not allowed: ${name}`);
} }
}); });
export const 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),
),
); );
} }

View file

@ -1,48 +1,48 @@
const 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) {
expect(res.body).to.be.a('array'); expect(res.body).to.be.a('array');
expect(res.body.length).to.be.greaterThan(0); expect(res.body.length).to.be.greaterThan(0);
}).end(done); })
.end(done);
}); });
}); });
}; };
const 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) {
expect(res.body.tiles.length).to.be.greaterThan(0); expect(res.body.tiles.length).to.be.greaterThan(0);
}).end(done); })
.end(done);
}); });
}); });
}; };
describe('Metadata', function() { 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').expect(200, done);
.get('/health')
.expect(200, done);
}); });
}); });
@ -50,24 +50,25 @@ describe('Metadata', function() {
testTileJSONArray('/rendered.json'); testTileJSONArray('/rendered.json');
testTileJSONArray('/data.json'); testTileJSONArray('/data.json');
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) {
expect(res.body).to.be.a('array'); expect(res.body).to.be.a('array');
expect(res.body.length).to.be.greaterThan(0); expect(res.body.length).to.be.greaterThan(0);
expect(res.body[0].version).to.be.equal(8); expect(res.body[0].version).to.be.equal(8);
expect(res.body[0].id).to.be.a('string'); expect(res.body[0].id).to.be.a('string');
expect(res.body[0].name).to.be.a('string'); expect(res.body[0].name).to.be.a('string');
}).end(done); })
.end(done);
}); });
}); });

View file

@ -1,28 +1,29 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import {expect} from 'chai'; import { expect } from 'chai';
import supertest from 'supertest'; import supertest from 'supertest';
import {server} from '../src/server.js'; import { server } from '../src/server.js';
global.expect = expect; global.expect = expect;
global.supertest = supertest; global.supertest = supertest;
before(function() { before(function () {
console.log('global setup'); console.log('global setup');
process.chdir('test_data'); process.chdir('test_data');
const running = server({ const running = server({
configPath: 'config.json', configPath: 'config.json',
port: 8888, port: 8888,
publicUrl: '/test/' publicUrl: '/test/',
}); });
global.app = running.app; global.app = running.app;
global.server = running.server; global.server = running.server;
return running.startupPromise; return running.startupPromise;
}); });
after(function() { after(function () {
console.log('global teardown'); console.log('global teardown');
global.server.close(function() { global.server.close(function () {
console.log('Done'); process.exit(); console.log('Done');
process.exit();
}); });
}); });

View file

@ -1,10 +1,10 @@
const 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';
let 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) {
const 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);
@ -14,17 +14,45 @@ const testStatic = function(prefix, q, format, status, scale, type, query) {
const prefix = 'test-style'; const prefix = 'test-style';
describe('Static endpoints', function() { describe('Static endpoints', function () {
describe('center-based', function() { describe('center-based', function () {
describe('valid requests', function() { describe('valid requests', function () {
describe('various formats', function() { describe('various formats', function () {
testStatic(prefix, '0,0,0/256x256', 'png', 200, undefined, /image\/png/); testStatic(
testStatic(prefix, '0,0,0/256x256', 'jpg', 200, undefined, /image\/jpeg/); prefix,
testStatic(prefix, '0,0,0/256x256', 'jpeg', 200, undefined, /image\/jpeg/); '0,0,0/256x256',
testStatic(prefix, '0,0,0/256x256', 'webp', 200, undefined, /image\/webp/); 'png',
200,
undefined,
/image\/png/,
);
testStatic(
prefix,
'0,0,0/256x256',
'jpg',
200,
undefined,
/image\/jpeg/,
);
testStatic(
prefix,
'0,0,0/256x256',
'jpeg',
200,
undefined,
/image\/jpeg/,
);
testStatic(
prefix,
'0,0,0/256x256',
'webp',
200,
undefined,
/image\/webp/,
);
}); });
describe('different parameters', function() { describe('different parameters', function () {
testStatic(prefix, '0,0,0/300x300', 'png', 200, 2); testStatic(prefix, '0,0,0/300x300', 'png', 200, 2);
testStatic(prefix, '0,0,0/300x300', 'png', 200, 3); testStatic(prefix, '0,0,0/300x300', 'png', 200, 3);
@ -42,7 +70,7 @@ describe('Static endpoints', function() {
}); });
}); });
describe('invalid requests return 4xx', function() { describe('invalid requests return 4xx', function () {
testStatic(prefix, '190,0,0/256x256', 'png', 400); testStatic(prefix, '190,0,0/256x256', 'png', 400);
testStatic(prefix, '0,86,0/256x256', 'png', 400); testStatic(prefix, '0,86,0/256x256', 'png', 400);
testStatic(prefix, '80,40,20/0x0', 'png', 400); testStatic(prefix, '80,40,20/0x0', 'png', 400);
@ -57,16 +85,44 @@ describe('Static endpoints', function() {
}); });
}); });
describe('area-based', function() { describe('area-based', function () {
describe('valid requests', function() { describe('valid requests', function () {
describe('various formats', function() { describe('various formats', function () {
testStatic(prefix, '-180,-80,180,80/10x10', 'png', 200, undefined, /image\/png/); testStatic(
testStatic(prefix, '-180,-80,180,80/10x10', 'jpg', 200, undefined, /image\/jpeg/); prefix,
testStatic(prefix, '-180,-80,180,80/10x10', 'jpeg', 200, undefined, /image\/jpeg/); '-180,-80,180,80/10x10',
testStatic(prefix, '-180,-80,180,80/10x10', 'webp', 200, undefined, /image\/webp/); 'png',
200,
undefined,
/image\/png/,
);
testStatic(
prefix,
'-180,-80,180,80/10x10',
'jpg',
200,
undefined,
/image\/jpeg/,
);
testStatic(
prefix,
'-180,-80,180,80/10x10',
'jpeg',
200,
undefined,
/image\/jpeg/,
);
testStatic(
prefix,
'-180,-80,180,80/10x10',
'webp',
200,
undefined,
/image\/webp/,
);
}); });
describe('different parameters', function() { describe('different parameters', function () {
testStatic(prefix, '-180,-90,180,90/20x20', 'png', 200, 2); testStatic(prefix, '-180,-90,180,90/20x20', 'png', 200, 2);
testStatic(prefix, '0,0,1,1/200x200', 'png', 200, 3); testStatic(prefix, '0,0,1,1/200x200', 'png', 200, 3);
@ -74,7 +130,7 @@ describe('Static endpoints', function() {
}); });
}); });
describe('invalid requests return 4xx', function() { describe('invalid requests return 4xx', function () {
testStatic(prefix, '0,87,1,88/5x2', 'png', 400); testStatic(prefix, '0,87,1,88/5x2', 'png', 400);
testStatic(prefix, '0,0,1,1/1x1', 'gif', 400); testStatic(prefix, '0,0,1,1/1x1', 'gif', 400);
@ -83,20 +139,60 @@ describe('Static endpoints', function() {
}); });
}); });
describe('autofit path', function() { describe('autofit path', function () {
describe('valid requests', function() { describe('valid requests', function () {
testStatic(prefix, 'auto/256x256', 'png', 200, undefined, /image\/png/, '?path=10,10|20,20'); testStatic(
prefix,
'auto/256x256',
'png',
200,
undefined,
/image\/png/,
'?path=10,10|20,20',
);
describe('different parameters', function() { describe('different parameters', function () {
testStatic(prefix, 'auto/20x20', 'png', 200, 2, /image\/png/, '?path=10,10|20,20'); testStatic(
testStatic(prefix, 'auto/200x200', 'png', 200, 3, /image\/png/, '?path=-10,-10|-20,-20'); prefix,
'auto/20x20',
'png',
200,
2,
/image\/png/,
'?path=10,10|20,20',
);
testStatic(
prefix,
'auto/200x200',
'png',
200,
3,
/image\/png/,
'?path=-10,-10|-20,-20',
);
}); });
}); });
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(
testStatic(prefix, 'auto/2560x2560', 'png', 400, undefined, undefined, '?path=10,10|20,20'); prefix,
'auto/256x256',
'png',
400,
undefined,
undefined,
'?path=invalid',
);
testStatic(
prefix,
'auto/2560x2560',
'png',
400,
undefined,
undefined,
'?path=10,10|20,20',
);
}); });
}); });
}); });

View file

@ -1,23 +1,25 @@
const testIs = function(url, type, status) { const testIs = function (url, type, status) {
it(url + ' return ' + (status || 200) + ' and is ' + type.toString(), it(
function(done) { url + ' return ' + (status || 200) + ' and is ' + type.toString(),
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);
}); },
);
}; };
const 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 () {
testIs('/styles/' + prefix + '/style.json', /application\/json/); testIs('/styles/' + prefix + '/style.json', /application\/json/);
it('contains expected properties', function(done) { it('contains expected properties', function (done) {
supertest(app) supertest(app)
.get('/styles/' + prefix + '/style.json') .get('/styles/' + prefix + '/style.json')
.expect(function(res) { .expect(function (res) {
expect(res.body.version).to.be.equal(8); expect(res.body.version).to.be.equal(8);
expect(res.body.name).to.be.a('string'); expect(res.body.name).to.be.a('string');
expect(res.body.sources).to.be.a('object'); expect(res.body.sources).to.be.a('object');
@ -25,14 +27,15 @@ describe('Styles', function() {
expect(res.body.sprite).to.be.a('string'); expect(res.body.sprite).to.be.a('string');
expect(res.body.sprite).to.be.equal('/test/styles/test-style/sprite'); expect(res.body.sprite).to.be.equal('/test/styles/test-style/sprite');
expect(res.body.layers).to.be.a('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 () {
testIs('/styles/streets/style.json', /./, 404); testIs('/styles/streets/style.json', /./, 404);
}); });
describe('/styles/' + prefix + '/sprite[@2x].{format}', function() { describe('/styles/' + prefix + '/sprite[@2x].{format}', function () {
testIs('/styles/' + prefix + '/sprite.json', /application\/json/); testIs('/styles/' + prefix + '/sprite.json', /application\/json/);
testIs('/styles/' + prefix + '/sprite@2x.json', /application\/json/); testIs('/styles/' + prefix + '/sprite@2x.json', /application\/json/);
testIs('/styles/' + prefix + '/sprite.png', /image\/png/); testIs('/styles/' + prefix + '/sprite.png', /image\/png/);
@ -40,11 +43,13 @@ describe('Styles', function() {
}); });
}); });
describe('Fonts', function() { 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(
/application\/x-protobuf/); '/fonts/Open Sans Bold,Open Sans Regular/0-255.pbf',
/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,6 +1,6 @@
const testTile = function(prefix, z, x, y, status) { const testTile = function (prefix, z, x, y, status) {
const 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) {
const 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/);
@ -10,13 +10,13 @@ const testTile = function(prefix, z, x, y, status) {
const prefix = 'openmaptiles'; const prefix = 'openmaptiles';
describe('Vector tiles', function() { describe('Vector tiles', function () {
describe('existing tiles', function() { describe('existing tiles', function () {
testTile(prefix, 0, 0, 0, 200); testTile(prefix, 0, 0, 0, 200);
testTile(prefix, 14, 8581, 5738, 200); testTile(prefix, 14, 8581, 5738, 200);
}); });
describe('non-existent requests return 4xx', function() { describe('non-existent requests return 4xx', function () {
testTile('non_existent', 0, 0, 0, 404); testTile('non_existent', 0, 0, 0, 404);
testTile(prefix, -1, 0, 0, 404); // err zoom testTile(prefix, -1, 0, 0, 404); // err zoom
testTile(prefix, 20, 0, 0, 404); // zoom out of bounds testTile(prefix, 20, 0, 0, 404); // zoom out of bounds

View file

@ -1,7 +1,7 @@
const 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';
const 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) {
const 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);
@ -11,16 +11,16 @@ const testTile = function(prefix, z, x, y, format, status, scale, type) {
const prefix = 'test-style'; const prefix = 'test-style';
describe('Raster tiles', function() { describe('Raster tiles', function () {
describe('valid requests', function() { describe('valid requests', function () {
describe('various formats', function() { describe('various formats', function () {
testTile(prefix, 0, 0, 0, 'png', 200, undefined, /image\/png/); testTile(prefix, 0, 0, 0, 'png', 200, undefined, /image\/png/);
testTile(prefix, 0, 0, 0, 'jpg', 200, undefined, /image\/jpeg/); testTile(prefix, 0, 0, 0, 'jpg', 200, undefined, /image\/jpeg/);
testTile(prefix, 0, 0, 0, 'jpeg', 200, undefined, /image\/jpeg/); testTile(prefix, 0, 0, 0, 'jpeg', 200, undefined, /image\/jpeg/);
testTile(prefix, 0, 0, 0, 'webp', 200, undefined, /image\/webp/); testTile(prefix, 0, 0, 0, 'webp', 200, undefined, /image\/webp/);
}); });
describe('different coordinates and scales', function() { describe('different coordinates and scales', function () {
testTile(prefix, 1, 1, 1, 'png', 200); testTile(prefix, 1, 1, 1, 'png', 200);
testTile(prefix, 0, 0, 0, 'png', 200, 2); testTile(prefix, 0, 0, 0, 'png', 200, 2);
@ -29,7 +29,7 @@ describe('Raster tiles', function() {
}); });
}); });
describe('invalid requests return 4xx', function() { describe('invalid requests return 4xx', function () {
testTile('non_existent', 0, 0, 0, 'png', 404); testTile('non_existent', 0, 0, 0, 'png', 404);
testTile(prefix, -1, 0, 0, 'png', 404); testTile(prefix, -1, 0, 0, 'png', 404);
testTile(prefix, 25, 0, 0, 'png', 404); testTile(prefix, 25, 0, 0, 'png', 404);