style: fix lint issues in code 🕺 (#626)

* style: fix lint issues in code 🕺

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* style: lint fix all files

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* chore: add `keywords` for better reach

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* feat: add `husky` & `commitlint`

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* chore: ignore `public` directory

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* revert: do not lint `public` directory

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* style: fix issues with lint

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* feat: add eslint config

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* feat: add lint-staged

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* style: lint fix all file(s)

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* fix: ignore rules for light version

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* fix: remove unnecessary space

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* chore(deps): update lockfile

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* style: autofix linting issue(s)

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>
This commit is contained in:
Vinayak Kulkarni 2022-11-09 09:26:07 +05:30 committed by GitHub
parent 50201f0a99
commit 9b64093c42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 8084 additions and 1264 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',
},
};

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

3
commitlint.config.cjs Normal file
View file

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

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',
};

7092
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,17 +5,17 @@
"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:80 $(docker build -q .)",
"prepare": "husky install"
}, },
"dependencies": { "dependencies": {
"@mapbox/glyph-pbf-composite": "0.0.3", "@mapbox/glyph-pbf-composite": "0.0.3",
@ -43,8 +43,40 @@
"sanitize-filename": "1.6.3" "sanitize-filename": "1.6.3"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.1.2",
"@commitlint/config-conventional": "^17.1.0",
"@typescript-eslint/eslint-plugin": "^5.38.0",
"@typescript-eslint/parser": "^5.38.0",
"chai": "4.3.6", "chai": "4.3.6",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-jsdoc": "^39.3.6",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-security": "^1.5.0",
"husky": "^8.0.1",
"lint-staged": "^13.0.3",
"mocha": "^10.0.0", "mocha": "^10.0.0",
"supertest": "^6.2.4" "prettier": "^2.7.1",
} "should": "^13.2.3",
"supertest": "^6.2.4",
"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,10 +12,13 @@
// 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';
@ -23,10 +26,13 @@ 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'];
@ -51,10 +57,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',
}); });

View file

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

View file

@ -12,7 +12,9 @@ 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] !== '-') {
@ -26,51 +28,28 @@ program
.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);
@ -117,53 +98,63 @@ const startWithMBTiles = (mbtilesFile) => {
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

@ -16,7 +16,9 @@ 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) => {
@ -51,8 +61,8 @@ export const serve_data = {
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 +83,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 +110,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 +119,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 +137,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 +146,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 +181,8 @@ export const serve_data = {
repo[id] = { repo[id] = {
tileJSON, tileJSON,
publicUrl, publicUrl,
source source,
}; };
}); });
} },
}; };

View file

@ -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).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) => {
}
}; };

View file

