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",
"bin": "src/main.js",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/maptiler/tileserver-gl.git"
},
"license": "BSD-2-Clause",
"engines": {
"node": ">=14.15.0 <17"
},
"scripts": {
"test": "mocha test/**.js --timeout 10000",
"docker": "docker build -f Dockerfile . && docker run --rm -i -p 8080:80 $(docker build -q .)"
"lint:yml": "yamllint --schema=CORE_SCHEMA *.{yml,yaml}",
"lint:js": "npm run lint:eslint && npm run lint:prettier",
"lint:js:fix": "npm run lint:eslint:fix && npm run lint:prettier:fix",
"lint:eslint": "eslint \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs}\" --ignore-path .gitignore",
"lint:eslint:fix": "eslint --fix \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs}\" --ignore-path .gitignore",
"lint:prettier": "prettier --check \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs,json}\" --ignore-path .gitignore",
"lint:prettier:fix": "prettier --write \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs,json}\" --ignore-path .gitignore",
"docker": "docker build -f Dockerfile . && docker run --rm -i -p 8080:80 $(docker build -q .)",
"prepare": "husky install"
},
"dependencies": {
"@mapbox/glyph-pbf-composite": "0.0.3",
@ -43,8 +43,40 @@
"sanitize-filename": "1.6.3"
},
"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",
"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",
"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,21 +12,27 @@
// SYNC THE `light` FOLDER
import child_process from 'child_process'
child_process.execSync('rsync -av --exclude="light" --exclude=".git" --exclude="node_modules" --delete . light', {
stdio: 'inherit'
});
import child_process from 'child_process';
child_process.execSync(
'rsync -av --exclude="light" --exclude=".git" --exclude="node_modules" --delete . light',
{
stdio: 'inherit',
},
);
// PATCH `package.json`
import fs from 'fs';
import path from 'path';
import {fileURLToPath} from 'url';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageJson = JSON.parse(fs.readFileSync(__dirname + '/package.json', 'utf8'))
const packageJson = JSON.parse(
fs.readFileSync(__dirname + '/package.json', 'utf8'),
);
packageJson.name += '-light';
packageJson.description = 'Map tile server for JSON GL styles - serving vector tiles';
packageJson.description =
'Map tile server for JSON GL styles - serving vector tiles';
delete packageJson.dependencies['canvas'];
delete packageJson.dependencies['@maplibre/maplibre-gl-native'];
delete packageJson.dependencies['sharp'];
@ -51,10 +57,10 @@ if (process.argv.length > 2 && process.argv[2] == '--no-publish') {
// tileserver-gl
child_process.execSync('npm publish . --access public', {
stdio: 'inherit'
stdio: 'inherit',
});
// tileserver-gl-light
child_process.execSync('npm publish ./light --access public', {
stdio: 'inherit'
stdio: 'inherit',
});

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -2,8 +2,7 @@
'use strict';
import os from 'os';
process.env.UV_THREADPOOL_SIZE =
Math.ceil(Math.max(4, os.cpus().length * 1.5));
process.env.UV_THREADPOOL_SIZE = Math.ceil(Math.max(4, os.cpus().length * 1.5));
import fs from 'node:fs';
import path from 'path';
@ -17,19 +16,27 @@ import handlebars from 'handlebars';
import SphericalMercator from '@mapbox/sphericalmercator';
const mercator = new SphericalMercator();
import morgan from 'morgan';
import {serve_data} from './serve_data.js';
import {serve_style} from './serve_style.js';
import {serve_font} from './serve_font.js';
import {getTileUrls, getPublicUrl} from './utils.js';
import { serve_data } from './serve_data.js';
import { serve_style } from './serve_style.js';
import { serve_font } from './serve_font.js';
import { getTileUrls, getPublicUrl } from './utils.js';
import {fileURLToPath} from 'url';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageJson = JSON.parse(fs.readFileSync(__dirname + '/../package.json', 'utf8'));
const packageJson = JSON.parse(
fs.readFileSync(__dirname + '/../package.json', 'utf8'),
);
const isLight = packageJson.name.slice(-6) === '-light';
const serve_rendered = (await import(`${!isLight ? `./serve_rendered.js` : `./serve_light.js`}`)).serve_rendered;
const serve_rendered = (
await import(`${!isLight ? `./serve_rendered.js` : `./serve_light.js`}`)
).serve_rendered;
/**
*
* @param opts
*/
function start(opts) {
console.log('Starting server');
@ -38,18 +45,24 @@ function start(opts) {
styles: {},
rendered: {},
data: {},
fonts: {}
fonts: {},
};
app.enable('trust proxy');
if (process.env.NODE_ENV !== 'test') {
const defaultLogFormat = process.env.NODE_ENV === 'production' ? 'tiny' : 'dev';
const defaultLogFormat =
process.env.NODE_ENV === 'production' ? 'tiny' : 'dev';
const logFormat = opts.logFormat || defaultLogFormat;
app.use(morgan(logFormat, {
stream: opts.logFile ? fs.createWriteStream(opts.logFile, {flags: 'a'}) : process.stdout,
skip: (req, res) => opts.silent && (res.statusCode === 200 || res.statusCode === 304)
}));
app.use(
morgan(logFormat, {
stream: opts.logFile
? fs.createWriteStream(opts.logFile, { flags: 'a' })
: process.stdout,
skip: (req, res) =>
opts.silent && (res.statusCode === 200 || res.statusCode === 304),
}),
);
}
let config = opts.config || null;
@ -74,7 +87,8 @@ function start(opts) {
options.paths = paths;
paths.root = path.resolve(
configPath ? path.dirname(configPath) : process.cwd(),
paths.root || '');
paths.root || '',
);
paths.styles = path.resolve(paths.root, paths.styles || '');
paths.fonts = path.resolve(paths.root, paths.fonts || '');
paths.sprites = path.resolve(paths.root, paths.sprites || '');
@ -85,7 +99,9 @@ function start(opts) {
const checkPath = (type) => {
if (!fs.existsSync(paths[type])) {
console.error(`The specified path for "${type}" does not exist (${paths[type]}).`);
console.error(
`The specified path for "${type}" does not exist (${paths[type]}).`,
);
process.exit(1);
}
};
@ -96,37 +112,48 @@ function start(opts) {
checkPath('icons');
/**
* Recursively get all files within a directory.
* Inspired by https://stackoverflow.com/a/45130990/10133863
* @param {String} directory Absolute path to a directory to get files from.
*/
* Recursively get all files within a directory.
* Inspired by https://stackoverflow.com/a/45130990/10133863
*
* @param {string} directory Absolute path to a directory to get files from.
*/
const getFiles = async (directory) => {
// Fetch all entries of the directory and attach type information
const dirEntries = await fs.promises.readdir(directory, { withFileTypes: true });
const dirEntries = await fs.promises.readdir(directory, {
withFileTypes: true,
});
// Iterate through entries and return the relative file-path to the icon directory if it is not a directory
// otherwise initiate a recursive call
const files = await Promise.all(dirEntries.map((dirEntry) => {
const entryPath = path.resolve(directory, dirEntry.name);
return dirEntry.isDirectory() ?
getFiles(entryPath) : entryPath.replace(paths.icons + path.sep, "");
}));
const files = await Promise.all(
dirEntries.map((dirEntry) => {
const entryPath = path.resolve(directory, dirEntry.name);
return dirEntry.isDirectory()
? getFiles(entryPath)
: entryPath.replace(paths.icons + path.sep, '');
}),
);
// Flatten the list of files to a single array
return files.flat();
}
};
// Load all available icons into a settings object
startupPromises.push(new Promise(resolve => {
getFiles(paths.icons).then((files) => {
paths.availableIcons = files;
resolve();
});
}));
startupPromises.push(
new Promise((resolve) => {
getFiles(paths.icons).then((files) => {
paths.availableIcons = files;
resolve();
});
}),
);
if (options.dataDecorator) {
try {
options.dataDecoratorFunc = require(path.resolve(paths.root, options.dataDecorator));
options.dataDecoratorFunc = require(path.resolve(
paths.root,
options.dataDecorator,
));
} catch (e) {}
}
@ -140,54 +167,69 @@ function start(opts) {
app.use('/styles/', serve_style.init(options, serving.styles));
if (!isLight) {
startupPromises.push(
serve_rendered.init(options, serving.rendered)
.then((sub) => {
app.use('/styles/', sub);
})
serve_rendered.init(options, serving.rendered).then((sub) => {
app.use('/styles/', sub);
}),
);
}
const addStyle = (id, item, allowMoreData, reportFonts) => {
let success = true;
if (item.serve_data !== false) {
success = serve_style.add(options, serving.styles, item, id, opts.publicUrl,
(mbtiles, fromData) => {
let dataItemId;
for (const id of Object.keys(data)) {
if (fromData) {
if (id === mbtiles) {
dataItemId = id;
}
} else {
if (data[id].mbtiles === mbtiles) {
dataItemId = id;
}
success = serve_style.add(
options,
serving.styles,
item,
id,
opts.publicUrl,
(mbtiles, fromData) => {
let dataItemId;
for (const id of Object.keys(data)) {
if (fromData) {
if (id === mbtiles) {
dataItemId = id;
}
}
if (dataItemId) { // mbtiles exist in the data config
return dataItemId;
} else {
if (fromData || !allowMoreData) {
console.log(`ERROR: style "${item.style}" using unknown mbtiles "${mbtiles}"! Skipping...`);
return undefined;
} else {
let id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles;
while (data[id]) id += '_';
data[id] = {
'mbtiles': mbtiles
};
return id;
if (data[id].mbtiles === mbtiles) {
dataItemId = id;
}
}
}, (font) => {
if (reportFonts) {
serving.fonts[font] = true;
}
if (dataItemId) {
// mbtiles exist in the data config
return dataItemId;
} else {
if (fromData || !allowMoreData) {
console.log(
`ERROR: style "${item.style}" using unknown mbtiles "${mbtiles}"! Skipping...`,
);
return undefined;
} else {
let id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles;
while (data[id]) id += '_';
data[id] = {
mbtiles: mbtiles,
};
return id;
}
});
}
},
(font) => {
if (reportFonts) {
serving.fonts[font] = true;
}
},
);
}
if (success && item.serve_rendered !== false) {
if (!isLight) {
startupPromises.push(serve_rendered.add(options, serving.rendered, item, id, opts.publicUrl,
startupPromises.push(
serve_rendered.add(
options,
serving.rendered,
item,
id,
opts.publicUrl,
(mbtiles) => {
let mbtilesFile;
for (const id of Object.keys(data)) {
@ -196,8 +238,9 @@ function start(opts) {
}
}
return mbtilesFile;
}
));
},
),
);
} else {
item.serve_rendered = false;
}
@ -215,9 +258,9 @@ function start(opts) {
}
startupPromises.push(
serve_font(options, serving.fonts).then((sub) => {
app.use('/', sub);
})
serve_font(options, serving.fonts).then((sub) => {
app.use('/', sub);
}),
);
for (const id of Object.keys(data)) {
@ -228,61 +271,65 @@ function start(opts) {
}
startupPromises.push(
serve_data.add(options, serving.data, item, id, opts.publicUrl)
serve_data.add(options, serving.data, item, id, opts.publicUrl),
);
}
if (options.serveAllStyles) {
fs.readdir(options.paths.styles, {withFileTypes: true}, (err, files) => {
fs.readdir(options.paths.styles, { withFileTypes: true }, (err, files) => {
if (err) {
return;
}
for (const file of files) {
if (file.isFile() &&
path.extname(file.name).toLowerCase() == '.json') {
if (file.isFile() && path.extname(file.name).toLowerCase() == '.json') {
const id = path.basename(file.name, '.json');
const item = {
style: file.name
style: file.name,
};
addStyle(id, item, false, false);
}
}
});
const watcher = chokidar.watch(path.join(options.paths.styles, '*.json'),
{
});
watcher.on('all',
(eventType, filename) => {
if (filename) {
const id = path.basename(filename, '.json');
console.log(`Style "${id}" changed, updating...`);
const watcher = chokidar.watch(
path.join(options.paths.styles, '*.json'),
{},
);
watcher.on('all', (eventType, filename) => {
if (filename) {
const id = path.basename(filename, '.json');
console.log(`Style "${id}" changed, updating...`);
serve_style.remove(serving.styles, id);
if (!isLight) {
serve_rendered.remove(serving.rendered, id);
}
serve_style.remove(serving.styles, id);
if (!isLight) {
serve_rendered.remove(serving.rendered, id);
}
if (eventType == 'add' || eventType == 'change') {
const item = {
style: filename
};
addStyle(id, item, false, false);
}
}
});
if (eventType == 'add' || eventType == 'change') {
const item = {
style: filename,
};
addStyle(id, item, false, false);
}
}
});
}
app.get('/styles.json', (req, res, next) => {
const result = [];
const query = req.query.key ? (`?key=${encodeURIComponent(req.query.key)}`) : '';
const query = req.query.key
? `?key=${encodeURIComponent(req.query.key)}`
: '';
for (const id of Object.keys(serving.styles)) {
const styleJSON = serving.styles[id].styleJSON;
result.push({
version: styleJSON.version,
name: styleJSON.name,
id: id,
url: `${getPublicUrl(opts.publicUrl, req)}styles/${id}/style.json${query}`
url: `${getPublicUrl(
opts.publicUrl,
req,
)}styles/${id}/style.json${query}`,
});
}
res.send(result);
@ -297,9 +344,16 @@ function start(opts) {
} else {
path = `${type}/${id}`;
}
info.tiles = getTileUrls(req, info.tiles, path, info.format, opts.publicUrl, {
'pbf': options.pbfAlias
});
info.tiles = getTileUrls(
req,
info.tiles,
path,
info.format,
opts.publicUrl,
{
pbf: options.pbfAlias,
},
);
arr.push(info);
}
return arr;
@ -325,40 +379,49 @@ function start(opts) {
if (template === 'index') {
if (options.frontPage === false) {
return;
} else if (options.frontPage &&
options.frontPage.constructor === String) {
} else if (
options.frontPage &&
options.frontPage.constructor === String
) {
templateFile = path.resolve(paths.root, options.frontPage);
}
}
startupPromises.push(new Promise((resolve, reject) => {
fs.readFile(templateFile, (err, content) => {
if (err) {
err = new Error(`Template not found: ${err.message}`);
reject(err);
return;
}
const compiled = handlebars.compile(content.toString());
app.use(urlPath, (req, res, next) => {
let data = {};
if (dataGetter) {
data = dataGetter(req);
if (!data) {
return res.status(404).send('Not found');
}
startupPromises.push(
new Promise((resolve, reject) => {
fs.readFile(templateFile, (err, content) => {
if (err) {
err = new Error(`Template not found: ${err.message}`);
reject(err);
return;
}
data['server_version'] = `${packageJson.name} v${packageJson.version}`;
data['public_url'] = opts.publicUrl || '/';
data['is_light'] = isLight;
data['key_query_part'] =
req.query.key ? `key=${encodeURIComponent(req.query.key)}&amp;` : '';
data['key_query'] = req.query.key ? `?key=${encodeURIComponent(req.query.key)}` : '';
if (template === 'wmts') res.set('Content-Type', 'text/xml');
return res.status(200).send(compiled(data));
const compiled = handlebars.compile(content.toString());
app.use(urlPath, (req, res, next) => {
let data = {};
if (dataGetter) {
data = dataGetter(req);
if (!data) {
return res.status(404).send('Not found');
}
}
data[
'server_version'
] = `${packageJson.name} v${packageJson.version}`;
data['public_url'] = opts.publicUrl || '/';
data['is_light'] = isLight;
data['key_query_part'] = req.query.key
? `key=${encodeURIComponent(req.query.key)}&amp;`
: '';
data['key_query'] = req.query.key
? `?key=${encodeURIComponent(req.query.key)}`
: '';
if (template === 'wmts') res.set('Content-Type', 'text/xml');
return res.status(200).send(compiled(data));
});
resolve();
});
resolve();
});
}));
}),
);
};
serveTemplate('/$', 'index', (req) => {
@ -371,15 +434,23 @@ function start(opts) {
if (style.serving_rendered) {
const center = style.serving_rendered.tileJSON.center;
if (center) {
style.viewer_hash = `#${center[2]}/${center[1].toFixed(5)}/${center[0].toFixed(5)}`;
style.viewer_hash = `#${center[2]}/${center[1].toFixed(
5,
)}/${center[0].toFixed(5)}`;
const centerPx = mercator.px([center[0], center[1]], center[2]);
style.thumbnail = `${center[2]}/${Math.floor(centerPx[0] / 256)}/${Math.floor(centerPx[1] / 256)}.png`;
style.thumbnail = `${center[2]}/${Math.floor(
centerPx[0] / 256,
)}/${Math.floor(centerPx[1] / 256)}.png`;
}
style.xyz_link = getTileUrls(
req, style.serving_rendered.tileJSON.tiles,
`styles/${id}`, style.serving_rendered.tileJSON.format, opts.publicUrl)[0];
req,
style.serving_rendered.tileJSON.tiles,
`styles/${id}`,
style.serving_rendered.tileJSON.format,
opts.publicUrl,
)[0];
}
}
const data = clone(serving.data || {});
@ -388,19 +459,29 @@ function start(opts) {
const tilejson = data[id].tileJSON;
const center = tilejson.center;
if (center) {
data_.viewer_hash = `#${center[2]}/${center[1].toFixed(5)}/${center[0].toFixed(5)}`;
data_.viewer_hash = `#${center[2]}/${center[1].toFixed(
5,
)}/${center[0].toFixed(5)}`;
}
data_.is_vector = tilejson.format === 'pbf';
if (!data_.is_vector) {
if (center) {
const centerPx = mercator.px([center[0], center[1]], center[2]);
data_.thumbnail = `${center[2]}/${Math.floor(centerPx[0] / 256)}/${Math.floor(centerPx[1] / 256)}.${data_.tileJSON.format}`;
data_.thumbnail = `${center[2]}/${Math.floor(
centerPx[0] / 256,
)}/${Math.floor(centerPx[1] / 256)}.${data_.tileJSON.format}`;
}
data_.xyz_link = getTileUrls(
req, tilejson.tiles, `data/${id}`, tilejson.format, opts.publicUrl, {
'pbf': options.pbfAlias
})[0];
req,
tilejson.tiles,
`data/${id}`,
tilejson.format,
opts.publicUrl,
{
pbf: options.pbfAlias,
},
)[0];
}
if (data_.filesize) {
let suffix = 'kB';
@ -418,7 +499,7 @@ function start(opts) {
}
return {
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;
if (opts.publicUrl) {
wmts.baseUrl = opts.publicUrl;
}
else {
wmts.baseUrl = `${req.get('X-Forwarded-Protocol') ? req.get('X-Forwarded-Protocol') : req.protocol}://${req.get('host')}/`;
} else {
wmts.baseUrl = `${
req.get('X-Forwarded-Protocol')
? req.get('X-Forwarded-Protocol')
: req.protocol
}://${req.get('host')}/`;
}
return wmts;
});
@ -484,13 +568,17 @@ function start(opts) {
}
});
const server = app.listen(process.env.PORT || opts.port, process.env.BIND || opts.bind, function() {
let address = this.address().address;
if (address.indexOf('::') === 0) {
address = `[${address}]`; // literal IPv6 address
}
console.log(`Listening at http://${address}:${this.address().port}/`);
});
const server = app.listen(
process.env.PORT || opts.port,
process.env.BIND || opts.bind,
function () {
let address = this.address().address;
if (address.indexOf('::') === 0) {
address = `[${address}]`; // literal IPv6 address
}
console.log(`Listening at http://${address}:${this.address().port}/`);
},
);
// add server.shutdown() to gracefully stop serving
enableShutdown(server);
@ -498,10 +586,14 @@ function start(opts) {
return {
app: app,
server: server,
startupPromise: startupPromise
startupPromise: startupPromise,
};
}
/**
*
* @param opts
*/
export function server(opts) {
const running = start(opts);
@ -525,4 +617,4 @@ export function server(opts) {
});
return running;
};
}

View file

@ -6,8 +6,8 @@ import fs from 'node:fs';
import clone from 'clone';
import glyphCompose from '@mapbox/glyph-pbf-composite';
export const getPublicUrl = (publicUrl, req) => publicUrl || `${req.protocol}://${req.headers.host}/`;
export const getPublicUrl = (publicUrl, req) =>
publicUrl || `${req.protocol}://${req.headers.host}/`;
export const getTileUrls = (req, domains, path, format, publicUrl, aliases) => {
if (domains) {
@ -16,7 +16,8 @@ export const getTileUrls = (req, domains, path, format, publicUrl, aliases) => {
}
const host = req.headers.host;
const hostParts = host.split('.');
const relativeSubdomainsUsable = hostParts.length > 1 &&
const relativeSubdomainsUsable =
hostParts.length > 1 &&
!/^([0-9]{1,3}\.){3}[0-9]{1,3}(\:[0-9]+)?$/.test(host);
const newDomains = [];
for (const domain of domains) {
@ -43,7 +44,7 @@ export const getTileUrls = (req, domains, path, format, publicUrl, aliases) => {
if (req.query.style) {
queryParams.push(`style=${encodeURIComponent(req.query.style)}`);
}
const query = queryParams.length > 0 ? (`?${queryParams.join('&')}`) : '';
const query = queryParams.length > 0 ? `?${queryParams.join('&')}` : '';
if (aliases && aliases[format]) {
format = aliases[format];
@ -52,7 +53,9 @@ export const getTileUrls = (req, domains, path, format, publicUrl, aliases) => {
const uris = [];
if (!publicUrl) {
for (const domain of domains) {
uris.push(`${req.protocol}://${domain}/${path}/{z}/{x}/{y}.${format}${query}`);
uris.push(
`${req.protocol}://${domain}/${path}/{z}/{x}/{y}.${format}${query}`,
);
}
} else {
uris.push(`${publicUrl}${path}/{z}/{x}/{y}.${format}${query}`);
@ -69,59 +72,75 @@ export const fixTileJSONCenter = (tileJSON) => {
(tileJSON.bounds[0] + tileJSON.bounds[2]) / 2,
(tileJSON.bounds[1] + tileJSON.bounds[3]) / 2,
Math.round(
-Math.log((tileJSON.bounds[2] - tileJSON.bounds[0]) / 360 / tiles) /
Math.LN2
)
-Math.log((tileJSON.bounds[2] - tileJSON.bounds[0]) / 360 / tiles) /
Math.LN2,
),
];
}
};
const getFontPbf = (allowedFonts, fontPath, name, range, fallbacks) => new Promise((resolve, reject) => {
if (!allowedFonts || (allowedFonts[name] && fallbacks)) {
const filename = path.join(fontPath, name, `${range}.pbf`);
if (!fallbacks) {
fallbacks = clone(allowedFonts || {});
}
delete fallbacks[name];
fs.readFile(filename, (err, data) => {
if (err) {
console.error(`ERROR: Font not found: ${name}`);
if (fallbacks && Object.keys(fallbacks).length) {
let fallbackName;
let fontStyle = name.split(' ').pop();
if (['Regular', 'Bold', 'Italic'].indexOf(fontStyle) < 0) {
fontStyle = 'Regular';
}
fallbackName = `Noto Sans ${fontStyle}`;
if (!fallbacks[fallbackName]) {
fallbackName = `Open Sans ${fontStyle}`;
if (!fallbacks[fallbackName]) {
fallbackName = Object.keys(fallbacks)[0];
}
}
console.error(`ERROR: Trying to use ${fallbackName} as a fallback`);
delete fallbacks[fallbackName];
getFontPbf(null, fontPath, fallbackName, range, fallbacks).then(resolve, reject);
} else {
reject(`Font load error: ${name}`);
}
} else {
resolve(data);
const getFontPbf = (allowedFonts, fontPath, name, range, fallbacks) =>
new Promise((resolve, reject) => {
if (!allowedFonts || (allowedFonts[name] && fallbacks)) {
const filename = path.join(fontPath, name, `${range}.pbf`);
if (!fallbacks) {
fallbacks = clone(allowedFonts || {});
}
});
} else {
reject(`Font not allowed: ${name}`);
}
});
delete fallbacks[name];
fs.readFile(filename, (err, data) => {
if (err) {
console.error(`ERROR: Font not found: ${name}`);
if (fallbacks && Object.keys(fallbacks).length) {
let fallbackName;
export const getFontsPbf = (allowedFonts, fontPath, names, range, fallbacks) => {
let fontStyle = name.split(' ').pop();
if (['Regular', 'Bold', 'Italic'].indexOf(fontStyle) < 0) {
fontStyle = 'Regular';
}
fallbackName = `Noto Sans ${fontStyle}`;
if (!fallbacks[fallbackName]) {
fallbackName = `Open Sans ${fontStyle}`;
if (!fallbacks[fallbackName]) {
fallbackName = Object.keys(fallbacks)[0];
}
}
console.error(`ERROR: Trying to use ${fallbackName} as a fallback`);
delete fallbacks[fallbackName];
getFontPbf(null, fontPath, fallbackName, range, fallbacks).then(
resolve,
reject,
);
} else {
reject(`Font load error: ${name}`);
}
} else {
resolve(data);
}
});
} else {
reject(`Font not allowed: ${name}`);
}
});
export const getFontsPbf = (
allowedFonts,
fontPath,
names,
range,
fallbacks,
) => {
const fonts = names.split(',');
const queue = [];
for (const font of fonts) {
queue.push(
getFontPbf(allowedFonts, fontPath, font, range, clone(allowedFonts || fallbacks))
getFontPbf(
allowedFonts,
fontPath,
font,
range,
clone(allowedFonts || fallbacks),
),
);
}

View file

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

View file

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

View file

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

View file

@ -1,38 +1,41 @@
const testIs = function(url, type, status) {
it(url + ' return ' + (status || 200) + ' and is ' + type.toString(),
function(done) {
supertest(app)
.get(url)
.expect(status || 200)
.expect('Content-Type', type, done);
});
const testIs = function (url, type, status) {
it(
url + ' return ' + (status || 200) + ' and is ' + type.toString(),
function (done) {
supertest(app)
.get(url)
.expect(status || 200)
.expect('Content-Type', type, done);
},
);
};
const prefix = 'test-style';
describe('Styles', function() {
describe('/styles/' + prefix + '/style.json is valid style', function() {
describe('Styles', function () {
describe('/styles/' + prefix + '/style.json is valid style', function () {
testIs('/styles/' + prefix + '/style.json', /application\/json/);
it('contains expected properties', function(done) {
it('contains expected properties', function (done) {
supertest(app)
.get('/styles/' + prefix + '/style.json')
.expect(function(res) {
expect(res.body.version).to.be.equal(8);
expect(res.body.name).to.be.a('string');
expect(res.body.sources).to.be.a('object');
expect(res.body.glyphs).to.be.a('string');
expect(res.body.sprite).to.be.a('string');
expect(res.body.sprite).to.be.equal('/test/styles/test-style/sprite');
expect(res.body.layers).to.be.a('array');
}).end(done);
.get('/styles/' + prefix + '/style.json')
.expect(function (res) {
expect(res.body.version).to.be.equal(8);
expect(res.body.name).to.be.a('string');
expect(res.body.sources).to.be.a('object');
expect(res.body.glyphs).to.be.a('string');
expect(res.body.sprite).to.be.a('string');
expect(res.body.sprite).to.be.equal('/test/styles/test-style/sprite');
expect(res.body.layers).to.be.a('array');
})
.end(done);
});
});
describe('/styles/streets/style.json is not served', function() {
describe('/styles/streets/style.json is not served', function () {
testIs('/styles/streets/style.json', /./, 404);
});
describe('/styles/' + prefix + '/sprite[@2x].{format}', function() {
describe('/styles/' + prefix + '/sprite[@2x].{format}', function () {
testIs('/styles/' + prefix + '/sprite.json', /application\/json/);
testIs('/styles/' + prefix + '/sprite@2x.json', /application\/json/);
testIs('/styles/' + prefix + '/sprite.png', /image\/png/);
@ -40,11 +43,13 @@ describe('Styles', function() {
});
});
describe('Fonts', function() {
describe('Fonts', function () {
testIs('/fonts/Open Sans Bold/0-255.pbf', /application\/x-protobuf/);
testIs('/fonts/Open Sans Regular/65280-65535.pbf', /application\/x-protobuf/);
testIs('/fonts/Open Sans Bold,Open Sans Regular/0-255.pbf',
/application\/x-protobuf/);
testIs(
'/fonts/Open Sans Bold,Open Sans Regular/0-255.pbf',
/application\/x-protobuf/,
);
testIs('/fonts/Nonsense,Open Sans Bold/0-255.pbf', /./, 400);
testIs('/fonts/Nonsense/0-255.pbf', /./, 400);

View file

@ -1,6 +1,6 @@
const testTile = function(prefix, z, x, y, status) {
const testTile = function (prefix, z, x, y, status) {
const path = '/data/' + prefix + '/' + z + '/' + x + '/' + y + '.pbf';
it(path + ' returns ' + status, function(done) {
it(path + ' returns ' + status, function (done) {
const test = supertest(app).get(path);
if (status) test.expect(status);
if (status == 200) test.expect('Content-Type', /application\/x-protobuf/);
@ -10,13 +10,13 @@ const testTile = function(prefix, z, x, y, status) {
const prefix = 'openmaptiles';
describe('Vector tiles', function() {
describe('existing tiles', function() {
describe('Vector tiles', function () {
describe('existing tiles', function () {
testTile(prefix, 0, 0, 0, 200);
testTile(prefix, 14, 8581, 5738, 200);
});
describe('non-existent requests return 4xx', function() {
describe('non-existent requests return 4xx', function () {
testTile('non_existent', 0, 0, 0, 404);
testTile(prefix, -1, 0, 0, 404); // err zoom
testTile(prefix, 20, 0, 0, 404); // zoom out of bounds

View file

@ -1,7 +1,7 @@
const testTile = function(prefix, z, x, y, format, status, scale, type) {
const testTile = function (prefix, z, x, y, format, status, scale, type) {
if (scale) y += '@' + scale + 'x';
const path = '/styles/' + prefix + '/' + z + '/' + x + '/' + y + '.' + format;
it(path + ' returns ' + status, function(done) {
it(path + ' returns ' + status, function (done) {
const test = supertest(app).get(path);
test.expect(status);
if (type) test.expect('Content-Type', type);
@ -11,16 +11,16 @@ const testTile = function(prefix, z, x, y, format, status, scale, type) {
const prefix = 'test-style';
describe('Raster tiles', function() {
describe('valid requests', function() {
describe('various formats', function() {
describe('Raster tiles', function () {
describe('valid requests', function () {
describe('various formats', function () {
testTile(prefix, 0, 0, 0, 'png', 200, undefined, /image\/png/);
testTile(prefix, 0, 0, 0, 'jpg', 200, undefined, /image\/jpeg/);
testTile(prefix, 0, 0, 0, 'jpeg', 200, undefined, /image\/jpeg/);
testTile(prefix, 0, 0, 0, 'webp', 200, undefined, /image\/webp/);
});
describe('different coordinates and scales', function() {
describe('different coordinates and scales', function () {
testTile(prefix, 1, 1, 1, 'png', 200);
testTile(prefix, 0, 0, 0, 'png', 200, 2);
@ -29,7 +29,7 @@ describe('Raster tiles', function() {
});
});
describe('invalid requests return 4xx', function() {
describe('invalid requests return 4xx', function () {
testTile('non_existent', 0, 0, 0, 'png', 404);
testTile(prefix, -1, 0, 0, 'png', 404);
testTile(prefix, 25, 0, 0, 'png', 404);