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/*;
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
@ -72,11 +72,14 @@ COPY --from=builder /usr/src/app /usr/src/app
COPY . /usr/src/app
RUN mkdir -p /data && chown node:node /data
VOLUME /data
WORKDIR /data
EXPOSE 80
EXPOSE 8080
USER node:node
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; \
rm -rf /var/lib/apt/lists/*;
EXPOSE 80
EXPOSE 8080
RUN mkdir -p /data && chown node:node /data
VOLUME /data
WORKDIR /data
ENTRYPOINT ["/usr/src/app/docker-entrypoint.sh"]
RUN mkdir -p /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

@ -44,7 +44,7 @@ An alternative to npm to start the packed software easier is to install [Docker]
Example using a mbtiles file
```bash
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]
```
@ -52,13 +52,13 @@ 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
docker run --rm -it -v $(pwd):/data -p 8080:8080 maptiler/tileserver-gl
[in your browser, visit http://[server ip]:8080]
```
Example using a different path
```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

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
# 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
# again for all jobs to actually complete before continuing.
wait $! || RETVAL=$?

View file

@ -20,7 +20,7 @@ trap refresh HUP
if ! which -- "${1}"; then
# 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
# again for all jobs to actually complete before continuing.
wait $! || RETVAL=$?

View file

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

View file

@ -38,15 +38,41 @@ Static images
* ``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``
* 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``)
* ``stroke`` - color of the path 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)
* 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.
* 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.
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:

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-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",
"version": "4.1.1",
"version": "4.2.1",
"description": "Map tile server for JSON GL styles - vector and server side generated raster tiles",
"main": "src/main.js",
"bin": "src/main.js",
"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": {
"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": {
"@mapbox/glyph-pbf-composite": "0.0.3",
@ -29,9 +29,9 @@
"chokidar": "3.5.3",
"clone": "2.1.2",
"color": "4.2.3",
"commander": "9.4.0",
"commander": "9.4.1",
"cors": "2.8.5",
"express": "4.18.1",
"express": "4.18.2",
"handlebars": "4.7.7",
"http-shutdown": "1.2.2",
"morgan": "1.10.0",
@ -39,11 +39,44 @@
"proj4": "2.8.0",
"request": "2.88.2",
"sharp": "0.31.0",
"tileserver-gl-styles": "2.0.0"
"tileserver-gl-styles": "2.0.0",
"sanitize-filename": "1.6.3"
},
"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",
"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
import child_process from 'child_process'
child_process.execSync('rsync -av --exclude="light" --exclude=".git" --exclude="node_modules" --delete . light', {
stdio: 'inherit'
});
import child_process from 'child_process';
child_process.execSync(
'rsync -av --exclude="light" --exclude=".git" --exclude="node_modules" --delete . light',
{
stdio: 'inherit',
},
);
// PATCH `package.json`
import fs from 'fs';
import path from 'path';
import {fileURLToPath} from 'url';
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'))
const packageJson = JSON.parse(
fs.readFileSync(__dirname + '/package.json', 'utf8'),
);
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['@maplibre/maplibre-gl-native'];
delete packageJson.dependencies['sharp'];
delete packageJson.scripts['prepare'];
delete packageJson.optionalDependencies;
delete packageJson.devDependencies;
@ -51,10 +59,10 @@ if (process.argv.length > 2 && process.argv[2] == '--no-publish') {
// tileserver-gl
child_process.execSync('npm publish . --access public', {
stdio: 'inherit'
stdio: 'inherit',
});
// tileserver-gl-light
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
cd /data
node /usr/src/app/ -p 80 "$@" &
node /usr/src/app/ "$@" &
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 path from 'path';
import {fileURLToPath} from 'url';
import { fileURLToPath } from 'url';
import request from 'request';
import {server} from './server.js';
import { server } from './server.js';
import MBTiles from '@mapbox/mbtiles';
const __filename = fileURLToPath(import.meta.url);
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;
if (args.length >= 3 && args[2][0] !== '-') {
args.splice(2, 0, '--mbtiles');
}
import {program} from 'commander';
import { program } from 'commander';
program
.description('tileserver-gl startup options')
.usage('tileserver-gl [mbtiles] [options]')
.option(
'--mbtiles <file>',
'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(
'-c, --config <file>',
'Configuration file [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'
'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(
'-u|--public_url <url>',
'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)'
'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(
'-f|--log_format <format>',
'define the log format: https://github.com/expressjs/morgan#morganformat-options'
)
.version(
packageJson.version,
'-v, --version'
'define the log format: https://github.com/expressjs/morgan#morganformat-options',
)
.version(packageJson.version, '-v, --version');
program.parse(process.argv);
const opts = program.opts();
@ -91,14 +70,16 @@ const startServer = (configPath, config) => {
silent: opts.silent,
logFile: opts.log_file,
logFormat: opts.log_format,
publicUrl: publicUrl
publicUrl: publicUrl,
});
};
const startWithMBTiles = (mbtilesFile) => {
console.log(`[INFO] Automatically creating config file for ${mbtilesFile}`);
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);
@ -110,60 +91,70 @@ const startWithMBTiles = (mbtilesFile) => {
const instance = new MBTiles(mbtilesFile + '?mode=ro', (err) => {
if (err) {
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);
}
instance.getInfo((err, info) => {
if (err || !info) {
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);
}
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 = {
'options': {
'paths': {
'root': styleDir,
'fonts': 'fonts',
'styles': 'styles',
'mbtiles': path.dirname(mbtilesFile)
}
options: {
paths: {
root: styleDir,
fonts: 'fonts',
styles: 'styles',
mbtiles: path.dirname(mbtilesFile),
},
},
'styles': {},
'data': {}
styles: {},
data: {},
};
if (info.format === 'pbf' &&
info.name.toLowerCase().indexOf('openmaptiles') > -1) {
if (
info.format === 'pbf' &&
info.name.toLowerCase().indexOf('openmaptiles') > -1
) {
config['data'][`v3`] = {
'mbtiles': path.basename(mbtilesFile)
mbtiles: path.basename(mbtilesFile),
};
const styles = fs.readdirSync(path.resolve(styleDir, 'styles'));
for (const styleName of styles) {
const styleFileRel = styleName + '/style.json';
const styleFile = path.resolve(styleDir, 'styles', styleFileRel);
if (fs.existsSync(styleFile)) {
config['styles'][styleName] = {
'style': styleFileRel,
'tilejson': {
'bounds': bounds
}
style: styleFileRel,
tilejson: {
bounds: bounds,
},
};
}
}
} else {
console.log(`WARN: MBTiles not in "openmaptiles" format. Serving raw data only...`);
config['data'][(info.id || 'mbtiles')
console.log(
`WARN: MBTiles not in "openmaptiles" format. Serving raw data only...`,
);
config['data'][
(info.id || 'mbtiles')
.replace(/\//g, '_')
.replace(/:/g, '_')
.replace(/\?/g, '_')] = {
'mbtiles': path.basename(mbtilesFile)
.replace(/\?/g, '_')
] = {
mbtiles: path.basename(mbtilesFile),
};
}
@ -197,7 +188,8 @@ fs.stat(path.resolve(opts.config), (err, stats) => {
console.log(`No MBTiles specified, using ${mbtiles}`);
return startWithMBTiles(mbtiles);
} 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 stream = fs.createWriteStream(filename);
console.log(`No MBTiles found`);

View file

@ -10,97 +10,111 @@ import MBTiles from '@mapbox/mbtiles';
import Pbf from 'pbf';
import VectorTile from '@mapbox/vector-tile';
import {getTileUrls, fixTileJSONCenter} from './utils.js';
import { getTileUrls, fixTileJSONCenter } from './utils.js';
export const serve_data = {
init: (options, repo) => {
const app = express().disable('x-powered-by');
app.get('/:id/:z(\\d+)/:x(\\d+)/:y(\\d+).:format([\\w.]+)', (req, res, next) => {
const item = repo[req.params.id];
if (!item) {
return res.sendStatus(404);
}
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 ||
app.get(
'/:id/:z(\\d+)/:x(\\d+)/:y(\\d+).:format([\\w.]+)',
(req, res, next) => {
const item = repo[req.params.id];
if (!item) {
return res.sendStatus(404);
}
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 (/does not exist/.test(err.message)) {
return res.status(204).send();
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 (/does not exist/.test(err.message)) {
return res.status(204).send();
} else {
return res
.status(500)
.header('Content-Type', 'text/plain')
.send(err.message);
}
} else {
return res.status(500).send(err.message);
}
} else {
if (data == null) {
return res.status(404).send('Not found');
} else {
if (tileJSONFormat === 'pbf') {
isGzipped = data.slice(0, 2).indexOf(
Buffer.from([0x1f, 0x8b])) === 0;
if (options.dataDecoratorFunc) {
if (data == null) {
return res.status(404).send('Not found');
} else {
if (tileJSONFormat === 'pbf') {
isGzipped =
data.slice(0, 2).indexOf(Buffer.from([0x1f, 0x8b])) === 0;
if (options.dataDecoratorFunc) {
if (isGzipped) {
data = zlib.unzipSync(data);
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) {
data = zlib.unzipSync(data);
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) {
data = zlib.unzipSync(data);
isGzipped = false;
}
const tile = new VectorTile(new Pbf(data));
const geojson = {
'type': 'FeatureCollection',
'features': []
};
for (const layerName in tile.layers) {
const layer = tile.layers[layerName];
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);
const tile = new VectorTile(new Pbf(data));
const geojson = {
type: 'FeatureCollection',
features: [],
};
for (const layerName in tile.layers) {
const layer = tile.layers[layerName];
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';
res.set(headers);
delete headers['ETag']; // do not trust the tile ETag -- regenerate
headers['Content-Encoding'] = 'gzip';
res.set(headers);
if (!isGzipped) {
data = zlib.gzipSync(data);
isGzipped = true;
}
if (!isGzipped) {
data = zlib.gzipSync(data);
isGzipped = true;
}
return res.status(200).send(data);
return res.status(200).send(data);
}
}
}
});
});
});
},
);
app.get('/:id.json', (req, res, next) => {
const item = repo[req.params.id];
@ -108,10 +122,16 @@ export const serve_data = {
return res.sendStatus(404);
}
const info = clone(item.tileJSON);
info.tiles = getTileUrls(req, info.tiles,
`data/${req.params.id}`, info.format, item.publicUrl, {
'pbf': options.pbfAlias
});
info.tiles = getTileUrls(
req,
info.tiles,
`data/${req.params.id}`,
info.format,
item.publicUrl,
{
pbf: options.pbfAlias,
},
);
return res.send(info);
});
@ -120,7 +140,7 @@ export const serve_data = {
add: (options, repo, params, id, publicUrl) => {
const mbtilesFile = path.resolve(options.paths.mbtiles, params.mbtiles);
let tileJSON = {
'tiles': params.domains || options.domains
tiles: params.domains || options.domains,
};
const mbtilesFileStats = fs.statSync(mbtilesFile);
@ -129,7 +149,7 @@ export const serve_data = {
}
let source;
const sourceInfoPromise = new Promise((resolve, reject) => {
source = new MBTiles(mbtilesFile + '?mode=ro', err => {
source = new MBTiles(mbtilesFile + '?mode=ro', (err) => {
if (err) {
reject(err);
return;
@ -164,8 +184,8 @@ export const serve_data = {
repo[id] = {
tileJSON,
publicUrl,
source
source,
};
});
}
},
};

View file

@ -4,7 +4,7 @@ import express from 'express';
import fs from 'node:fs';
import path from 'path';
import {getFontsPbf} from './utils.js';
import { getFontsPbf } from './utils.js';
export const serve_font = (options, allowedFonts) => {
const app = express().disable('x-powered-by');
@ -26,8 +26,10 @@ export const serve_font = (options, allowedFonts) => {
reject(err);
return;
}
if (stats.isDirectory() &&
fs.existsSync(path.join(fontPath, file, '0-255.pbf'))) {
if (
stats.isDirectory() &&
fs.existsSync(path.join(fontPath, file, '0-255.pbf'))
) {
existingFonts[path.basename(file)] = true;
}
});
@ -40,19 +42,26 @@ export const serve_font = (options, allowedFonts) => {
const fontstack = decodeURI(req.params.fontstack);
const range = req.params.range;
getFontsPbf(options.serveAllFonts ? null : allowedFonts,
fontPath, fontstack, range, existingFonts).then((concated) => {
res.header('Content-type', 'application/x-protobuf');
res.header('Last-Modified', lastModified);
return res.send(concated);
}, (err) => res.status(400).send(err)
getFontsPbf(
options.serveAllFonts ? null : allowedFonts,
fontPath,
fontstack,
range,
existingFonts,
).then(
(concated) => {
res.header('Content-type', 'application/x-protobuf');
res.header('Last-Modified', lastModified);
return res.send(concated);
},
(err) => res.status(400).header('Content-Type', 'text/plain').send(err),
);
});
app.get('/fonts.json', (req, res, next) => {
res.header('Content-type', 'application/json');
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';
export const serve_rendered = {
init: (options, repo) => {
},
add: (options, repo, params, id, publicUrl, dataResolver) => {
},
remove: (repo, id) => {
}
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

@ -5,14 +5,14 @@ import fs from 'node:fs';
import clone from 'clone';
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 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;
}
const queryParams = [];
@ -23,8 +23,7 @@ const fixUrl = (req, url, publicUrl, opt_nokey) => {
if (queryParams.length) {
query = `?${queryParams.join('&')}`;
}
return url.replace(
'local://', getPublicUrl(publicUrl, req)) + query;
return url.replace('local://', getPublicUrl(publicUrl, req)) + query;
};
export const serve_style = {
@ -43,10 +42,20 @@ export const serve_style = {
}
// mapbox-gl-js viewer cannot handle sprite urls with query
if (styleJSON_.sprite) {
styleJSON_.sprite = fixUrl(req, styleJSON_.sprite, item.publicUrl, false);
styleJSON_.sprite = fixUrl(
req,
styleJSON_.sprite,
item.publicUrl,
false,
);
}
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_);
});
@ -89,7 +98,9 @@ export const serve_style = {
const validationErrors = validate(styleFileData);
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) {
console.log(`${err.line}: ${err.message}`);
}
@ -102,8 +113,8 @@ export const serve_style = {
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] === '}';
const fromData =
mbtilesFile[0] === '{' && mbtilesFile[mbtilesFile.length - 1] === '}';
if (fromData) {
mbtilesFile = mbtilesFile.substr(1, mbtilesFile.length - 2);
@ -135,10 +146,14 @@ export const serve_style = {
let spritePath;
if (styleJSON.sprite && !httpTester.test(styleJSON.sprite)) {
spritePath = path.join(options.paths.sprites,
styleJSON.sprite
.replace('{style}', path.basename(styleFile, '.json'))
.replace('{styleJsonFolder}', path.relative(options.paths.sprites, path.dirname(styleFile)))
spritePath = path.join(
options.paths.sprites,
styleJSON.sprite
.replace('{style}', path.basename(styleFile, '.json'))
.replace(
'{styleJsonFolder}',
path.relative(options.paths.sprites, path.dirname(styleFile)),
),
);
styleJSON.sprite = `local://styles/${id}/sprite`;
}
@ -150,9 +165,9 @@ export const serve_style = {
styleJSON,
spritePath,
publicUrl,
name: styleJSON.name
name: styleJSON.name,
};
return true;
}
},
};

View file

@ -2,8 +2,7 @@
'use strict';
import os from 'os';
process.env.UV_THREADPOOL_SIZE =
Math.ceil(Math.max(4, os.cpus().length * 1.5));
process.env.UV_THREADPOOL_SIZE = Math.ceil(Math.max(4, os.cpus().length * 1.5));
import fs from 'node:fs';
import path from 'path';
@ -17,20 +16,28 @@ import handlebars from 'handlebars';
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';
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';
import {fileURLToPath} from 'url';
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'));
const packageJson = JSON.parse(
fs.readFileSync(__dirname + '/../package.json', 'utf8'),
);
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');
const app = express().disable('x-powered-by');
@ -38,18 +45,24 @@ export function server(opts) {
styles: {},
rendered: {},
data: {},
fonts: {}
fonts: {},
};
app.enable('trust proxy');
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;
app.use(morgan(logFormat, {
stream: opts.logFile ? fs.createWriteStream(opts.logFile, {flags: 'a'}) : process.stdout,
skip: (req, res) => opts.silent && (res.statusCode === 200 || res.statusCode === 304)
}));
app.use(
morgan(logFormat, {
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;
@ -74,17 +87,21 @@ export function server(opts) {
options.paths = paths;
paths.root = path.resolve(
configPath ? path.dirname(configPath) : process.cwd(),
paths.root || '');
paths.root || '',
);
paths.styles = path.resolve(paths.root, paths.styles || '');
paths.fonts = path.resolve(paths.root, paths.fonts || '');
paths.sprites = path.resolve(paths.root, paths.sprites || '');
paths.mbtiles = path.resolve(paths.root, paths.mbtiles || '');
paths.icons = path.resolve(paths.root, paths.icons || '');
const startupPromises = [];
const checkPath = (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);
}
};
@ -92,10 +109,51 @@ export function server(opts) {
checkPath('fonts');
checkPath('sprites');
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) {
try {
options.dataDecoratorFunc = require(path.resolve(paths.root, options.dataDecorator));
options.dataDecoratorFunc = require(path.resolve(
paths.root,
options.dataDecorator,
));
} catch (e) {}
}
@ -109,54 +167,69 @@ export function server(opts) {
app.use('/styles/', serve_style.init(options, serving.styles));
if (!isLight) {
startupPromises.push(
serve_rendered.init(options, serving.rendered)
.then((sub) => {
app.use('/styles/', sub);
})
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) {
success = serve_style.add(options, serving.styles, item, id, opts.publicUrl,
(mbtiles, fromData) => {
let dataItemId;
for (const id of Object.keys(data)) {
if (fromData) {
if (id === mbtiles) {
dataItemId = id;
}
} else {
if (data[id].mbtiles === mbtiles) {
dataItemId = id;
}
success = serve_style.add(
options,
serving.styles,
item,
id,
opts.publicUrl,
(mbtiles, fromData) => {
let dataItemId;
for (const id of Object.keys(data)) {
if (fromData) {
if (id === mbtiles) {
dataItemId = id;
}
}
if (dataItemId) { // mbtiles exist in the data config
return dataItemId;
} else {
if (fromData || !allowMoreData) {
console.log(`ERROR: style "${item.style}" using unknown mbtiles "${mbtiles}"! Skipping...`);
return undefined;
} else {
let id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles;
while (data[id]) id += '_';
data[id] = {
'mbtiles': mbtiles
};
return id;
if (data[id].mbtiles === mbtiles) {
dataItemId = id;
}
}
}, (font) => {
if (reportFonts) {
serving.fonts[font] = true;
}
if (dataItemId) {
// mbtiles exist in the data config
return dataItemId;
} else {
if (fromData || !allowMoreData) {
console.log(
`ERROR: style "${item.style}" using unknown mbtiles "${mbtiles}"! Skipping...`,
);
return undefined;
} else {
let id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles;
while (data[id]) id += '_';
data[id] = {
mbtiles: mbtiles,
};
return id;
}
});
}
},
(font) => {
if (reportFonts) {
serving.fonts[font] = true;
}
},
);
}
if (success && item.serve_rendered !== false) {
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) => {
let mbtilesFile;
for (const id of Object.keys(data)) {
@ -165,8 +238,9 @@ export function server(opts) {
}
}
return mbtilesFile;
}
));
},
),
);
} else {
item.serve_rendered = false;
}
@ -184,9 +258,9 @@ export function server(opts) {
}
startupPromises.push(
serve_font(options, serving.fonts).then((sub) => {
app.use('/', sub);
})
serve_font(options, serving.fonts).then((sub) => {
app.use('/', sub);
}),
);
for (const id of Object.keys(data)) {
@ -197,61 +271,65 @@ export function server(opts) {
}
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) {
fs.readdir(options.paths.styles, {withFileTypes: true}, (err, files) => {
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') {
if (file.isFile() && path.extname(file.name).toLowerCase() == '.json') {
const id = path.basename(file.name, '.json');
const item = {
style: file.name
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...`);
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);
}
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);
}
}
});
if (eventType == 'add' || eventType == 'change') {
const item = {
style: filename,
};
addStyle(id, item, false, false);
}
}
});
}
app.get('/styles.json', (req, res, next) => {
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)) {
const styleJSON = serving.styles[id].styleJSON;
result.push({
version: styleJSON.version,
name: styleJSON.name,
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);
@ -266,9 +344,16 @@ export function server(opts) {
} else {
path = `${type}/${id}`;
}
info.tiles = getTileUrls(req, info.tiles, path, info.format, opts.publicUrl, {
'pbf': options.pbfAlias
});
info.tiles = getTileUrls(
req,
info.tiles,
path,
info.format,
opts.publicUrl,
{
pbf: options.pbfAlias,
},
);
arr.push(info);
}
return arr;
@ -294,40 +379,49 @@ export function server(opts) {
if (template === 'index') {
if (options.frontPage === false) {
return;
} else if (options.frontPage &&
options.frontPage.constructor === String) {
} else if (
options.frontPage &&
options.frontPage.constructor === String
) {
templateFile = path.resolve(paths.root, options.frontPage);
}
}
startupPromises.push(new Promise((resolve, reject) => {
fs.readFile(templateFile, (err, content) => {
if (err) {
err = new Error(`Template not found: ${err.message}`);
reject(err);
return;
}
const compiled = handlebars.compile(content.toString());
app.use(urlPath, (req, res, next) => {
let data = {};
if (dataGetter) {
data = dataGetter(req);
if (!data) {
return res.status(404).send('Not found');
}
startupPromises.push(
new Promise((resolve, reject) => {
fs.readFile(templateFile, (err, content) => {
if (err) {
err = new Error(`Template not found: ${err.message}`);
reject(err);
return;
}
data['server_version'] = `${packageJson.name} v${packageJson.version}`;
data['public_url'] = opts.publicUrl || '/';
data['is_light'] = isLight;
data['key_query_part'] =
req.query.key ? `key=${encodeURIComponent(req.query.key)}&amp;` : '';
data['key_query'] = req.query.key ? `?key=${encodeURIComponent(req.query.key)}` : '';
if (template === 'wmts') res.set('Content-Type', 'text/xml');
return res.status(200).send(compiled(data));
const compiled = handlebars.compile(content.toString());
app.use(urlPath, (req, res, next) => {
let data = {};
if (dataGetter) {
data = dataGetter(req);
if (!data) {
return res.status(404).send('Not found');
}
}
data[
'server_version'
] = `${packageJson.name} v${packageJson.version}`;
data['public_url'] = opts.publicUrl || '/';
data['is_light'] = isLight;
data['key_query_part'] = req.query.key
? `key=${encodeURIComponent(req.query.key)}&amp;`
: '';
data['key_query'] = req.query.key
? `?key=${encodeURIComponent(req.query.key)}`
: '';
if (template === 'wmts') res.set('Content-Type', 'text/xml');
return res.status(200).send(compiled(data));
});
resolve();
});
resolve();
});
}));
}),
);
};
serveTemplate('/$', 'index', (req) => {
@ -340,15 +434,23 @@ export function server(opts) {
if (style.serving_rendered) {
const center = style.serving_rendered.tileJSON.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]);
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(
req, style.serving_rendered.tileJSON.tiles,
`styles/${id}`, style.serving_rendered.tileJSON.format, opts.publicUrl)[0];
req,
style.serving_rendered.tileJSON.tiles,
`styles/${id}`,
style.serving_rendered.tileJSON.format,
opts.publicUrl,
)[0];
}
}
const data = clone(serving.data || {});
@ -357,19 +459,29 @@ export function server(opts) {
const tilejson = data[id].tileJSON;
const center = tilejson.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';
if (!data_.is_vector) {
if (center) {
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(
req, tilejson.tiles, `data/${id}`, tilejson.format, opts.publicUrl, {
'pbf': options.pbfAlias
})[0];
req,
tilejson.tiles,
`data/${id}`,
tilejson.format,
opts.publicUrl,
{
pbf: options.pbfAlias,
},
)[0];
}
if (data_.filesize) {
let suffix = 'kB';
@ -387,7 +499,7 @@ export function server(opts) {
}
return {
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;
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')}/`;
} else {
wmts.baseUrl = `${
req.get('X-Forwarded-Protocol')
? req.get('X-Forwarded-Protocol')
: req.protocol
}://${req.get('host')}/`;
}
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() {
let address = this.address().address;
if (address.indexOf('::') === 0) {
address = `[${address}]`; // literal IPv6 address
}
console.log(`Listening at http://${address}:${this.address().port}/`);
});
const server = app.listen(
process.env.PORT || opts.port,
process.env.BIND || opts.bind,
function () {
let address = this.address().address;
if (address.indexOf('::') === 0) {
address = `[${address}]`; // literal IPv6 address
}
console.log(`Listening at http://${address}:${this.address().port}/`);
},
);
// add server.shutdown() to gracefully stop serving
enableShutdown(server);
@ -467,11 +586,15 @@ export function server(opts) {
return {
app: app,
server: server,
startupPromise: startupPromise
startupPromise: startupPromise,
};
}
export const exports = (opts) => {
/**
*
* @param opts
*/
export function server(opts) {
const running = start(opts);
running.startupPromise.catch((err) => {
@ -487,10 +610,6 @@ export const exports = (opts) => {
console.log('Stopping server and reloading config');
running.server.shutdown(() => {
for (const key in require.cache) {
delete require.cache[key];
}
const restarted = start(opts);
running.server = restarted.server;
running.app = restarted.app;
@ -498,4 +617,4 @@ export const exports = (opts) => {
});
return running;
};
}

View file

@ -6,8 +6,8 @@ import fs from 'node:fs';
import clone from 'clone';
import glyphCompose from '@mapbox/glyph-pbf-composite';
export const getPublicUrl = (publicUrl, req) => publicUrl || `${req.protocol}://${req.headers.host}/`;
export const getPublicUrl = (publicUrl, req) =>
publicUrl || `${req.protocol}://${req.headers.host}/`;
export const getTileUrls = (req, domains, path, format, publicUrl, aliases) => {
if (domains) {
@ -16,7 +16,8 @@ export const getTileUrls = (req, domains, path, format, publicUrl, aliases) => {
}
const host = req.headers.host;
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);
const newDomains = [];
for (const domain of domains) {
@ -43,7 +44,7 @@ export const getTileUrls = (req, domains, path, format, publicUrl, aliases) => {
if (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]) {
format = aliases[format];
@ -52,7 +53,9 @@ export const getTileUrls = (req, domains, path, format, publicUrl, aliases) => {
const uris = [];
if (!publicUrl) {
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 {
uris.push(`${publicUrl}${path}/{z}/{x}/{y}.${format}${query}`);
@ -69,59 +72,75 @@ export const fixTileJSONCenter = (tileJSON) => {
(tileJSON.bounds[0] + tileJSON.bounds[2]) / 2,
(tileJSON.bounds[1] + tileJSON.bounds[3]) / 2,
Math.round(
-Math.log((tileJSON.bounds[2] - tileJSON.bounds[0]) / 360 / tiles) /
Math.LN2
)
-Math.log((tileJSON.bounds[2] - tileJSON.bounds[0]) / 360 / tiles) /
Math.LN2,
),
];
}
};
const getFontPbf = (allowedFonts, fontPath, name, range, fallbacks) => new Promise((resolve, reject) => {
if (!allowedFonts || (allowedFonts[name] && fallbacks)) {
const filename = path.join(fontPath, name, `${range}.pbf`);
if (!fallbacks) {
fallbacks = clone(allowedFonts || {});
}
delete fallbacks[name];
fs.readFile(filename, (err, data) => {
if (err) {
console.error(`ERROR: Font not found: ${name}`);
if (fallbacks && Object.keys(fallbacks).length) {
let fallbackName;
let fontStyle = name.split(' ').pop();
if (['Regular', 'Bold', 'Italic'].indexOf(fontStyle) < 0) {
fontStyle = 'Regular';
}
fallbackName = `Noto Sans ${fontStyle}`;
if (!fallbacks[fallbackName]) {
fallbackName = `Open Sans ${fontStyle}`;
if (!fallbacks[fallbackName]) {
fallbackName = Object.keys(fallbacks)[0];
}
}
console.error(`ERROR: Trying to use ${fallbackName} as a fallback`);
delete fallbacks[fallbackName];
getFontPbf(null, fontPath, fallbackName, range, fallbacks).then(resolve, reject);
} else {
reject(`Font load error: ${name}`);
}
} else {
resolve(data);
const getFontPbf = (allowedFonts, fontPath, name, range, fallbacks) =>
new Promise((resolve, reject) => {
if (!allowedFonts || (allowedFonts[name] && fallbacks)) {
const filename = path.join(fontPath, name, `${range}.pbf`);
if (!fallbacks) {
fallbacks = clone(allowedFonts || {});
}
});
} else {
reject(`Font not allowed: ${name}`);
}
});
delete fallbacks[name];
fs.readFile(filename, (err, data) => {
if (err) {
console.error(`ERROR: Font not found: ${name}`);
if (fallbacks && Object.keys(fallbacks).length) {
let fallbackName;
export const getFontsPbf = (allowedFonts, fontPath, names, range, fallbacks) => {
let fontStyle = name.split(' ').pop();
if (['Regular', 'Bold', 'Italic'].indexOf(fontStyle) < 0) {
fontStyle = 'Regular';
}
fallbackName = `Noto Sans ${fontStyle}`;
if (!fallbacks[fallbackName]) {
fallbackName = `Open Sans ${fontStyle}`;
if (!fallbacks[fallbackName]) {
fallbackName = Object.keys(fallbacks)[0];
}
}
console.error(`ERROR: Trying to use ${fallbackName} as a fallback`);
delete fallbacks[fallbackName];
getFontPbf(null, fontPath, fallbackName, range, fallbacks).then(
resolve,
reject,
);
} else {
reject(`Font load error: ${name}`);
}
} else {
resolve(data);
}
});
} else {
reject(`Font not allowed: ${name}`);
}
});
export const getFontsPbf = (
allowedFonts,
fontPath,
names,
range,
fallbacks,
) => {
const fonts = names.split(',');
const queue = [];
for (const font of fonts) {
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) {
describe(url + ' is array of TileJSONs', function() {
it('is json', function(done) {
const testTileJSONArray = function (url) {
describe(url + ' is array of TileJSONs', function () {
it('is json', function (done) {
supertest(app)
.get(url)
.expect(200)
.expect('Content-Type', /application\/json/, done);
.get(url)
.expect(200)
.expect('Content-Type', /application\/json/, done);
});
it('is non-empty array', function(done) {
it('is non-empty array', function (done) {
supertest(app)
.get(url)
.expect(function(res) {
expect(res.body).to.be.a('array');
expect(res.body.length).to.be.greaterThan(0);
}).end(done);
.get(url)
.expect(function (res) {
expect(res.body).to.be.a('array');
expect(res.body.length).to.be.greaterThan(0);
})
.end(done);
});
});
};
const testTileJSON = function(url) {
describe(url + ' is TileJSON', function() {
it('is json', function(done) {
const testTileJSON = function (url) {
describe(url + ' is TileJSON', function () {
it('is json', function (done) {
supertest(app)
.get(url)
.expect(200)
.expect('Content-Type', /application\/json/, done);
.get(url)
.expect(200)
.expect('Content-Type', /application\/json/, done);
});
it('has valid tiles', function(done) {
it('has valid tiles', function (done) {
supertest(app)
.get(url)
.expect(function(res) {
expect(res.body.tiles.length).to.be.greaterThan(0);
}).end(done);
.get(url)
.expect(function (res) {
expect(res.body.tiles.length).to.be.greaterThan(0);
})
.end(done);
});
});
};
describe('Metadata', function() {
describe('/health', function() {
it('returns 200', function(done) {
supertest(app)
.get('/health')
.expect(200, done);
describe('Metadata', function () {
describe('/health', function () {
it('returns 200', function (done) {
supertest(app).get('/health').expect(200, done);
});
});
@ -50,24 +50,25 @@ describe('Metadata', function() {
testTileJSONArray('/rendered.json');
testTileJSONArray('/data.json');
describe('/styles.json is valid array', function() {
it('is json', function(done) {
describe('/styles.json is valid array', function () {
it('is json', function (done) {
supertest(app)
.get('/styles.json')
.expect(200)
.expect('Content-Type', /application\/json/, done);
.get('/styles.json')
.expect(200)
.expect('Content-Type', /application\/json/, done);
});
it('contains valid item', function(done) {
it('contains valid item', function (done) {
supertest(app)
.get('/styles.json')
.expect(function(res) {
expect(res.body).to.be.a('array');
expect(res.body.length).to.be.greaterThan(0);
expect(res.body[0].version).to.be.equal(8);
expect(res.body[0].id).to.be.a('string');
expect(res.body[0].name).to.be.a('string');
}).end(done);
.get('/styles.json')
.expect(function (res) {
expect(res.body).to.be.a('array');
expect(res.body.length).to.be.greaterThan(0);
expect(res.body[0].version).to.be.equal(8);
expect(res.body[0].id).to.be.a('string');
expect(res.body[0].name).to.be.a('string');
})
.end(done);
});
});

View file

@ -1,28 +1,29 @@
process.env.NODE_ENV = 'test';
import {expect} from 'chai';
import { expect } from 'chai';
import supertest from 'supertest';
import {server} from '../src/server.js';
import { server } from '../src/server.js';
global.expect = expect;
global.supertest = supertest;
before(function() {
before(function () {
console.log('global setup');
process.chdir('test_data');
const running = server({
configPath: 'config.json',
port: 8888,
publicUrl: '/test/'
publicUrl: '/test/',
});
global.app = running.app;
global.server = running.server;
return running.startupPromise;
});
after(function() {
after(function () {
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,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';
let path = '/styles/' + prefix + '/static/' + q + '.' + format;
if (query) {
path += query;
}
it(path + ' returns ' + status, function(done) {
it(path + ' returns ' + status, function (done) {
const test = supertest(app).get(path);
if (status) test.expect(status);
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';
describe('Static endpoints', function() {
describe('center-based', function() {
describe('valid requests', function() {
describe('various formats', function() {
testStatic(prefix, '0,0,0/256x256', '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('Static endpoints', function () {
describe('center-based', function () {
describe('valid requests', function () {
describe('various formats', function () {
testStatic(
prefix,
'0,0,0/256x256',
'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, 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, '0,86,0/256x256', 'png', 400);
testStatic(prefix, '80,40,20/0x0', 'png', 400);
@ -57,16 +85,44 @@ describe('Static endpoints', function() {
});
});
describe('area-based', function() {
describe('valid requests', function() {
describe('various formats', function() {
testStatic(prefix, '-180,-80,180,80/10x10', '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('area-based', function () {
describe('valid requests', function () {
describe('various formats', function () {
testStatic(
prefix,
'-180,-80,180,80/10x10',
'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, '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,0,1,1/1x1', 'gif', 400);
@ -83,20 +139,60 @@ describe('Static endpoints', function() {
});
});
describe('autofit path', function() {
describe('valid requests', function() {
testStatic(prefix, 'auto/256x256', 'png', 200, undefined, /image\/png/, '?path=10,10|20,20');
describe('autofit path', function () {
describe('valid requests', function () {
testStatic(
prefix,
'auto/256x256',
'png',
200,
undefined,
/image\/png/,
'?path=10,10|20,20',
);
describe('different parameters', function() {
testStatic(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('different parameters', function () {
testStatic(
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, undefined, undefined, '?path=10,10');
testStatic(prefix, 'auto/2560x2560', 'png', 400, undefined, undefined, '?path=10,10|20,20');
testStatic(
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,38 +1,41 @@
const testIs = function(url, type, status) {
it(url + ' return ' + (status || 200) + ' and is ' + type.toString(),
function(done) {
supertest(app)
.get(url)
.expect(status || 200)
.expect('Content-Type', type, done);
});
const testIs = function (url, type, status) {
it(
url + ' return ' + (status || 200) + ' and is ' + type.toString(),
function (done) {
supertest(app)
.get(url)
.expect(status || 200)
.expect('Content-Type', type, done);
},
);
};
const prefix = 'test-style';
describe('Styles', function() {
describe('/styles/' + prefix + '/style.json is valid style', function() {
describe('Styles', function () {
describe('/styles/' + prefix + '/style.json is valid style', function () {
testIs('/styles/' + prefix + '/style.json', /application\/json/);
it('contains expected properties', function(done) {
it('contains expected properties', function (done) {
supertest(app)
.get('/styles/' + prefix + '/style.json')
.expect(function(res) {
expect(res.body.version).to.be.equal(8);
expect(res.body.name).to.be.a('string');
expect(res.body.sources).to.be.a('object');
expect(res.body.glyphs).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.layers).to.be.a('array');
}).end(done);
.get('/styles/' + prefix + '/style.json')
.expect(function (res) {
expect(res.body.version).to.be.equal(8);
expect(res.body.name).to.be.a('string');
expect(res.body.sources).to.be.a('object');
expect(res.body.glyphs).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.layers).to.be.a('array');
})
.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);
});
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@2x.json', /application\/json/);
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 Regular/65280-65535.pbf', /application\/x-protobuf/);
testIs('/fonts/Open Sans Bold,Open Sans Regular/0-255.pbf',
/application\/x-protobuf/);
testIs(
'/fonts/Open Sans Bold,Open Sans Regular/0-255.pbf',
/application\/x-protobuf/,
);
testIs('/fonts/Nonsense,Open Sans Bold/0-255.pbf', /./, 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';
it(path + ' returns ' + status, function(done) {
it(path + ' returns ' + status, function (done) {
const test = supertest(app).get(path);
if (status) test.expect(status);
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';
describe('Vector tiles', function() {
describe('existing tiles', function() {
describe('Vector tiles', function () {
describe('existing tiles', function () {
testTile(prefix, 0, 0, 0, 200);
testTile(prefix, 14, 8581, 5738, 200);
});
describe('non-existent requests return 4xx', function() {
describe('non-existent requests return 4xx', function () {
testTile('non_existent', 0, 0, 0, 404);
testTile(prefix, -1, 0, 0, 404); // err zoom
testTile(prefix, 20, 0, 0, 404); // zoom out of bounds

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';
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);
test.expect(status);
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';
describe('Raster tiles', function() {
describe('valid requests', function() {
describe('various formats', function() {
describe('Raster tiles', function () {
describe('valid requests', function () {
describe('various formats', function () {
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, 'jpeg', 200, undefined, /image\/jpeg/);
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, 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(prefix, -1, 0, 0, 'png', 404);
testTile(prefix, 25, 0, 0, 'png', 404);