@ -11,7 +11,7 @@ import {createCanvas, Image} from 'canvas';
import clone from 'clone'; import clone from 'clone';
import Color from 'color'; import Color from 'color';
import express from 'express'; import express from 'express';
import sanitize from "sanitize-filename"; import sanitize from 'sanitize-filename';
import SphericalMercator from '@mapbox/sphericalmercator'; import SphericalMercator from '@mapbox/sphericalmercator';
import mlgl from '@maplibre/maplibre-gl-native'; import mlgl from '@maplibre/maplibre-gl-native';
import MBTiles from '@mapbox/mbtiles'; import MBTiles from '@mapbox/mbtiles';
@ -19,7 +19,7 @@ import proj4 from 'proj4';
import request from 'request'; import request from 'request';
import { getFontsPbf, getTileUrls, fixTileJSONCenter } from './utils.js'; import { getFontsPbf, getTileUrls, fixTileJSONCenter } from './utils.js';
const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+\.?\\d+)'; const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+.?\\d+)';
const httpTester = /^(http(s)?:)?\/\//; const httpTester = /^(http(s)?:)?\/\//;
const mercator = new SphericalMercator(); const mercator = new SphericalMercator();
@ -38,7 +38,7 @@ const extensionToFormat = {
'.jpg': 'jpeg', '.jpg': 'jpeg',
'.jpeg': 'jpeg', '.jpeg': 'jpeg',
'.png': 'png', '.png': 'png',
'.webp': 'webp' '.webp': 'webp',
}; };
/** /**
@ -46,11 +46,12 @@ const extensionToFormat = {
* string is for unknown or unsupported formats. * string is for unknown or unsupported formats.
*/ */
const cachedEmptyResponses = { const cachedEmptyResponses = {
'': Buffer.alloc(0) '': Buffer.alloc(0),
}; };
/** /**
* Create an appropriate mlgl response for http errors. * Create an appropriate mlgl response for http errors.
*
* @param {string} format The format (a sharp format or 'pbf'). * @param {string} format The format (a sharp format or 'pbf').
* @param {string} color The background color (or empty string for transparent). * @param {string} color The background color (or empty string for transparent).
* @param {Function} callback The mlgl callback. * @param {Function} callback The mlgl callback.
@ -83,9 +84,11 @@ function createEmptyResponse(format, color, callback) {
raw: { raw: {
width: 1, width: 1,
height: 1, height: 1,
channels: channels channels: channels,
} },
}).toFormat(format).toBuffer((err, buffer, info) => { })
.toFormat(format)
.toBuffer((err, buffer, info) => {
if (!err) { if (!err) {
cachedEmptyResponses[cacheKey] = buffer; cachedEmptyResponses[cacheKey] = buffer;
} }
@ -96,8 +99,10 @@ function createEmptyResponse(format, color, callback) {
/** /**
* Parses coordinate pair provided to pair of floats and ensures the resulting * Parses coordinate pair provided to pair of floats and ensures the resulting
* pair is a longitude/latitude combination depending on lnglat query parameter. * pair is a longitude/latitude combination depending on lnglat query parameter.
*
* @param {List} coordinatePair Coordinate pair. * @param {List} coordinatePair Coordinate pair.
* @param {Object} query Request query parameters. * @param coordinates
* @param {object} query Request query parameters.
*/ */
const parseCoordinatePair = (coordinates, query) => { const parseCoordinatePair = (coordinates, query) => {
const firstCoordinate = parseFloat(coordinates[0]); const firstCoordinate = parseFloat(coordinates[0]);
@ -119,8 +124,9 @@ const parseCoordinatePair = (coordinates, query) => {
/** /**
* Parses a coordinate pair from query arguments and optionally transforms it. * Parses a coordinate pair from query arguments and optionally transforms it.
*
* @param {List} coordinatePair Coordinate pair. * @param {List} coordinatePair Coordinate pair.
* @param {Object} query Request query parameters. * @param {object} query Request query parameters.
* @param {Function} transformer Optional transform function. * @param {Function} transformer Optional transform function.
*/ */
const parseCoordinates = (coordinatePair, query, transformer) => { const parseCoordinates = (coordinatePair, query, transformer) => {
@ -134,10 +140,10 @@ const parseCoordinates = (coordinatePair, query, transformer) => {
return parsedCoordinates; return parsedCoordinates;
}; };
/** /**
* Parses paths provided via query into a list of path objects. * Parses paths provided via query into a list of path objects.
* @param {Object} query Request query parameters. *
* @param {object} query Request query parameters.
* @param {Function} transformer Optional transform function. * @param {Function} transformer Optional transform function.
*/ */
const extractPathsFromQuery = (query, transformer) => { const extractPathsFromQuery = (query, transformer) => {
@ -180,9 +186,8 @@ const extractPathsFromQuery = (query, transformer) => {
// Extend list of paths with current path if it contains coordinates // Extend list of paths with current path if it contains coordinates
if (currentPath.length) { if (currentPath.length) {
paths.push(currentPath) paths.push(currentPath);
} }
} }
return paths; return paths;
}; };
@ -192,8 +197,9 @@ const extractPathsFromQuery = (query, transformer) => {
* on marker object. * on marker object.
* Options adhere to the following format * Options adhere to the following format
* [optionName]:[optionValue] * [optionName]:[optionValue]
*
* @param {List[String]} optionsList List of option strings. * @param {List[String]} optionsList List of option strings.
* @param {Object} marker Marker object to configure. * @param {object} marker Marker object to configure.
*/ */
const parseMarkerOptions = (optionsList, marker) => { const parseMarkerOptions = (optionsList, marker) => {
for (const options of optionsList) { for (const options of optionsList) {
@ -207,7 +213,7 @@ const parseMarkerOptions = (optionsList, marker) => {
// Scale factor to up- or downscale icon // Scale factor to up- or downscale icon
case 'scale': case 'scale':
// Scale factors must not be negative // Scale factors must not be negative
marker.scale = Math.abs(parseFloat(optionParts[1])) marker.scale = Math.abs(parseFloat(optionParts[1]));
break; break;
// Icon offset as positive or negative pixel value in the following // Icon offset as positive or negative pixel value in the following
// format [offsetX],[offsetY] where [offsetY] is optional // format [offsetX],[offsetY] where [offsetY] is optional
@ -226,8 +232,9 @@ const parseMarkerOptions = (optionsList, marker) => {
/** /**
* Parses markers provided via query into a list of marker objects. * Parses markers provided via query into a list of marker objects.
* @param {Object} query Request query parameters. *
* @param {Object} options Configuration options. * @param {object} query Request query parameters.
* @param {object} options Configuration options.
* @param {Function} transformer Optional transform function. * @param {Function} transformer Optional transform function.
*/ */
const extractMarkersFromQuery = (query, options, transformer) => { const extractMarkersFromQuery = (query, options, transformer) => {
@ -240,8 +247,9 @@ const extractMarkersFromQuery = (query, options, transformer) => {
// Check if multiple markers have been provided and mimic a list if it's a // Check if multiple markers have been provided and mimic a list if it's a
// single maker. // single maker.
const providedMarkers = Array.isArray(query.marker) ? const providedMarkers = Array.isArray(query.marker)
query.marker : [query.marker]; ? query.marker
: [query.marker];
// Iterate through provided markers which can have one of the following // Iterate through provided markers which can have one of the following
// formats // formats
@ -266,7 +274,7 @@ const extractMarkersFromQuery = (query, options, transformer) => {
if (!(iconURI.startsWith('http://') || iconURI.startsWith('https://'))) { if (!(iconURI.startsWith('http://') || iconURI.startsWith('https://'))) {
// Sanitize URI with sanitize-filename // Sanitize URI with sanitize-filename
// https://www.npmjs.com/package/sanitize-filename#details // https://www.npmjs.com/package/sanitize-filename#details
iconURI = sanitize(iconURI) iconURI = sanitize(iconURI);
// If the selected icon is not part of available icons skip it // If the selected icon is not part of available icons skip it
if (!options.paths.availableIcons.includes(iconURI)) { if (!options.paths.availableIcons.includes(iconURI)) {
@ -298,15 +306,15 @@ const extractMarkersFromQuery = (query, options, transformer) => {
// Add marker to list // Add marker to list
markers.push(marker); markers.push(marker);
} }
return markers; return markers;
}; };
/** /**
* Transforms coordinates to pixels. * Transforms coordinates to pixels.
*
* @param {List[Number]} ll Longitude/Latitude coordinate pair. * @param {List[Number]} ll Longitude/Latitude coordinate pair.
* @param {Number} zoom Map zoom level. * @param {number} zoom Map zoom level.
*/ */
const precisePx = (ll, zoom) => { const precisePx = (ll, zoom) => {
const px = mercator.px(ll, 20); const px = mercator.px(ll, 20);
@ -316,12 +324,13 @@ const precisePx = (ll, zoom) => {
/** /**
* Draws a marker in cavans context. * Draws a marker in cavans context.
* @param {Object} ctx Canvas context object. *
* @param {Object} marker Marker object parsed by extractMarkersFromQuery. * @param {object} ctx Canvas context object.
* @param {Number} z Map zoom level. * @param {object} marker Marker object parsed by extractMarkersFromQuery.
* @param {number} z Map zoom level.
*/ */
const drawMarker = (ctx, marker, z) => { const drawMarker = (ctx, marker, z) => {
return new Promise(resolve => { return new Promise((resolve) => {
const img = new Image(); const img = new Image();
const pixelCoords = precisePx(marker.location, z); const pixelCoords = precisePx(marker.location, z);
@ -340,15 +349,15 @@ const drawMarker = (ctx, marker, z) => {
// scaled as well. Additionally offsets are provided as either positive or // scaled as well. Additionally offsets are provided as either positive or
// negative values so we always add them // negative values so we always add them
if (marker.offsetX) { if (marker.offsetX) {
xCoordinate = xCoordinate + (marker.offsetX * scale); xCoordinate = xCoordinate + marker.offsetX * scale;
} }
if (marker.offsetY) { if (marker.offsetY) {
yCoordinate = yCoordinate + (marker.offsetY * scale); yCoordinate = yCoordinate + marker.offsetY * scale;
} }
return { return {
'x': xCoordinate, x: xCoordinate,
'y': yCoordinate y: yCoordinate,
}; };
}; };
@ -375,19 +384,22 @@ const drawMarker = (ctx, marker, z) => {
}; };
img.onload = drawOnCanvas; img.onload = drawOnCanvas;
img.onerror = err => { throw err }; img.onerror = (err) => {
throw err;
};
img.src = marker.icon; img.src = marker.icon;
}); });
} };
/** /**
* Draws a list of markers onto a canvas. * Draws a list of markers onto a canvas.
* Wraps drawing of markers into list of promises and awaits them. * Wraps drawing of markers into list of promises and awaits them.
* It's required because images are expected to load asynchronous in canvas js * It's required because images are expected to load asynchronous in canvas js
* even when provided from a local disk. * even when provided from a local disk.
* @param {Object} ctx Canvas context object. *
* @param {object} ctx Canvas context object.
* @param {List[Object]} markers Marker objects parsed by extractMarkersFromQuery. * @param {List[Object]} markers Marker objects parsed by extractMarkersFromQuery.
* @param {Number} z Map zoom level. * @param {number} z Map zoom level.
*/ */
const drawMarkers = async (ctx, markers, z) => { const drawMarkers = async (ctx, markers, z) => {
const markerPromises = []; const markerPromises = [];
@ -399,14 +411,15 @@ const drawMarkers = async (ctx, markers, z) => {
// Await marker drawings before continuing // Await marker drawings before continuing
await Promise.all(markerPromises); await Promise.all(markerPromises);
} };
/** /**
* Draws a list of coordinates onto a canvas and styles the resulting path. * Draws a list of coordinates onto a canvas and styles the resulting path.
* @param {Object} ctx Canvas context object. *
* @param {object} ctx Canvas context object.
* @param {List[Number]} path List of coordinates. * @param {List[Number]} path List of coordinates.
* @param {Object} query Request query parameters. * @param {object} query Request query parameters.
* @param {Number} z Map zoom level. * @param {number} z Map zoom level.
*/ */
const drawPath = (ctx, path, query, z) => { const drawPath = (ctx, path, query, z) => {
if (!path || path.length < 2) { if (!path || path.length < 2) {
@ -422,8 +435,10 @@ const drawPath = (ctx, path, query, z) => {
} }
// Check if first coordinate matches last coordinate // Check if first coordinate matches last coordinate
if (path[0][0] === path[path.length - 1][0] && if (
path[0][1] === path[path.length - 1][1]) { path[0][0] === path[path.length - 1][0] &&
path[0][1] === path[path.length - 1][1]
) {
ctx.closePath(); ctx.closePath();
} }
@ -434,14 +449,15 @@ const drawPath = (ctx, path, query, z) => {
} }
// Get line width from query and fall back to 1 if not provided // Get line width from query and fall back to 1 if not provided
const lineWidth = query.width !== undefined ? const lineWidth = query.width !== undefined ? parseFloat(query.width) : 1;
parseFloat(query.width) : 1;
// Ensure line width is valid // Ensure line width is valid
if (lineWidth > 0) { if (lineWidth > 0) {
// Get border width from query and fall back to 10% of line width // Get border width from query and fall back to 10% of line width
const borderWidth = query.borderwidth !== undefined ? const borderWidth =
parseFloat(query.borderwidth) : lineWidth * 0.1; query.borderwidth !== undefined
? parseFloat(query.borderwidth)
: lineWidth * 0.1;
// Set rendering style for the start and end points of the path // Set rendering style for the start and end points of the path
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap
@ -456,7 +472,7 @@ const drawPath = (ctx, path, query, z) => {
if (query.border !== undefined && borderWidth > 0) { if (query.border !== undefined && borderWidth > 0) {
// We need to double the desired border width and add it to the line width // We need to double the desired border width and add it to the line width
// in order to get the desired border on each side of the line. // in order to get the desired border on each side of the line.
ctx.lineWidth = lineWidth + (borderWidth * 2); ctx.lineWidth = lineWidth + borderWidth * 2;
// Set border style as rgba // Set border style as rgba
ctx.strokeStyle = query.border; ctx.strokeStyle = query.border;
ctx.stroke(); ctx.stroke();
@ -466,9 +482,21 @@ const drawPath = (ctx, path, query, z) => {
ctx.strokeStyle = query.stroke || 'rgba(0,64,255,0.7)'; ctx.strokeStyle = query.stroke || 'rgba(0,64,255,0.7)';
ctx.stroke(); ctx.stroke();
} }
} };
const renderOverlay = async (z, x, y, bearing, pitch, w, h, scale, paths, markers, query) => { const renderOverlay = async (
z,
x,
y,
bearing,
pitch,
w,
h,
scale,
paths,
markers,
query,
) => {
if ((!paths || paths.length === 0) && (!markers || markers.length === 0)) { if ((!paths || paths.length === 0) && (!markers || markers.length === 0)) {
return null; return null;
} }
@ -479,7 +507,7 @@ const renderOverlay = async (z, x, y, bearing, pitch, w, h, scale, paths, marker
const maxEdge = center[1] + h / 2; const maxEdge = center[1] + h / 2;
const minEdge = center[1] - h / 2; const minEdge = center[1] - h / 2;
if (maxEdge > mapHeight) { if (maxEdge > mapHeight) {
center[1] -= (maxEdge - mapHeight); center[1] -= maxEdge - mapHeight;
} else if (minEdge < 0) { } else if (minEdge < 0) {
center[1] -= minEdge; center[1] -= minEdge;
} }
@ -489,7 +517,7 @@ const renderOverlay = async (z, x, y, bearing, pitch, w, h, scale, paths, marker
ctx.scale(scale, scale); ctx.scale(scale, scale);
if (bearing) { if (bearing) {
ctx.translate(w / 2, h / 2); ctx.translate(w / 2, h / 2);
ctx.rotate(-bearing / 180 * Math.PI); ctx.rotate((-bearing / 180) * Math.PI);
ctx.translate(-center[0], -center[1]); ctx.translate(-center[0], -center[1]);
} else { } else {
// optimized path // optimized path
@ -510,17 +538,17 @@ const renderOverlay = async (z, x, y, bearing, pitch, w, h, scale, paths, marker
const calcZForBBox = (bbox, w, h, query) => { const calcZForBBox = (bbox, w, h, query) => {
let z = 25; let z = 25;
const padding = query.padding !== undefined ? const padding = query.padding !== undefined ? parseFloat(query.padding) : 0.1;
parseFloat(query.padding) : 0.1;
const minCorner = mercator.px([bbox[0], bbox[3]], z); const minCorner = mercator.px([bbox[0], bbox[3]], z);
const maxCorner = mercator.px([bbox[2], bbox[1]], z); const maxCorner = mercator.px([bbox[2], bbox[1]], z);
const w_ = w / (1 + 2 * padding); const w_ = w / (1 + 2 * padding);
const h_ = h / (1 + 2 * padding); const h_ = h / (1 + 2 * padding);
z -= Math.max( z -=
Math.max(
Math.log((maxCorner[0] - minCorner[0]) / w_), Math.log((maxCorner[0] - minCorner[0]) / w_),
Math.log((maxCorner[1] - minCorner[1]) / h_) Math.log((maxCorner[1] - minCorner[1]) / h_),
) / Math.LN2; ) / Math.LN2;
z = Math.max(Math.log(Math.max(w, h) / 256) / Math.LN2, Math.min(25, z)); z = Math.max(Math.log(Math.max(w, h) / 256) / Math.LN2, Math.min(25, z));
@ -563,14 +591,36 @@ export const serve_rendered = {
const app = express().disable('x-powered-by'); const app = express().disable('x-powered-by');
const respondImage = (item, z, lon, lat, bearing, pitch, width, height, scale, format, res, next, opt_overlay, opt_mode='tile') => { const respondImage = (
if (Math.abs(lon) > 180 || Math.abs(lat) > 85.06 || item,
lon !== lon || lat !== lat) { z,
lon,
lat,
bearing,
pitch,
width,
height,
scale,
format,
res,
next,
opt_overlay,
opt_mode = 'tile',
) => {
if (
Math.abs(lon) > 180 ||
Math.abs(lat) > 85.06 ||
lon !== lon ||
lat !== lat
) {
return res.status(400).send('Invalid center'); return res.status(400).send('Invalid center');
} }
if (Math.min(width, height) <= 0 || if (
Math.min(width, height) <= 0 ||
Math.max(width, height) * scale > (options.maxSize || 2048) || Math.max(width, height) * scale > (options.maxSize || 2048) ||
width !== width || height !== height) { width !== width ||
height !== height
) {
return res.status(400).send('Invalid size'); return res.status(400).send('Invalid size');
} }
if (format === 'png' || format === 'webp') { if (format === 'png' || format === 'webp') {
@ -594,7 +644,7 @@ export const serve_rendered = {
bearing: bearing, bearing: bearing,
pitch: pitch, pitch: pitch,
width: width, width: width,
height: height height: height,
}; };
if (z === 0) { if (z === 0) {
params.width *= 2; params.width *= 2;
@ -634,18 +684,21 @@ export const serve_rendered = {
raw: { raw: {
width: params.width * scale, width: params.width * scale,
height: params.height * scale, height: params.height * scale,
channels: 4 channels: 4,
} },
}); });
if (z > 2 && tileMargin > 0) { if (z > 2 && tileMargin > 0) {
const [_, y] = mercator.px(params.center, z); const [_, y] = mercator.px(params.center, z);
let yoffset = Math.max(Math.min(0, y - 128 - tileMargin), y + 128 + tileMargin - Math.pow(2, z + 8)); let yoffset = Math.max(
Math.min(0, y - 128 - tileMargin),
y + 128 + tileMargin - Math.pow(2, z + 8),
);
image.extract({ image.extract({
left: tileMargin * scale, left: tileMargin * scale,
top: (tileMargin + yoffset) * scale, top: (tileMargin + yoffset) * scale,
width: width * scale, width: width * scale,
height: height * scale height: height * scale,
}); });
} }
@ -687,7 +740,7 @@ export const serve_rendered = {
res.set({ res.set({
'Last-Modified': item.lastModified, 'Last-Modified': item.lastModified,
'Content-Type': `image/${format}` 'Content-Type': `image/${format}`,
}); });
return res.status(200).send(buffer); return res.status(200).send(buffer);
}); });
@ -695,13 +748,16 @@ export const serve_rendered = {
}); });
}; };
app.get(`/:id/:z(\\d+)/:x(\\d+)/:y(\\d+):scale(${scalePattern})?.:format([\\w]+)`, (req, res, next) => { app.get(
`/:id/:z(\\d+)/:x(\\d+)/:y(\\d+):scale(${scalePattern})?.: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);
} }
const modifiedSince = req.get('if-modified-since'); const cc = req.get('cache-control'); const modifiedSince = req.get('if-modified-since');
const cc = req.get('cache-control');
if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) { if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) {
if (new Date(item.lastModified) <= new Date(modifiedSince)) { if (new Date(item.lastModified) <= new Date(modifiedSince)) {
return res.sendStatus(304); return res.sendStatus(304);
@ -713,28 +769,56 @@ export const serve_rendered = {
const y = req.params.y | 0; const y = req.params.y | 0;
const scale = getScale(req.params.scale); const scale = getScale(req.params.scale);
const format = req.params.format; const format = req.params.format;
if (z < 0 || x < 0 || y < 0 || if (
z > 22 || x >= Math.pow(2, z) || y >= Math.pow(2, z)) { z < 0 ||
x < 0 ||
y < 0 ||
z > 22 ||
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');
} }
const tileSize = 256; const tileSize = 256;
const tileCenter = mercator.ll([ const tileCenter = mercator.ll(
[
((x + 0.5) / (1 << z)) * (256 << z), ((x + 0.5) / (1 << z)) * (256 << z),
((y + 0.5) / (1 << z)) * (256 << z) ((y + 0.5) / (1 << z)) * (256 << z),
], z); ],
return respondImage(item, z, tileCenter[0], tileCenter[1], 0, 0, tileSize, tileSize, scale, format, res, next); z,
}); );
return respondImage(
item,
z,
tileCenter[0],
tileCenter[1],
0,
0,
tileSize,
tileSize,
scale,
format,
res,
next,
);
},
);
if (options.serveStaticMaps !== false) { if (options.serveStaticMaps !== false) {
const staticPattern = const staticPattern = `/:id/static/:raw(raw)?/%s/:width(\\d+)x:height(\\d+):scale(${scalePattern})?.:format([\\w]+)`;
`/:id/static/:raw(raw)?/%s/:width(\\d+)x:height(\\d+):scale(${scalePattern})?.:format([\\w]+)`;
const centerPattern = const centerPattern = util.format(
util.format(':x(%s),:y(%s),:z(%s)(@:bearing(%s)(,:pitch(%s))?)?', ':x(%s),:y(%s),:z(%s)(@:bearing(%s)(,:pitch(%s))?)?',
FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN,
FLOAT_PATTERN, FLOAT_PATTERN); FLOAT_PATTERN,
FLOAT_PATTERN,
FLOAT_PATTERN,
FLOAT_PATTERN,
);
app.get(util.format(staticPattern, centerPattern), async (req, res, next) => { app.get(
util.format(staticPattern, centerPattern),
async (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);
@ -754,8 +838,9 @@ export const serve_rendered = {
return res.status(404).send('Invalid zoom'); return res.status(404).send('Invalid zoom');
} }
const transformer = raw ? const transformer = raw
mercator.inverse.bind(mercator) : item.dataProjWGStoInternalWGS; ? mercator.inverse.bind(mercator)
: item.dataProjWGStoInternalWGS;
if (transformer) { if (transformer) {
const ll = transformer([x, y]); const ll = transformer([x, y]);
@ -764,11 +849,43 @@ export const serve_rendered = {
} }
const paths = extractPathsFromQuery(req.query, transformer); const paths = extractPathsFromQuery(req.query, transformer);
const markers = extractMarkersFromQuery(req.query, options, transformer); const markers = extractMarkersFromQuery(
const overlay = await renderOverlay(z, x, y, bearing, pitch, w, h, scale, paths, markers, req.query); req.query,
options,
transformer,
);
const overlay = await renderOverlay(
z,
x,
y,
bearing,
pitch,
w,
h,
scale,
paths,
markers,
req.query,
);
return respondImage(item, z, x, y, bearing, pitch, w, h, scale, format, res, next, overlay, 'static'); return respondImage(
}); item,
z,
x,
y,
bearing,
pitch,
w,
h,
scale,
format,
res,
next,
overlay,
'static',
);
},
);
const serveBounds = async (req, res, next) => { const serveBounds = async (req, res, next) => {
const item = repo[req.params.id]; const item = repo[req.params.id];
@ -776,11 +893,17 @@ export const serve_rendered = {
return res.sendStatus(404); return res.sendStatus(404);
} }
const raw = req.params.raw; const raw = req.params.raw;
const bbox = [+req.params.minx, +req.params.miny, +req.params.maxx, +req.params.maxy]; const bbox = [
+req.params.minx,
+req.params.miny,
+req.params.maxx,
+req.params.maxy,
];
let center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2]; let center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2];
const transformer = raw ? const transformer = raw
mercator.inverse.bind(mercator) : item.dataProjWGStoInternalWGS; ? mercator.inverse.bind(mercator)
: item.dataProjWGStoInternalWGS;
if (transformer) { if (transformer) {
const minCorner = transformer(bbox.slice(0, 2)); const minCorner = transformer(bbox.slice(0, 2));
@ -804,14 +927,49 @@ export const serve_rendered = {
const pitch = 0; const pitch = 0;
const paths = extractPathsFromQuery(req.query, transformer); const paths = extractPathsFromQuery(req.query, transformer);
const markers = extractMarkersFromQuery(req.query, options, transformer); const markers = extractMarkersFromQuery(
const overlay = await renderOverlay(z, x, y, bearing, pitch, w, h, scale, paths, markers, req.query); req.query,
return respondImage(item, z, x, y, bearing, pitch, w, h, scale, format, res, next, overlay, 'static'); options,
transformer,
);
const overlay = await renderOverlay(
z,
x,
y,
bearing,
pitch,
w,
h,
scale,
paths,
markers,
req.query,
);
return respondImage(
item,
z,
x,
y,
bearing,
pitch,
w,
h,
scale,
format,
res,
next,
overlay,
'static',
);
}; };
const boundsPattern = const boundsPattern = util.format(
util.format(':minx(%s),:miny(%s),:maxx(%s),:maxy(%s)', ':minx(%s),:miny(%s),:maxx(%s),:maxy(%s)',
FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN); FLOAT_PATTERN,
FLOAT_PATTERN,
FLOAT_PATTERN,
FLOAT_PATTERN,
);
app.get(util.format(staticPattern, boundsPattern), serveBounds); app.get(util.format(staticPattern, boundsPattern), serveBounds);
@ -839,7 +997,9 @@ export const serve_rendered = {
const autoPattern = 'auto'; const autoPattern = 'auto';
app.get(util.format(staticPattern, autoPattern), async (req, res, next) => { app.get(
util.format(staticPattern, autoPattern),
async (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);
@ -852,11 +1012,16 @@ export const serve_rendered = {
const scale = getScale(req.params.scale); const scale = getScale(req.params.scale);
const format = req.params.format; const format = req.params.format;
const transformer = raw ? const transformer = raw
mercator.inverse.bind(mercator) : item.dataProjWGStoInternalWGS; ? mercator.inverse.bind(mercator)
: item.dataProjWGStoInternalWGS;
const paths = extractPathsFromQuery(req.query, transformer); const paths = extractPathsFromQuery(req.query, transformer);
const markers = extractMarkersFromQuery(req.query, options, transformer); const markers = extractMarkersFromQuery(
req.query,
options,
transformer,
);
// Extract coordinates from markers // Extract coordinates from markers
const markerCoordinates = []; const markerCoordinates = [];
@ -865,7 +1030,7 @@ export const serve_rendered = {
} }
// Create array with coordinates from markers and path // Create array with coordinates from markers and path
const coords = new Array().concat(paths.flat()).concat(markerCoordinates); const coords = [].concat(paths.flat()).concat(markerCoordinates);
// Check if we have at least one coordinate to calculate a bounding box // Check if we have at least one coordinate to calculate a bounding box
if (coords.length < 1) { if (coords.length < 1) {
@ -881,9 +1046,10 @@ export const serve_rendered = {
} }
const bbox_ = mercator.convert(bbox, '900913'); const bbox_ = mercator.convert(bbox, '900913');
const center = mercator.inverse( const center = mercator.inverse([
[(bbox_[0] + bbox_[2]) / 2, (bbox_[1] + bbox_[3]) / 2] (bbox_[0] + bbox_[2]) / 2,
); (bbox_[1] + bbox_[3]) / 2,
]);
// Calculate zoom level // Calculate zoom level
const maxZoom = parseFloat(req.query.maxzoom); const maxZoom = parseFloat(req.query.maxzoom);
@ -895,10 +1061,38 @@ export const serve_rendered = {
const x = center[0]; const x = center[0];
const y = center[1]; const y = center[1];
const overlay = await renderOverlay(z, x, y, bearing, pitch, w, h, scale, paths, markers, req.query); const overlay = await renderOverlay(
z,
x,
y,
bearing,
pitch,
w,
h,
scale,
paths,
markers,
req.query,
);
return respondImage(item, z, x, y, bearing, pitch, w, h, scale, format, res, next, overlay, 'static'); return respondImage(
}); item,
z,
x,
y,
bearing,
pitch,
w,
h,
scale,
format,
res,
next,
overlay,
'static',
);
},
);
} }
app.get('/:id.json', (req, res, next) => { app.get('/:id.json', (req, res, next) => {
@ -907,8 +1101,13 @@ export const serve_rendered = {
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(
`styles/${req.params.id}`, info.format, item.publicUrl); req,
info.tiles,
`styles/${req.params.id}`,
info.format,
item.publicUrl,
);
return res.send(info); return res.send(info);
}); });
@ -918,7 +1117,7 @@ export const serve_rendered = {
const map = { const map = {
renderers: [], renderers: [],
renderers_static: [], renderers_static: [],
sources: {} sources: {},
}; };
let styleJSON; let styleJSON;
@ -941,12 +1140,19 @@ export const serve_rendered = {
const fontstack = unescape(parts[2]); const fontstack = unescape(parts[2]);
const range = parts[3].split('.')[0]; const range = parts[3].split('.')[0];
getFontsPbf( getFontsPbf(
null, options.paths[protocol], fontstack, range, existingFonts null,
).then((concated) => { options.paths[protocol],
fontstack,
range,
existingFonts,
).then(
(concated) => {
callback(null, { data: concated }); callback(null, { data: concated });
}, (err) => { },
(err) => {
callback(err, { data: null }); callback(err, { data: null });
}); },
);
} else if (protocol === 'mbtiles') { } else if (protocol === 'mbtiles') {
const parts = req.url.split('/'); const parts = req.url.split('/');
const sourceId = parts[2]; const sourceId = parts[2];
@ -958,8 +1164,13 @@ export const serve_rendered = {
const format = parts[5].split('.')[1]; const format = parts[5].split('.')[1];
source.getTile(z, x, y, (err, data, headers) => { source.getTile(z, x, y, (err, data, headers) => {
if (err) { if (err) {
if (options.verbose) console.log('MBTiles error, serving empty', err); if (options.verbose)
createEmptyResponse(sourceInfo.format, sourceInfo.color, callback); console.log('MBTiles error, serving empty', err);
createEmptyResponse(
sourceInfo.format,
sourceInfo.color,
callback,
);
return; return;
} }
@ -972,11 +1183,23 @@ export const serve_rendered = {
try { try {
response.data = zlib.unzipSync(data); response.data = zlib.unzipSync(data);
} catch (err) { } catch (err) {
console.log('Skipping incorrect header for tile mbtiles://%s/%s/%s/%s.pbf', id, z, x, y); console.log(
'Skipping incorrect header for tile mbtiles://%s/%s/%s/%s.pbf',
id,
z,
x,
y,
);
} }
if (options.dataDecoratorFunc) { if (options.dataDecoratorFunc) {
response.data = options.dataDecoratorFunc( response.data = options.dataDecoratorFunc(
sourceId, 'data', response.data, z, x, y); sourceId,
'data',
response.data,
z,
x,
y,
);
} }
} else { } else {
response.data = data; response.data = data;
@ -985,11 +1208,13 @@ export const serve_rendered = {
callback(null, response); callback(null, response);
}); });
} else if (protocol === 'http' || protocol === 'https') { } else if (protocol === 'http' || protocol === 'https') {
request({ request(
{
url: req.url, url: req.url,
encoding: null, encoding: null,
gzip: true gzip: true,
}, (err, res, body) => { },
(err, res, body) => {
const parts = url.parse(req.url); const parts = url.parse(req.url);
const extension = path.extname(parts.pathname).toLowerCase(); const extension = path.extname(parts.pathname).toLowerCase();
const format = extensionToFormat[extension] || ''; const format = extensionToFormat[extension] || '';
@ -1012,9 +1237,10 @@ export const serve_rendered = {
response.data = body; response.data = body;
callback(null, response); callback(null, response);
}); },
} );
} }
},
}); });
renderer.load(styleJSON); renderer.load(styleJSON);
createCallback(null, renderer); createCallback(null, renderer);
@ -1025,7 +1251,7 @@ export const serve_rendered = {
create: createRenderer.bind(null, ratio), create: createRenderer.bind(null, ratio),
destroy: (renderer) => { destroy: (renderer) => {
renderer.release(); renderer.release();
} },
}); });
}; };
@ -1039,16 +1265,20 @@ export const serve_rendered = {
} }
if (styleJSON.sprite && !httpTester.test(styleJSON.sprite)) { if (styleJSON.sprite && !httpTester.test(styleJSON.sprite)) {
styleJSON.sprite = 'sprites://' + styleJSON.sprite =
'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(styleJSONPath))); .replace(
'{styleJsonFolder}',
path.relative(options.paths.sprites, path.dirname(styleJSONPath)),
);
} }
if (styleJSON.glyphs && !httpTester.test(styleJSON.glyphs)) { if (styleJSON.glyphs && !httpTester.test(styleJSON.glyphs)) {
styleJSON.glyphs = `fonts://${styleJSON.glyphs}`; styleJSON.glyphs = `fonts://${styleJSON.glyphs}`;
} }
for (const layer of (styleJSON.layers || [])) { for (const layer of styleJSON.layers || []) {
if (layer && layer.paint) { if (layer && layer.paint) {
// Remove (flatten) 3D buildings // Remove (flatten) 3D buildings
if (layer.paint['fill-extrusion-height']) { if (layer.paint['fill-extrusion-height']) {
@ -1061,14 +1291,14 @@ export const serve_rendered = {
} }
const tileJSON = { const tileJSON = {
'tilejson': '2.0.0', tilejson: '2.0.0',
'name': styleJSON.name, name: styleJSON.name,
'attribution': '', attribution: '',
'minzoom': 0, minzoom: 0,
'maxzoom': 20, maxzoom: 20,
'bounds': [-180, -85.0511, 180, 85.0511], bounds: [-180, -85.0511, 180, 85.0511],
'format': 'png', format: 'png',
'type': 'baselayer' type: 'baselayer',
}; };
const attributionOverride = params.tilejson && params.tilejson.attribution; const attributionOverride = params.tilejson && params.tilejson.attribution;
Object.assign(tileJSON, params.tilejson || {}); Object.assign(tileJSON, params.tilejson || {});
@ -1081,7 +1311,7 @@ export const serve_rendered = {
map, map,
dataProjWGStoInternalWGS: null, dataProjWGStoInternalWGS: null,
lastModified: new Date().toUTCString(), lastModified: new Date().toUTCString(),
watermark: params.watermark || options.watermark watermark: params.watermark || options.watermark,
}; };
repo[id] = repoobj; repo[id] = repoobj;
@ -1095,8 +1325,8 @@ export const serve_rendered = {
delete source.url; delete source.url;
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);
@ -1111,13 +1341,14 @@ export const serve_rendered = {
} }
} }
queue.push(new Promise((resolve, reject) => { queue.push(
new Promise((resolve, reject) => {
mbtilesFile = path.resolve(options.paths.mbtiles, mbtilesFile); mbtilesFile = path.resolve(options.paths.mbtiles, mbtilesFile);
const mbtilesFileStats = fs.statSync(mbtilesFile); const mbtilesFileStats = fs.statSync(mbtilesFile);
if (!mbtilesFileStats.isFile() || mbtilesFileStats.size === 0) { if (!mbtilesFileStats.isFile() || mbtilesFileStats.size === 0) {
throw Error(`Not valid MBTiles file: ${mbtilesFile}`); throw Error(`Not valid MBTiles file: ${mbtilesFile}`);
} }
map.sources[name] = new MBTiles(mbtilesFile + '?mode=ro', err => { map.sources[name] = new MBTiles(mbtilesFile + '?mode=ro', (err) => {
map.sources[name].getInfo((err, info) => { map.sources[name].getInfo((err, info) => {
if (err) { if (err) {
console.error(err); console.error(err);
@ -1128,7 +1359,8 @@ export const serve_rendered = {
// how to do this for multiple sources with different proj4 defs? // how to do this for multiple sources with different proj4 defs?
const to3857 = proj4('EPSG:3857'); const to3857 = proj4('EPSG:3857');
const toDataProj = proj4(info.proj4); const toDataProj = proj4(info.proj4);
repoobj.dataProjWGStoInternalWGS = (xy) => to3857.inverse(toDataProj.forward(xy)); repoobj.dataProjWGStoInternalWGS = (xy) =>
to3857.inverse(toDataProj.forward(xy));
} }
const type = source.type; const type = source.type;
@ -1136,7 +1368,7 @@ export const serve_rendered = {
source.type = type; source.type = type;
source.tiles = [ source.tiles = [
// meta url which will be detected when requested // meta url which will be detected when requested
`mbtiles://${name}/{z}/{x}/{y}.${info.format || 'pbf'}` `mbtiles://${name}/{z}/{x}/{y}.${info.format || 'pbf'}`,
]; ];
delete source.scheme; delete source.scheme;
@ -1144,8 +1376,11 @@ export const serve_rendered = {
source = options.dataDecoratorFunc(name, 'tilejson', source); source = options.dataDecoratorFunc(name, 'tilejson', source);
} }
if (!attributionOverride && if (
source.attribution && source.attribution.length > 0) { !attributionOverride &&
source.attribution &&
source.attribution.length > 0
) {
if (!tileJSON.attribution.includes(source.attribution)) { if (!tileJSON.attribution.includes(source.attribution)) {
if (tileJSON.attribution.length > 0) { if (tileJSON.attribution.length > 0) {
tileJSON.attribution += ' | '; tileJSON.attribution += ' | ';
@ -1156,7 +1391,8 @@ export const serve_rendered = {
resolve(); resolve();
}); });
}); });
})); }),
);
} }
} }
@ -1170,7 +1406,12 @@ export const serve_rendered = {
const minPoolSize = minPoolSizes[i]; const minPoolSize = minPoolSizes[i];
const maxPoolSize = Math.max(minPoolSize, maxPoolSizes[j]); const maxPoolSize = Math.max(minPoolSize, maxPoolSizes[j]);
map.renderers[s] = createPool(s, 'tile', minPoolSize, maxPoolSize); map.renderers[s] = createPool(s, 'tile', minPoolSize, maxPoolSize);
map.renderers_static[s] = createPool(s, 'static', minPoolSize, maxPoolSize); map.renderers_static[s] = createPool(
s,
'static',
minPoolSize,
maxPoolSize,
);
} }
}); });
@ -1187,5 +1428,5 @@ export const serve_rendered = {
}); });
} }
delete repo[id]; delete repo[id];
} },
}; };

View file

@ -12,7 +12,7 @@ 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';
@ -25,11 +24,19 @@ 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;
/**
*
* @param opts
*/
function start(opts) { function start(opts) {
console.log('Starting server'); console.log('Starting server');
@ -38,18 +45,24 @@ function start(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,7 +87,8 @@ function start(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 || '');
@ -85,7 +99,9 @@ function start(opts) {
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);
} }
}; };
@ -98,35 +114,46 @@ function start(opts) {
/** /**
* Recursively get all files within a directory. * Recursively get all files within a directory.
* Inspired by https://stackoverflow.com/a/45130990/10133863 * Inspired by https://stackoverflow.com/a/45130990/10133863
* @param {String} directory Absolute path to a directory to get files from. *
* @param {string} directory Absolute path to a directory to get files from.
*/ */
const getFiles = async (directory) => { const getFiles = async (directory) => {
// Fetch all entries of the directory and attach type information // Fetch all entries of the directory and attach type information
const dirEntries = await fs.promises.readdir(directory, { withFileTypes: true }); 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 // Iterate through entries and return the relative file-path to the icon directory if it is not a directory
// otherwise initiate a recursive call // otherwise initiate a recursive call
const files = await Promise.all(dirEntries.map((dirEntry) => { const files = await Promise.all(
dirEntries.map((dirEntry) => {
const entryPath = path.resolve(directory, dirEntry.name); const entryPath = path.resolve(directory, dirEntry.name);
return dirEntry.isDirectory() ? return dirEntry.isDirectory()
getFiles(entryPath) : entryPath.replace(paths.icons + path.sep, ""); ? getFiles(entryPath)
})); : entryPath.replace(paths.icons + path.sep, '');
}),
);
// Flatten the list of files to a single array // Flatten the list of files to a single array
return files.flat(); return files.flat();
} };
// Load all available icons into a settings object // Load all available icons into a settings object
startupPromises.push(new Promise(resolve => { startupPromises.push(
new Promise((resolve) => {
getFiles(paths.icons).then((files) => { getFiles(paths.icons).then((files) => {
paths.availableIcons = files; paths.availableIcons = files;
resolve(); 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) {}
} }
@ -140,17 +167,21 @@ function start(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)) {
@ -164,30 +195,41 @@ function start(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)) {
@ -196,8 +238,9 @@ function start(opts) {
} }
} }
return mbtilesFile; return mbtilesFile;
} },
)); ),
);
} else { } else {
item.serve_rendered = false; item.serve_rendered = false;
} }
@ -217,7 +260,7 @@ function start(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)) {
@ -228,7 +271,7 @@ function start(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),
); );
} }
@ -238,22 +281,21 @@ function start(opts) {
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...`);
@ -265,7 +307,7 @@ function start(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);
} }
@ -275,14 +317,19 @@ function start(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);
@ -297,9 +344,16 @@ function start(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;
@ -325,12 +379,15 @@ function start(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}`);
@ -347,18 +404,24 @@ function start(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) => {
@ -371,15 +434,23 @@ function start(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 || {});
@ -388,19 +459,29 @@ function start(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';
@ -418,7 +499,7 @@ function start(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,
}; };
}); });
@ -453,9 +534,12 @@ function start(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;
}); });
@ -484,13 +568,17 @@ function start(opts) {
} }
}); });
const server = app.listen(process.env.PORT || opts.port, process.env.BIND || opts.bind, function() { const server = app.listen(
process.env.PORT || opts.port,
process.env.BIND || opts.bind,
function () {
let address = this.address().address; let address = this.address().address;
if (address.indexOf('::') === 0) { if (address.indexOf('::') === 0) {
address = `[${address}]`; // literal IPv6 address address = `[${address}]`; // literal IPv6 address
} }
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);
@ -498,10 +586,14 @@ function start(opts) {
return { return {
app: app, app: app,
server: server, server: server,
startupPromise: startupPromise startupPromise: startupPromise,
}; };
} }
/**
*
* @param opts
*/
export function server(opts) { export function server(opts) {
const running = start(opts); const running = start(opts);
@ -525,4 +617,4 @@ export function server(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}`);
} }
@ -116,12 +123,24 @@ const getFontPbf = (allowedFonts, fontPath, name, range, fallbacks) => new Promi
} }
}); });
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

@ -13,7 +13,8 @@ const testTileJSONArray = function(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);
}); });
}); });
}; };
@ -32,7 +33,8 @@ const testTileJSON = function(url) {
.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);
}); });
}); });
}; };
@ -40,9 +42,7 @@ const testTileJSON = function(url) {
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);
}); });
}); });
@ -67,7 +67,8 @@ describe('Metadata', function() {
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

@ -13,7 +13,7 @@ before(function() {
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;
@ -23,6 +23,7 @@ before(function() {
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

@ -18,10 +18,38 @@ 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 () {
@ -60,10 +88,38 @@ 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 () {
@ -85,18 +141,58 @@ 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=invalid'); 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,11 +1,13 @@
const testIs = function (url, type, status) { const testIs = function (url, type, status) {
it(url + ' return ' + (status || 200) + ' and is ' + type.toString(), it(
url + ' return ' + (status || 200) + ' and is ' + type.toString(),
function (done) { function (done) {
supertest(app) supertest(app)
.get(url) .get(url)
.expect(status || 200) .expect(status || 200)
.expect('Content-Type', type, done); .expect('Content-Type', type, done);
}); },
);
}; };
const prefix = 'test-style'; const prefix = 'test-style';
@ -25,7 +27,8 @@ 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 () {
@ -43,8 +46,10 @@ 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);