diff --git a/publish.js b/publish.js index 30bdc3c..02d3915 100644 --- a/publish.js +++ b/publish.js @@ -12,18 +12,23 @@ // 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 = @@ -52,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', }); diff --git a/src/main.js b/src/main.js index dfc2a4b..c0f6b47 100644 --- a/src/main.js +++ b/src/main.js @@ -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 ', '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 ', 'Configuration file [config.json]', - 'config.json' - ) - .option( - '-b, --bind
', - 'Bind address' - ) - .option( - '-p, --port ', - 'Port [8080]', - 8080, - parseInt - ) - .option( - '-C|--no-cors', - 'Disable Cross-origin resource sharing headers' + 'config.json', ) + .option('-b, --bind
', 'Bind address') + .option('-p, --port ', 'Port [8080]', 8080, parseInt) + .option('-C|--no-cors', 'Disable Cross-origin resource sharing headers') .option( '-u|--public_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 ', - '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 ', 'output log file (defaults to standard out)') .option( '-f|--log_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,72 @@ 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 +190,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`); diff --git a/src/serve_data.js b/src/serve_data.js index 084c408..1c8a70b 100644 --- a/src/serve_data.js +++ b/src/serve_data.js @@ -10,30 +10,38 @@ 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) @@ -49,38 +57,43 @@ export const serve_data = { 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; } - 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); } @@ -106,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); }); @@ -162,7 +181,7 @@ export const serve_data = { repo[id] = { tileJSON, publicUrl, - source + source, }; }); }, diff --git a/src/serve_font.js b/src/serve_font.js index 5775142..1c63f01 100644 --- a/src/serve_font.js +++ b/src/serve_font.js @@ -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(), ); }); diff --git a/src/serve_light.js b/src/serve_light.js index 8505232..c748deb 100644 --- a/src/serve_light.js +++ b/src/serve_light.js @@ -1,10 +1,7 @@ '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) => {}, }; diff --git a/src/serve_rendered.js b/src/serve_rendered.js index eb9040c..6366663 100644 --- a/src/serve_rendered.js +++ b/src/serve_rendered.js @@ -16,12 +16,12 @@ import mlgl from '@maplibre/maplibre-gl-native'; import MBTiles from '@mapbox/mbtiles'; import proj4 from 'proj4'; 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 {createCanvas} = pkg; +const { createCanvas } = pkg; const mercator = new SphericalMercator(); const getScale = (scale) => (scale || '@1x').slice(1, 2) | 0; @@ -38,7 +38,7 @@ const extensionToFormat = { '.jpg': 'jpeg', '.jpeg': 'jpeg', '.png': 'png', - '.webp': 'webp' + '.webp': 'webp', }; /** @@ -46,18 +46,19 @@ const extensionToFormat = { * string is for unknown or unsupported formats. */ const cachedEmptyResponses = { - '': Buffer.alloc(0) + '': Buffer.alloc(0), }; /** * Create an appropriate mlgl response for http errors. + * * @param {string} format The format (a sharp format or 'pbf'). * @param {string} color The background color (or empty string for transparent). * @param {Function} callback The mlgl callback. */ function createEmptyResponse(format, color, callback) { if (!format || format === 'pbf') { - callback(null, {data: cachedEmptyResponses['']}); + callback(null, { data: cachedEmptyResponses[''] }); return; } @@ -71,7 +72,7 @@ function createEmptyResponse(format, color, callback) { const cacheKey = `${format},${color}`; const data = cachedEmptyResponses[cacheKey]; if (data) { - callback(null, {data: data}); + callback(null, { data: data }); return; } @@ -83,14 +84,16 @@ function createEmptyResponse(format, color, callback) { raw: { width: 1, height: 1, - channels: channels - } - }).toFormat(format).toBuffer((err, buffer, info) => { - if (!err) { - cachedEmptyResponses[cacheKey] = buffer; - } - callback(null, {data: buffer}); - }); + channels: channels, + }, + }) + .toFormat(format) + .toBuffer((err, buffer, info) => { + if (!err) { + cachedEmptyResponses[cacheKey] = buffer; + } + callback(null, { data: buffer }); + }); } const extractPathFromQuery = (query, transformer) => { @@ -101,9 +104,9 @@ const extractPathFromQuery = (query, transformer) => { if (pairParts.length === 2) { let pair; if (query.latlng === '1' || query.latlng === 'true') { - pair = [+(pairParts[1]), +(pairParts[0])]; + pair = [+pairParts[1], +pairParts[0]]; } else { - pair = [+(pairParts[0]), +(pairParts[1])]; + pair = [+pairParts[0], +pairParts[1]]; } if (transformer) { pair = transformer(pair); @@ -114,8 +117,7 @@ const extractPathFromQuery = (query, transformer) => { return path; }; -const renderOverlay = (z, x, y, bearing, pitch, w, h, scale, - path, query) => { +const renderOverlay = (z, x, y, bearing, pitch, w, h, scale, path, query) => { if (!path || path.length < 2) { return null; } @@ -131,7 +133,7 @@ const renderOverlay = (z, x, y, bearing, pitch, w, h, scale, const maxEdge = center[1] + h / 2; const minEdge = center[1] - h / 2; if (maxEdge > mapHeight) { - center[1] -= (maxEdge - mapHeight); + center[1] -= maxEdge - mapHeight; } else if (minEdge < 0) { center[1] -= minEdge; } @@ -141,14 +143,13 @@ const renderOverlay = (z, x, y, bearing, pitch, w, h, scale, ctx.scale(scale, scale); if (bearing) { ctx.translate(w / 2, h / 2); - ctx.rotate(-bearing / 180 * Math.PI); + ctx.rotate((-bearing / 180) * Math.PI); ctx.translate(-center[0], -center[1]); } else { // optimized path ctx.translate(-center[0] + w / 2, -center[1] + h / 2); } - const lineWidth = query.width !== undefined ? - parseFloat(query.width) : 1; + const lineWidth = query.width !== undefined ? parseFloat(query.width) : 1; ctx.lineWidth = lineWidth; ctx.strokeStyle = query.stroke || 'rgba(0,64,255,0.7)'; ctx.fillStyle = query.fill || 'rgba(255,255,255,0.4)'; @@ -157,8 +158,10 @@ const renderOverlay = (z, x, y, bearing, pitch, w, h, scale, const px = precisePx(pair, z); ctx.lineTo(px[0], px[1]); } - if (path[0][0] === path[path.length - 1][0] && - path[0][1] === path[path.length - 1][1]) { + if ( + path[0][0] === path[path.length - 1][0] && + path[0][1] === path[path.length - 1][1] + ) { ctx.closePath(); } ctx.fill(); @@ -172,18 +175,18 @@ const renderOverlay = (z, x, y, bearing, pitch, w, h, scale, const calcZForBBox = (bbox, w, h, query) => { let z = 25; - const padding = query.padding !== undefined ? - parseFloat(query.padding) : 0.1; + const padding = query.padding !== undefined ? parseFloat(query.padding) : 0.1; const minCorner = mercator.px([bbox[0], bbox[3]], z); const maxCorner = mercator.px([bbox[2], bbox[1]], z); const w_ = w / (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[1] - minCorner[1]) / h_) - ) / Math.LN2; + Math.log((maxCorner[1] - minCorner[1]) / h_), + ) / Math.LN2; z = Math.max(Math.log(Math.max(w, h) / 256) / Math.LN2, Math.min(25, z)); @@ -225,14 +228,36 @@ export const serve_rendered = { 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') => { - if (Math.abs(lon) > 180 || Math.abs(lat) > 85.06 || - lon !== lon || lat !== lat) { + const respondImage = ( + item, + 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'); } - if (Math.min(width, height) <= 0 || + if ( + Math.min(width, height) <= 0 || Math.max(width, height) * scale > (options.maxSize || 2048) || - width !== width || height !== height) { + width !== width || + height !== height + ) { return res.status(400).send('Invalid size'); } if (format === 'png' || format === 'webp') { @@ -256,7 +281,7 @@ export const serve_rendered = { bearing: bearing, pitch: pitch, width: width, - height: height + height: height, }; if (z === 0) { params.width *= 2; @@ -296,18 +321,21 @@ export const serve_rendered = { raw: { width: params.width * scale, height: params.height * scale, - channels: 4 - } + channels: 4, + }, }); if (z > 2 && tileMargin > 0) { 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({ left: tileMargin * scale, top: (tileMargin + yoffset) * scale, width: width * scale, - height: height * scale + height: height * scale, }); } @@ -317,7 +345,7 @@ export const serve_rendered = { } if (opt_overlay) { - image.composite([{input: opt_overlay}]); + image.composite([{ input: opt_overlay }]); } if (item.watermark) { const canvas = createCanvas(scale * width, scale * height); @@ -330,17 +358,17 @@ export const serve_rendered = { ctx.fillStyle = 'rgba(0,0,0,.4)'; ctx.fillText(item.watermark, 5, height - 5); - image.composite([{input: canvas.toBuffer()}]); + image.composite([{ input: canvas.toBuffer() }]); } const formatQuality = (options.formatQuality || {})[format]; if (format === 'png') { - image.png({adaptiveFiltering: false}); + image.png({ adaptiveFiltering: false }); } else if (format === 'jpeg') { - image.jpeg({quality: formatQuality || 80}); + image.jpeg({ quality: formatQuality || 80 }); } else if (format === 'webp') { - image.webp({quality: formatQuality || 90}); + image.webp({ quality: formatQuality || 90 }); } image.toBuffer((err, buffer, info) => { if (!buffer) { @@ -349,7 +377,7 @@ export const serve_rendered = { res.set({ 'Last-Modified': item.lastModified, - 'Content-Type': `image/${format}` + 'Content-Type': `image/${format}`, }); return res.status(200).send(buffer); }); @@ -357,44 +385,73 @@ export const serve_rendered = { }); }; - app.get(`/:id/:z(\\d+)/:x(\\d+)/:y(\\d+):scale(${scalePattern})?.:format([\\w]+)`, (req, res, next) => { - const item = repo[req.params.id]; - if (!item) { - return res.sendStatus(404); - } - - const modifiedSince = req.get('if-modified-since'); const cc = req.get('cache-control'); - if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) { - if (new Date(item.lastModified) <= new Date(modifiedSince)) { - return res.sendStatus(304); + app.get( + `/:id/:z(\\d+)/:x(\\d+)/:y(\\d+):scale(${scalePattern})?.:format([\\w]+)`, + (req, res, next) => { + const item = repo[req.params.id]; + if (!item) { + return res.sendStatus(404); } - } - const z = req.params.z | 0; - const x = req.params.x | 0; - const y = req.params.y | 0; - const scale = getScale(req.params.scale); - const format = req.params.format; - if (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'); - } - const tileSize = 256; - const tileCenter = mercator.ll([ - ((x + 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); - }); + const modifiedSince = req.get('if-modified-since'); + const cc = req.get('cache-control'); + if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) { + if (new Date(item.lastModified) <= new Date(modifiedSince)) { + return res.sendStatus(304); + } + } + + const z = req.params.z | 0; + const x = req.params.x | 0; + const y = req.params.y | 0; + const scale = getScale(req.params.scale); + const format = req.params.format; + if ( + 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'); + } + const tileSize = 256; + const tileCenter = mercator.ll( + [ + ((x + 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, + ); + }, + ); if (options.serveStaticMaps !== false) { - const staticPattern = - `/:id/static/:raw(raw)?/%s/:width(\\d+)x:height(\\d+):scale(${scalePattern})?.:format([\\w]+)`; + const staticPattern = `/:id/static/:raw(raw)?/%s/:width(\\d+)x:height(\\d+):scale(${scalePattern})?.:format([\\w]+)`; - const centerPattern = - util.format(':x(%s),:y(%s),:z(%s)(@:bearing(%s)(,:pitch(%s))?)?', - FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, - FLOAT_PATTERN, FLOAT_PATTERN); + const centerPattern = util.format( + ':x(%s),:y(%s),:z(%s)(@:bearing(%s)(,:pitch(%s))?)?', + FLOAT_PATTERN, + FLOAT_PATTERN, + FLOAT_PATTERN, + FLOAT_PATTERN, + FLOAT_PATTERN, + ); app.get(util.format(staticPattern, centerPattern), (req, res, next) => { const item = repo[req.params.id]; @@ -416,8 +473,9 @@ export const serve_rendered = { return res.status(404).send('Invalid zoom'); } - const transformer = raw ? - mercator.inverse.bind(mercator) : item.dataProjWGStoInternalWGS; + const transformer = raw + ? mercator.inverse.bind(mercator) + : item.dataProjWGStoInternalWGS; if (transformer) { const ll = transformer([x, y]); @@ -426,9 +484,35 @@ export const serve_rendered = { } const path = extractPathFromQuery(req.query, transformer); - const overlay = renderOverlay(z, x, y, bearing, pitch, w, h, scale, path, req.query); + const overlay = renderOverlay( + z, + x, + y, + bearing, + pitch, + w, + h, + scale, + path, + 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 = (req, res, next) => { @@ -437,11 +521,17 @@ export const serve_rendered = { return res.sendStatus(404); } 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]; - const transformer = raw ? - mercator.inverse.bind(mercator) : item.dataProjWGStoInternalWGS; + const transformer = raw + ? mercator.inverse.bind(mercator) + : item.dataProjWGStoInternalWGS; if (transformer) { const minCorner = transformer(bbox.slice(0, 2)); @@ -465,14 +555,44 @@ export const serve_rendered = { const pitch = 0; const path = extractPathFromQuery(req.query, transformer); - const overlay = renderOverlay(z, x, y, bearing, pitch, w, h, scale, path, req.query); + const overlay = renderOverlay( + z, + x, + y, + bearing, + pitch, + w, + h, + scale, + path, + 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 boundsPattern = - util.format(':minx(%s),:miny(%s),:maxx(%s),:maxy(%s)', - FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN); + const boundsPattern = util.format( + ':minx(%s),:miny(%s),:maxx(%s),:maxy(%s)', + FLOAT_PATTERN, + FLOAT_PATTERN, + FLOAT_PATTERN, + FLOAT_PATTERN, + ); app.get(util.format(staticPattern, boundsPattern), serveBounds); @@ -513,8 +633,9 @@ export const serve_rendered = { const scale = getScale(req.params.scale); const format = req.params.format; - const transformer = raw ? - mercator.inverse.bind(mercator) : item.dataProjWGStoInternalWGS; + const transformer = raw + ? mercator.inverse.bind(mercator) + : item.dataProjWGStoInternalWGS; const path = extractPathFromQuery(req.query, transformer); if (path.length < 2) { @@ -530,17 +651,44 @@ export const serve_rendered = { } const bbox_ = mercator.convert(bbox, '900913'); - const center = mercator.inverse( - [(bbox_[0] + bbox_[2]) / 2, (bbox_[1] + bbox_[3]) / 2] - ); + const center = mercator.inverse([ + (bbox_[0] + bbox_[2]) / 2, + (bbox_[1] + bbox_[3]) / 2, + ]); const z = calcZForBBox(bbox, w, h, req.query); const x = center[0]; const y = center[1]; - const overlay = renderOverlay(z, x, y, bearing, pitch, w, h, scale, path, req.query); + const overlay = renderOverlay( + z, + x, + y, + bearing, + pitch, + w, + h, + scale, + path, + 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', + ); }); } @@ -550,8 +698,13 @@ export const serve_rendered = { return res.sendStatus(404); } const info = clone(item.tileJSON); - info.tiles = getTileUrls(req, info.tiles, - `styles/${req.params.id}`, info.format, item.publicUrl); + info.tiles = getTileUrls( + req, + info.tiles, + `styles/${req.params.id}`, + info.format, + item.publicUrl, + ); return res.send(info); }); @@ -561,7 +714,7 @@ export const serve_rendered = { const map = { renderers: [], renderers_static: [], - sources: {} + sources: {}, }; let styleJSON; @@ -577,19 +730,26 @@ export const serve_rendered = { const dir = options.paths[protocol]; const file = unescape(req.url).substring(protocol.length + 3); fs.readFile(path.join(dir, file), (err, data) => { - callback(err, {data: data}); + callback(err, { data: data }); }); } else if (protocol === 'fonts') { const parts = req.url.split('/'); const fontstack = unescape(parts[2]); const range = parts[3].split('.')[0]; getFontsPbf( - null, options.paths[protocol], fontstack, range, existingFonts - ).then((concated) => { - callback(null, {data: concated}); - }, (err) => { - callback(err, {data: null}); - }); + null, + options.paths[protocol], + fontstack, + range, + existingFonts, + ).then( + (concated) => { + callback(null, { data: concated }); + }, + (err) => { + callback(err, { data: null }); + }, + ); } else if (protocol === 'mbtiles') { const parts = req.url.split('/'); const sourceId = parts[2]; @@ -601,8 +761,13 @@ export const serve_rendered = { const format = parts[5].split('.')[1]; source.getTile(z, x, y, (err, data, headers) => { if (err) { - if (options.verbose) console.log('MBTiles error, serving empty', err); - createEmptyResponse(sourceInfo.format, sourceInfo.color, callback); + if (options.verbose) + console.log('MBTiles error, serving empty', err); + createEmptyResponse( + sourceInfo.format, + sourceInfo.color, + callback, + ); return; } @@ -615,11 +780,23 @@ export const serve_rendered = { try { response.data = zlib.unzipSync(data); } 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) { response.data = options.dataDecoratorFunc( - sourceId, 'data', response.data, z, x, y); + sourceId, + 'data', + response.data, + z, + x, + y, + ); } } else { response.data = data; @@ -628,36 +805,39 @@ export const serve_rendered = { callback(null, response); }); } else if (protocol === 'http' || protocol === 'https') { - request({ - url: req.url, - encoding: null, - gzip: true - }, (err, res, body) => { - const parts = url.parse(req.url); - const extension = path.extname(parts.pathname).toLowerCase(); - const format = extensionToFormat[extension] || ''; - if (err || res.statusCode < 200 || res.statusCode >= 300) { - // console.log('HTTP error', err || res.statusCode); - createEmptyResponse(format, '', callback); - return; - } + request( + { + url: req.url, + encoding: null, + gzip: true, + }, + (err, res, body) => { + const parts = url.parse(req.url); + const extension = path.extname(parts.pathname).toLowerCase(); + const format = extensionToFormat[extension] || ''; + if (err || res.statusCode < 200 || res.statusCode >= 300) { + // console.log('HTTP error', err || res.statusCode); + createEmptyResponse(format, '', callback); + return; + } - const response = {}; - if (res.headers.modified) { - response.modified = new Date(res.headers.modified); - } - if (res.headers.expires) { - response.expires = new Date(res.headers.expires); - } - if (res.headers.etag) { - response.etag = res.headers.etag; - } + const response = {}; + if (res.headers.modified) { + response.modified = new Date(res.headers.modified); + } + if (res.headers.expires) { + response.expires = new Date(res.headers.expires); + } + if (res.headers.etag) { + response.etag = res.headers.etag; + } - response.data = body; - callback(null, response); - }); + response.data = body; + callback(null, response); + }, + ); } - } + }, }); renderer.load(styleJSON); createCallback(null, renderer); @@ -668,7 +848,7 @@ export const serve_rendered = { create: createRenderer.bind(null, ratio), destroy: (renderer) => { renderer.release(); - } + }, }); }; @@ -682,16 +862,20 @@ export const serve_rendered = { } if (styleJSON.sprite && !httpTester.test(styleJSON.sprite)) { - styleJSON.sprite = 'sprites://' + + styleJSON.sprite = + 'sprites://' + styleJSON.sprite - .replace('{style}', path.basename(styleFile, '.json')) - .replace('{styleJsonFolder}', path.relative(options.paths.sprites, path.dirname(styleJSONPath))); + .replace('{style}', path.basename(styleFile, '.json')) + .replace( + '{styleJsonFolder}', + path.relative(options.paths.sprites, path.dirname(styleJSONPath)), + ); } if (styleJSON.glyphs && !httpTester.test(styleJSON.glyphs)) { styleJSON.glyphs = `fonts://${styleJSON.glyphs}`; } - for (const layer of (styleJSON.layers || [])) { + for (const layer of styleJSON.layers || []) { if (layer && layer.paint) { // Remove (flatten) 3D buildings if (layer.paint['fill-extrusion-height']) { @@ -704,14 +888,14 @@ export const serve_rendered = { } const tileJSON = { - 'tilejson': '2.0.0', - 'name': styleJSON.name, - 'attribution': '', - 'minzoom': 0, - 'maxzoom': 20, - 'bounds': [-180, -85.0511, 180, 85.0511], - 'format': 'png', - 'type': 'baselayer' + tilejson: '2.0.0', + name: styleJSON.name, + attribution: '', + minzoom: 0, + maxzoom: 20, + bounds: [-180, -85.0511, 180, 85.0511], + format: 'png', + type: 'baselayer', }; const attributionOverride = params.tilejson && params.tilejson.attribution; Object.assign(tileJSON, params.tilejson || {}); @@ -724,7 +908,7 @@ export const serve_rendered = { map, dataProjWGStoInternalWGS: null, lastModified: new Date().toUTCString(), - watermark: params.watermark || options.watermark + watermark: params.watermark || options.watermark, }; repo[id] = repoobj; @@ -738,8 +922,8 @@ export const serve_rendered = { delete source.url; 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); @@ -754,52 +938,58 @@ export const serve_rendered = { } } - queue.push(new Promise((resolve, reject) => { - mbtilesFile = path.resolve(options.paths.mbtiles, mbtilesFile); - const mbtilesFileStats = fs.statSync(mbtilesFile); - if (!mbtilesFileStats.isFile() || mbtilesFileStats.size === 0) { - throw Error(`Not valid MBTiles file: ${mbtilesFile}`); - } - map.sources[name] = new MBTiles(mbtilesFile + '?mode=ro', err => { - map.sources[name].getInfo((err, info) => { - if (err) { - console.error(err); - return; - } - - if (!repoobj.dataProjWGStoInternalWGS && info.proj4) { - // how to do this for multiple sources with different proj4 defs? - const to3857 = proj4('EPSG:3857'); - const toDataProj = proj4(info.proj4); - repoobj.dataProjWGStoInternalWGS = (xy) => to3857.inverse(toDataProj.forward(xy)); - } - - const type = source.type; - Object.assign(source, info); - source.type = type; - source.tiles = [ - // meta url which will be detected when requested - `mbtiles://${name}/{z}/{x}/{y}.${info.format || 'pbf'}` - ]; - delete source.scheme; - - if (options.dataDecoratorFunc) { - source = options.dataDecoratorFunc(name, 'tilejson', source); - } - - if (!attributionOverride && - source.attribution && source.attribution.length > 0) { - if (!tileJSON.attribution.includes(source.attribution)) { - if (tileJSON.attribution.length > 0) { - tileJSON.attribution += ' | '; - } - tileJSON.attribution += source.attribution; + queue.push( + new Promise((resolve, reject) => { + mbtilesFile = path.resolve(options.paths.mbtiles, mbtilesFile); + const mbtilesFileStats = fs.statSync(mbtilesFile); + if (!mbtilesFileStats.isFile() || mbtilesFileStats.size === 0) { + throw Error(`Not valid MBTiles file: ${mbtilesFile}`); + } + map.sources[name] = new MBTiles(mbtilesFile + '?mode=ro', (err) => { + map.sources[name].getInfo((err, info) => { + if (err) { + console.error(err); + return; } - } - resolve(); + + if (!repoobj.dataProjWGStoInternalWGS && info.proj4) { + // how to do this for multiple sources with different proj4 defs? + const to3857 = proj4('EPSG:3857'); + const toDataProj = proj4(info.proj4); + repoobj.dataProjWGStoInternalWGS = (xy) => + to3857.inverse(toDataProj.forward(xy)); + } + + const type = source.type; + Object.assign(source, info); + source.type = type; + source.tiles = [ + // meta url which will be detected when requested + `mbtiles://${name}/{z}/{x}/{y}.${info.format || 'pbf'}`, + ]; + delete source.scheme; + + if (options.dataDecoratorFunc) { + source = options.dataDecoratorFunc(name, 'tilejson', source); + } + + if ( + !attributionOverride && + source.attribution && + source.attribution.length > 0 + ) { + if (!tileJSON.attribution.includes(source.attribution)) { + if (tileJSON.attribution.length > 0) { + tileJSON.attribution += ' | '; + } + tileJSON.attribution += source.attribution; + } + } + resolve(); + }); }); - }); - })); + }), + ); } } @@ -813,7 +1003,12 @@ export const serve_rendered = { const minPoolSize = minPoolSizes[i]; const maxPoolSize = Math.max(minPoolSize, maxPoolSizes[j]); 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, + ); } }); @@ -830,5 +1025,5 @@ export const serve_rendered = { }); } delete repo[id]; - } -}; \ No newline at end of file + }, +}; diff --git a/src/serve_style.js b/src/serve_style.js index 4e6013e..3620018 100644 --- a/src/serve_style.js +++ b/src/serve_style.js @@ -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; - } -}; \ No newline at end of file + }, +}; diff --git a/src/server.js b/src/server.js index 38d3f90..e56e934 100644 --- a/src/server.js +++ b/src/server.js @@ -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 + */ export function server(opts) { console.log('Starting server'); @@ -38,18 +45,24 @@ export function server(opts) { styles: {}, rendered: {}, data: {}, - fonts: {} + fonts: {}, }; app.enable('trust proxy'); if (process.env.NODE_ENV !== 'test') { - const defaultLogFormat = process.env.NODE_ENV === 'production' ? 'tiny' : 'dev'; + const defaultLogFormat = + process.env.NODE_ENV === 'production' ? 'tiny' : 'dev'; const logFormat = opts.logFormat || defaultLogFormat; - app.use(morgan(logFormat, { - stream: opts.logFile ? fs.createWriteStream(opts.logFile, {flags: 'a'}) : process.stdout, - skip: (req, res) => opts.silent && (res.statusCode === 200 || res.statusCode === 304) - })); + app.use( + morgan(logFormat, { + stream: opts.logFile + ? fs.createWriteStream(opts.logFile, { flags: 'a' }) + : process.stdout, + skip: (req, res) => + opts.silent && (res.statusCode === 200 || res.statusCode === 304), + }), + ); } let config = opts.config || null; @@ -74,7 +87,8 @@ export function server(opts) { options.paths = paths; paths.root = path.resolve( configPath ? path.dirname(configPath) : process.cwd(), - paths.root || ''); + paths.root || '', + ); paths.styles = path.resolve(paths.root, paths.styles || ''); paths.fonts = path.resolve(paths.root, paths.fonts || ''); paths.sprites = path.resolve(paths.root, paths.sprites || ''); @@ -84,7 +98,9 @@ export function server(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); } }; @@ -95,7 +111,10 @@ export function server(opts) { if (options.dataDecorator) { try { - options.dataDecoratorFunc = require(path.resolve(paths.root, options.dataDecorator)); + options.dataDecoratorFunc = require(path.resolve( + paths.root, + options.dataDecorator, + )); } catch (e) {} } @@ -109,54 +128,69 @@ export function server(opts) { app.use('/styles/', serve_style.init(options, serving.styles)); if (!isLight) { startupPromises.push( - serve_rendered.init(options, serving.rendered) - .then((sub) => { - app.use('/styles/', sub); - }) + serve_rendered.init(options, serving.rendered).then((sub) => { + app.use('/styles/', sub); + }), ); } const addStyle = (id, item, allowMoreData, reportFonts) => { let success = true; if (item.serve_data !== false) { - success = serve_style.add(options, serving.styles, item, id, opts.publicUrl, - (mbtiles, fromData) => { - let dataItemId; - for (const id of Object.keys(data)) { - if (fromData) { - if (id === mbtiles) { - dataItemId = id; - } - } else { - if (data[id].mbtiles === mbtiles) { - dataItemId = id; - } + success = serve_style.add( + options, + serving.styles, + item, + id, + opts.publicUrl, + (mbtiles, fromData) => { + let dataItemId; + for (const id of Object.keys(data)) { + if (fromData) { + if (id === mbtiles) { + dataItemId = id; } - } - if (dataItemId) { // mbtiles exist in the data config - return dataItemId; } else { - if (fromData || !allowMoreData) { - console.log(`ERROR: style "${item.style}" using unknown mbtiles "${mbtiles}"! Skipping...`); - return undefined; - } else { - let id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles; - while (data[id]) id += '_'; - data[id] = { - 'mbtiles': mbtiles - }; - return id; + if (data[id].mbtiles === mbtiles) { + dataItemId = id; } } - }, (font) => { - if (reportFonts) { - serving.fonts[font] = true; + } + if (dataItemId) { + // mbtiles exist in the data config + return dataItemId; + } else { + if (fromData || !allowMoreData) { + console.log( + `ERROR: style "${item.style}" using unknown mbtiles "${mbtiles}"! Skipping...`, + ); + return undefined; + } else { + let id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles; + while (data[id]) id += '_'; + data[id] = { + mbtiles: mbtiles, + }; + return id; } - }); + } + }, + (font) => { + if (reportFonts) { + serving.fonts[font] = true; + } + }, + ); } if (success && item.serve_rendered !== false) { if (!isLight) { - startupPromises.push(serve_rendered.add(options, serving.rendered, item, id, opts.publicUrl, + startupPromises.push( + serve_rendered.add( + options, + serving.rendered, + item, + id, + opts.publicUrl, (mbtiles) => { let mbtilesFile; for (const id of Object.keys(data)) { @@ -165,8 +199,9 @@ export function server(opts) { } } return mbtilesFile; - } - )); + }, + ), + ); } else { item.serve_rendered = false; } @@ -184,9 +219,9 @@ export function server(opts) { } startupPromises.push( - serve_font(options, serving.fonts).then((sub) => { - app.use('/', sub); - }) + serve_font(options, serving.fonts).then((sub) => { + app.use('/', sub); + }), ); for (const id of Object.keys(data)) { @@ -197,61 +232,65 @@ export function server(opts) { } startupPromises.push( - serve_data.add(options, serving.data, item, id, opts.publicUrl) + serve_data.add(options, serving.data, item, id, opts.publicUrl), ); } if (options.serveAllStyles) { - fs.readdir(options.paths.styles, {withFileTypes: true}, (err, files) => { + fs.readdir(options.paths.styles, { withFileTypes: true }, (err, files) => { if (err) { return; } for (const file of files) { - if (file.isFile() && - path.extname(file.name).toLowerCase() == '.json') { + if (file.isFile() && path.extname(file.name).toLowerCase() == '.json') { const id = path.basename(file.name, '.json'); const item = { - style: file.name + style: file.name, }; addStyle(id, item, false, false); } } }); - const watcher = chokidar.watch(path.join(options.paths.styles, '*.json'), - { - }); - watcher.on('all', - (eventType, filename) => { - if (filename) { - const id = path.basename(filename, '.json'); - console.log(`Style "${id}" changed, updating...`); + const watcher = chokidar.watch( + path.join(options.paths.styles, '*.json'), + {}, + ); + watcher.on('all', (eventType, filename) => { + if (filename) { + const id = path.basename(filename, '.json'); + console.log(`Style "${id}" changed, updating...`); - serve_style.remove(serving.styles, id); - if (!isLight) { - serve_rendered.remove(serving.rendered, id); - } + serve_style.remove(serving.styles, id); + if (!isLight) { + serve_rendered.remove(serving.rendered, id); + } - if (eventType == 'add' || eventType == 'change') { - const item = { - style: filename - }; - addStyle(id, item, false, false); - } - } - }); + if (eventType == 'add' || eventType == 'change') { + const item = { + style: filename, + }; + addStyle(id, item, false, false); + } + } + }); } app.get('/styles.json', (req, res, next) => { const result = []; - const query = req.query.key ? (`?key=${encodeURIComponent(req.query.key)}`) : ''; + const query = req.query.key + ? `?key=${encodeURIComponent(req.query.key)}` + : ''; for (const id of Object.keys(serving.styles)) { const styleJSON = serving.styles[id].styleJSON; result.push({ version: styleJSON.version, name: styleJSON.name, id: id, - url: `${getPublicUrl(opts.publicUrl, req)}styles/${id}/style.json${query}` + url: `${getPublicUrl( + opts.publicUrl, + req, + )}styles/${id}/style.json${query}`, }); } res.send(result); @@ -266,9 +305,16 @@ export function server(opts) { } else { path = `${type}/${id}`; } - info.tiles = getTileUrls(req, info.tiles, path, info.format, opts.publicUrl, { - 'pbf': options.pbfAlias - }); + info.tiles = getTileUrls( + req, + info.tiles, + path, + info.format, + opts.publicUrl, + { + pbf: options.pbfAlias, + }, + ); arr.push(info); } return arr; @@ -294,40 +340,49 @@ export function server(opts) { if (template === 'index') { if (options.frontPage === false) { return; - } else if (options.frontPage && - options.frontPage.constructor === String) { + } else if ( + options.frontPage && + options.frontPage.constructor === String + ) { templateFile = path.resolve(paths.root, options.frontPage); } } - startupPromises.push(new Promise((resolve, reject) => { - fs.readFile(templateFile, (err, content) => { - if (err) { - err = new Error(`Template not found: ${err.message}`); - reject(err); - return; - } - const compiled = handlebars.compile(content.toString()); - - app.use(urlPath, (req, res, next) => { - let data = {}; - if (dataGetter) { - data = dataGetter(req); - if (!data) { - return res.status(404).send('Not found'); - } + startupPromises.push( + new Promise((resolve, reject) => { + fs.readFile(templateFile, (err, content) => { + if (err) { + err = new Error(`Template not found: ${err.message}`); + reject(err); + return; } - data['server_version'] = `${packageJson.name} v${packageJson.version}`; - data['public_url'] = opts.publicUrl || '/'; - data['is_light'] = isLight; - data['key_query_part'] = - req.query.key ? `key=${encodeURIComponent(req.query.key)}&` : ''; - 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)}&` + : ''; + data['key_query'] = req.query.key + ? `?key=${encodeURIComponent(req.query.key)}` + : ''; + if (template === 'wmts') res.set('Content-Type', 'text/xml'); + return res.status(200).send(compiled(data)); + }); + resolve(); }); - resolve(); - }); - })); + }), + ); }; serveTemplate('/$', 'index', (req) => { @@ -340,15 +395,23 @@ export function server(opts) { if (style.serving_rendered) { const center = style.serving_rendered.tileJSON.center; if (center) { - style.viewer_hash = `#${center[2]}/${center[1].toFixed(5)}/${center[0].toFixed(5)}`; + style.viewer_hash = `#${center[2]}/${center[1].toFixed( + 5, + )}/${center[0].toFixed(5)}`; const centerPx = mercator.px([center[0], center[1]], center[2]); - style.thumbnail = `${center[2]}/${Math.floor(centerPx[0] / 256)}/${Math.floor(centerPx[1] / 256)}.png`; + style.thumbnail = `${center[2]}/${Math.floor( + centerPx[0] / 256, + )}/${Math.floor(centerPx[1] / 256)}.png`; } style.xyz_link = getTileUrls( - req, style.serving_rendered.tileJSON.tiles, - `styles/${id}`, style.serving_rendered.tileJSON.format, opts.publicUrl)[0]; + req, + style.serving_rendered.tileJSON.tiles, + `styles/${id}`, + style.serving_rendered.tileJSON.format, + opts.publicUrl, + )[0]; } } const data = clone(serving.data || {}); @@ -357,19 +420,29 @@ export function server(opts) { const tilejson = data[id].tileJSON; const center = tilejson.center; if (center) { - data_.viewer_hash = `#${center[2]}/${center[1].toFixed(5)}/${center[0].toFixed(5)}`; + data_.viewer_hash = `#${center[2]}/${center[1].toFixed( + 5, + )}/${center[0].toFixed(5)}`; } data_.is_vector = tilejson.format === 'pbf'; if (!data_.is_vector) { if (center) { const centerPx = mercator.px([center[0], center[1]], center[2]); - data_.thumbnail = `${center[2]}/${Math.floor(centerPx[0] / 256)}/${Math.floor(centerPx[1] / 256)}.${data_.tileJSON.format}`; + data_.thumbnail = `${center[2]}/${Math.floor( + centerPx[0] / 256, + )}/${Math.floor(centerPx[1] / 256)}.${data_.tileJSON.format}`; } data_.xyz_link = getTileUrls( - req, tilejson.tiles, `data/${id}`, tilejson.format, opts.publicUrl, { - 'pbf': options.pbfAlias - })[0]; + req, + tilejson.tiles, + `data/${id}`, + tilejson.format, + opts.publicUrl, + { + pbf: options.pbfAlias, + }, + )[0]; } if (data_.filesize) { let suffix = 'kB'; @@ -387,7 +460,7 @@ export function server(opts) { } return { styles: Object.keys(styles).length ? styles : null, - data: Object.keys(data).length ? data : null + data: Object.keys(data).length ? data : null, }; }); @@ -422,9 +495,12 @@ export function server(opts) { wmts.name = (serving.styles[id] || serving.rendered[id]).name; if (opts.publicUrl) { wmts.baseUrl = opts.publicUrl; - } - else { - wmts.baseUrl = `${req.get('X-Forwarded-Protocol') ? req.get('X-Forwarded-Protocol') : req.protocol}://${req.get('host')}/`; + } else { + wmts.baseUrl = `${ + req.get('X-Forwarded-Protocol') + ? req.get('X-Forwarded-Protocol') + : req.protocol + }://${req.get('host')}/`; } return wmts; }); @@ -453,13 +529,17 @@ export function server(opts) { } }); - const server = app.listen(process.env.PORT || opts.port, process.env.BIND || opts.bind, function() { - let address = this.address().address; - if (address.indexOf('::') === 0) { - address = `[${address}]`; // literal IPv6 address - } - console.log(`Listening at http://${address}:${this.address().port}/`); - }); + const server = app.listen( + process.env.PORT || opts.port, + process.env.BIND || opts.bind, + function () { + let address = this.address().address; + if (address.indexOf('::') === 0) { + address = `[${address}]`; // literal IPv6 address + } + console.log(`Listening at http://${address}:${this.address().port}/`); + }, + ); // add server.shutdown() to gracefully stop serving enableShutdown(server); @@ -467,7 +547,7 @@ export function server(opts) { return { app: app, server: server, - startupPromise: startupPromise + startupPromise: startupPromise, }; } @@ -498,4 +578,4 @@ export const exports = (opts) => { }); return running; -}; \ No newline at end of file +}; diff --git a/src/utils.js b/src/utils.js index fc6a6d4..4a46eca 100644 --- a/src/utils.js +++ b/src/utils.js @@ -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), + ), ); } diff --git a/test/metadata.js b/test/metadata.js index da46cb6..d68cc8c 100644 --- a/test/metadata.js +++ b/test/metadata.js @@ -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); }); }); diff --git a/test/setup.js b/test/setup.js index a97cc85..bae30ea 100644 --- a/test/setup.js +++ b/test/setup.js @@ -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(); }); }); diff --git a/test/static.js b/test/static.js index 9499034..15ea0e9 100644 --- a/test/static.js +++ b/test/static.js @@ -1,10 +1,10 @@ -const testStatic = function(prefix, q, format, status, scale, type, query) { +const testStatic = function (prefix, q, format, status, scale, type, query) { if (scale) q += '@' + scale + 'x'; let path = '/styles/' + prefix + '/static/' + q + '.' + format; if (query) { path += query; } - it(path + ' returns ' + status, function(done) { + it(path + ' returns ' + status, function (done) { const test = supertest(app).get(path); if (status) test.expect(status); if (type) test.expect('Content-Type', type); @@ -14,17 +14,45 @@ const testStatic = function(prefix, q, format, status, scale, type, query) { const prefix = 'test-style'; -describe('Static endpoints', function() { - describe('center-based', function() { - describe('valid requests', function() { - describe('various formats', function() { - testStatic(prefix, '0,0,0/256x256', 'png', 200, undefined, /image\/png/); - testStatic(prefix, '0,0,0/256x256', 'jpg', 200, undefined, /image\/jpeg/); - testStatic(prefix, '0,0,0/256x256', 'jpeg', 200, undefined, /image\/jpeg/); - testStatic(prefix, '0,0,0/256x256', 'webp', 200, undefined, /image\/webp/); +describe('Static endpoints', function () { + describe('center-based', function () { + describe('valid requests', function () { + describe('various formats', function () { + testStatic( + prefix, + '0,0,0/256x256', + 'png', + 200, + undefined, + /image\/png/, + ); + testStatic( + prefix, + '0,0,0/256x256', + 'jpg', + 200, + undefined, + /image\/jpeg/, + ); + testStatic( + prefix, + '0,0,0/256x256', + 'jpeg', + 200, + undefined, + /image\/jpeg/, + ); + testStatic( + prefix, + '0,0,0/256x256', + 'webp', + 200, + undefined, + /image\/webp/, + ); }); - describe('different parameters', function() { + describe('different parameters', function () { testStatic(prefix, '0,0,0/300x300', 'png', 200, 2); testStatic(prefix, '0,0,0/300x300', 'png', 200, 3); @@ -42,7 +70,7 @@ describe('Static endpoints', function() { }); }); - describe('invalid requests return 4xx', function() { + describe('invalid requests return 4xx', function () { testStatic(prefix, '190,0,0/256x256', 'png', 400); testStatic(prefix, '0,86,0/256x256', 'png', 400); testStatic(prefix, '80,40,20/0x0', 'png', 400); @@ -57,16 +85,44 @@ describe('Static endpoints', function() { }); }); - describe('area-based', function() { - describe('valid requests', function() { - describe('various formats', function() { - testStatic(prefix, '-180,-80,180,80/10x10', 'png', 200, undefined, /image\/png/); - testStatic(prefix, '-180,-80,180,80/10x10', 'jpg', 200, undefined, /image\/jpeg/); - testStatic(prefix, '-180,-80,180,80/10x10', 'jpeg', 200, undefined, /image\/jpeg/); - testStatic(prefix, '-180,-80,180,80/10x10', 'webp', 200, undefined, /image\/webp/); + describe('area-based', function () { + describe('valid requests', function () { + describe('various formats', function () { + testStatic( + prefix, + '-180,-80,180,80/10x10', + 'png', + 200, + undefined, + /image\/png/, + ); + testStatic( + prefix, + '-180,-80,180,80/10x10', + 'jpg', + 200, + undefined, + /image\/jpeg/, + ); + testStatic( + prefix, + '-180,-80,180,80/10x10', + 'jpeg', + 200, + undefined, + /image\/jpeg/, + ); + testStatic( + prefix, + '-180,-80,180,80/10x10', + 'webp', + 200, + undefined, + /image\/webp/, + ); }); - describe('different parameters', function() { + describe('different parameters', function () { testStatic(prefix, '-180,-90,180,90/20x20', 'png', 200, 2); testStatic(prefix, '0,0,1,1/200x200', 'png', 200, 3); @@ -74,7 +130,7 @@ describe('Static endpoints', function() { }); }); - describe('invalid requests return 4xx', function() { + describe('invalid requests return 4xx', function () { testStatic(prefix, '0,87,1,88/5x2', 'png', 400); testStatic(prefix, '0,0,1,1/1x1', 'gif', 400); @@ -83,20 +139,60 @@ describe('Static endpoints', function() { }); }); - describe('autofit path', function() { - describe('valid requests', function() { - testStatic(prefix, 'auto/256x256', 'png', 200, undefined, /image\/png/, '?path=10,10|20,20'); + describe('autofit path', function () { + describe('valid requests', function () { + testStatic( + prefix, + 'auto/256x256', + 'png', + 200, + undefined, + /image\/png/, + '?path=10,10|20,20', + ); - describe('different parameters', function() { - testStatic(prefix, 'auto/20x20', 'png', 200, 2, /image\/png/, '?path=10,10|20,20'); - testStatic(prefix, 'auto/200x200', 'png', 200, 3, /image\/png/, '?path=-10,-10|-20,-20'); + describe('different parameters', function () { + testStatic( + prefix, + 'auto/20x20', + 'png', + 200, + 2, + /image\/png/, + '?path=10,10|20,20', + ); + testStatic( + prefix, + 'auto/200x200', + 'png', + 200, + 3, + /image\/png/, + '?path=-10,-10|-20,-20', + ); }); }); - describe('invalid requests return 4xx', function() { + describe('invalid requests return 4xx', function () { testStatic(prefix, 'auto/256x256', 'png', 400); - testStatic(prefix, 'auto/256x256', 'png', 400, undefined, undefined, '?path=10,10'); - testStatic(prefix, 'auto/2560x2560', 'png', 400, undefined, undefined, '?path=10,10|20,20'); + testStatic( + prefix, + 'auto/256x256', + 'png', + 400, + undefined, + undefined, + '?path=10,10', + ); + testStatic( + prefix, + 'auto/2560x2560', + 'png', + 400, + undefined, + undefined, + '?path=10,10|20,20', + ); }); }); }); diff --git a/test/style.js b/test/style.js index f7ab30a..46607ec 100644 --- a/test/style.js +++ b/test/style.js @@ -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); diff --git a/test/tiles_data.js b/test/tiles_data.js index 310a1e8..55f8771 100644 --- a/test/tiles_data.js +++ b/test/tiles_data.js @@ -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 diff --git a/test/tiles_rendered.js b/test/tiles_rendered.js index 73e8c16..fb2fcc5 100644 --- a/test/tiles_rendered.js +++ b/test/tiles_rendered.js @@ -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);