This commit is contained in:
acalcutt 2025-01-03 01:53:32 -05:00
parent 468779b1dd
commit 8bdd14a5e9
6 changed files with 192 additions and 73 deletions

View file

@ -46,7 +46,9 @@ export const serve_data = {
*/ */
app.get('/:id/:z/:x/:y.:format', async (req, res) => { app.get('/:id/:z/:x/:y.:format', async (req, res) => {
if (verbose) { if (verbose) {
console.log(req.params); console.log(
`Handling tile request for: /data/${req.params.id}/${req.params.z}/${req.params.x}/${req.params.y}.${req.params.format}`,
);
} }
const item = repo[req.params.id]; const item = repo[req.params.id];
if (!item) { if (!item) {
@ -156,6 +158,11 @@ export const serve_data = {
*/ */
app.get('/:id/elevation/:z/:x/:y', async (req, res, next) => { app.get('/:id/elevation/:z/:x/:y', async (req, res, next) => {
try { try {
if (verbose) {
console.log(
`Handling elevation request for: /data/${req.params.id}/elevation/${req.params.z}/${req.params.x}/${req.params.y}`,
);
}
const item = repo?.[req.params.id]; const item = repo?.[req.params.id];
if (!item) return res.sendStatus(404); if (!item) return res.sendStatus(404);
if (!item.source) return res.status(404).send('Missing source'); if (!item.source) return res.status(404).send('Missing source');
@ -292,13 +299,18 @@ export const serve_data = {
}); });
/** /**
* Handles requests for metadata for the tiles. * Handles requests for tilejson for the data tiles.
* @param {object} req - Express request object. * @param {object} req - Express request object.
* @param {object} res - Express response object. * @param {object} res - Express response object.
* @param {string} req.params.id - ID of the data source. * @param {string} req.params.id - ID of the data source.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
app.get('/:id.json', (req, res) => { app.get('/:id.json', (req, res) => {
if (verbose) {
console.log(
`Handling tilejson request for: /data/${req.params.id}.json`,
);
}
const item = repo[req.params.id]; const item = repo[req.params.id];
if (!item) { if (!item) {
return res.sendStatus(404); return res.sendStatus(404);

View file

@ -23,7 +23,6 @@ export async function serve_font(options, allowedFonts, programOpts) {
/** /**
* Handles requests for a font file. * Handles requests for a font file.
*
* @param {object} req - Express request object. * @param {object} req - Express request object.
* @param {object} res - Express response object. * @param {object} res - Express response object.
* @param {string} req.params.fontstack - Name of the font stack. * @param {string} req.params.fontstack - Name of the font stack.
@ -32,7 +31,9 @@ export async function serve_font(options, allowedFonts, programOpts) {
*/ */
app.get('/fonts/:fontstack/:range.pbf', async (req, res) => { app.get('/fonts/:fontstack/:range.pbf', async (req, res) => {
if (verbose) { if (verbose) {
console.log(req.params); console.log(
`Handling font request for: /fonts/${req.params.fontstack}/${req.params.range}.pbf`,
);
} }
const fontstack = decodeURI(req.params.fontstack); const fontstack = decodeURI(req.params.fontstack);
const range = req.params.range; const range = req.params.range;
@ -60,12 +61,14 @@ export async function serve_font(options, allowedFonts, programOpts) {
/** /**
* Handles requests for a list of all available fonts. * Handles requests for a list of all available fonts.
*
* @param {object} req - Express request object. * @param {object} req - Express request object.
* @param {object} res - Express response object. * @param {object} res - Express response object.
* @returns {void} * @returns {void}
*/ */
app.get('/fonts.json', (req, res) => { app.get('/fonts.json', (req, res) => {
if (verbose) {
console.log('Handling list font request for /fonts.json');
}
res.header('Content-type', 'application/json'); res.header('Content-type', 'application/json');
return res.send( return res.send(
Object.keys(options.serveAllFonts ? existingFonts : allowedFonts).sort(), Object.keys(options.serveAllFonts ? existingFonts : allowedFonts).sort(),

View file

@ -34,6 +34,7 @@ import {
isValidHttpUrl, isValidHttpUrl,
fixTileJSONCenter, fixTileJSONCenter,
fetchTileData, fetchTileData,
allowedOptions,
} from './utils.js'; } from './utils.js';
import { openPMtiles, getPMtilesInfo } from './pmtiles_adapter.js'; import { openPMtiles, getPMtilesInfo } from './pmtiles_adapter.js';
import { renderOverlay, renderWatermark, renderAttribution } from './render.js'; import { renderOverlay, renderWatermark, renderAttribution } from './render.js';
@ -635,6 +636,7 @@ const respondImage = async (
* @param {object} res - Express response object. * @param {object} res - Express response object.
* @param {Function} next - Express next middleware function. * @param {Function} next - Express next middleware function.
* @param {number} maxScaleFactor - The maximum scale factor allowed. * @param {number} maxScaleFactor - The maximum scale factor allowed.
* @param defailtTileSize
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function handleTileRequest( async function handleTileRequest(
@ -644,6 +646,7 @@ async function handleTileRequest(
res, res,
next, next,
maxScaleFactor, maxScaleFactor,
defailtTileSize,
) { ) {
const { const {
id, id,
@ -658,6 +661,7 @@ async function handleTileRequest(
if (!item) { if (!item) {
return res.sendStatus(404); return res.sendStatus(404);
} }
console.log(req.params);
const modifiedSince = req.get('if-modified-since'); const modifiedSince = req.get('if-modified-since');
const cc = req.get('cache-control'); const cc = req.get('cache-control');
@ -670,7 +674,19 @@ async function handleTileRequest(
const x = parseFloat(xParam) | 0; const x = parseFloat(xParam) | 0;
const y = parseFloat(yParam) | 0; const y = parseFloat(yParam) | 0;
const scale = parseScale(scaleParam, maxScaleFactor); const scale = parseScale(scaleParam, maxScaleFactor);
const parsedTileSize = parseInt(tileSize, 10) || 256;
let parsedTileSize = defailtTileSize;
if (tileSize) {
const allowedTileSizes = allowedOptions(['256', '512'], {
defaultValue: null,
});
parsedTileSize = allowedTileSizes(tileSize);
if (parsedTileSize == null) {
return res.status(400).send('Invalid Tile Size');
}
}
if ( if (
scale == null || scale == null ||
z < 0 || z < 0 ||
@ -680,7 +696,7 @@ async function handleTileRequest(
x >= Math.pow(2, z) || x >= Math.pow(2, z) ||
y >= Math.pow(2, z) y >= Math.pow(2, z)
) { ) {
return res.status(404).send('Out of bounds'); return res.status(400).send('Out of bounds');
} }
const tileCenter = mercator.ll( const tileCenter = mercator.ll(
@ -722,14 +738,43 @@ async function handleStaticRequest(
id, id,
p2: raw, p2: raw,
p3: staticType, p3: staticType,
p4: width, p4: widthAndHeight,
p5: height,
scale: scaleParam, scale: scaleParam,
format, format,
} = req.params; } = req.params;
console.log(req.params);
const item = repo[id]; const item = repo[id];
const parsedWidth = parseInt(width) || 512;
const parsedHeight = parseInt(height) || 512; let parsedWidth = null;
let parsedHeight = null;
if (widthAndHeight) {
const sizeMatch = widthAndHeight.match(/^(\d+)x(\d+)$/);
if (sizeMatch) {
const width = parseInt(sizeMatch[1], 10);
const height = parseInt(sizeMatch[2], 10);
if (
isNaN(width) ||
isNaN(height) ||
width !== parseFloat(sizeMatch[1]) ||
height !== parseFloat(sizeMatch[2])
) {
return res
.status(400)
.send('Invalid width or height provided in size parameter');
}
parsedWidth = width;
parsedHeight = height;
} else {
return res
.status(400)
.send('Invalid width or height provided in size parameter');
}
} else {
return res
.status(400)
.send('Invalid width or height provided in size parameter');
}
const scale = parseScale(scaleParam, maxScaleFactor); const scale = parseScale(scaleParam, maxScaleFactor);
let isRaw = raw === 'raw'; let isRaw = raw === 'raw';
@ -740,11 +785,12 @@ async function handleStaticRequest(
const staticTypeMatch = staticType.match(staticTypeRegex); const staticTypeMatch = staticType.match(staticTypeRegex);
if (staticTypeMatch.groups.lon) { if (staticTypeMatch.groups.lon) {
// Center Based Static Image // Center Based Static Image
const z = parseFloat(staticTypeMatch.groups.zoom) || 0; const z = staticTypeMatch.groups.zoom;
let x = parseFloat(staticTypeMatch.groups.lon) || 0; let x = staticTypeMatch.groups.lon;
let y = parseFloat(staticTypeMatch.groups.lat) || 0; let y = staticTypeMatch.groups.lat;
const bearing = parseFloat(staticTypeMatch.groups.bearing) || 0; const bearing = staticTypeMatch.groups.bearing;
const pitch = parseInt(staticTypeMatch.groups.pitch) || 0; const pitch = staticTypeMatch.groups.pitch;
if (z < 0) { if (z < 0) {
return res.status(404).send('Invalid zoom'); return res.status(404).send('Invalid zoom');
} }
@ -764,13 +810,13 @@ async function handleStaticRequest(
// prettier-ignore // prettier-ignore
const overlay = await renderOverlay( const overlay = await renderOverlay(
z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, paths, markers, req.query, z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, paths, markers, req.query,
); );
// prettier-ignore // prettier-ignore
return await respondImage( return await respondImage(
options, item, z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, format, res, overlay, 'static', options, item, z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, format, res, overlay, 'static',
); );
} else if (staticTypeMatch.groups.minx) { } else if (staticTypeMatch.groups.minx) {
// Area Based Static Image // Area Based Static Image
const bbox = [ const bbox = [
@ -802,15 +848,16 @@ async function handleStaticRequest(
const pitch = 0; const pitch = 0;
const paths = extractPathsFromQuery(req.query, transformer); const paths = extractPathsFromQuery(req.query, transformer);
const markers = extractMarkersFromQuery(req.query, options, transformer); const markers = extractMarkersFromQuery(req.query, options, transformer);
// prettier-ignore // prettier-ignore
const overlay = await renderOverlay( const overlay = await renderOverlay(
z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, paths, markers, req.query, z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, paths, markers, req.query,
); );
// prettier-ignore // prettier-ignore
return await respondImage( return await respondImage(
options, item, z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, format, res, overlay, 'static', options, item, z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, format, res, overlay, 'static',
); );
} else if (staticTypeMatch.groups.auto) { } else if (staticTypeMatch.groups.auto) {
// Area Static Image // Area Static Image
const bearing = 0; const bearing = 0;
@ -863,13 +910,13 @@ async function handleStaticRequest(
// prettier-ignore // prettier-ignore
const overlay = await renderOverlay( const overlay = await renderOverlay(
z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, paths, markers, req.query, z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, paths, markers, req.query,
); );
// prettier-ignore // prettier-ignore
return await respondImage( return await respondImage(
options, item, z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, format, res, overlay, 'static', options, item, z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, format, res, overlay, 'static',
); );
} else { } else {
return res.sendStatus(404); return res.sendStatus(404);
} }
@ -887,7 +934,7 @@ export const serve_rendered = {
* @returns {Promise<express.Application>} A promise that resolves to the Express app. * @returns {Promise<express.Application>} A promise that resolves to the Express app.
*/ */
init: async function (options, repo, programOpts) { init: async function (options, repo, programOpts) {
const { verbose } = programOpts; const { verbose, tileSize: defailtTileSize = 256 } = programOpts;
maxScaleFactor = Math.min(Math.floor(options.maxScaleFactor || 3), 9); maxScaleFactor = Math.min(Math.floor(options.maxScaleFactor || 3), 9);
const app = express().disable('x-powered-by'); const app = express().disable('x-powered-by');
@ -896,7 +943,7 @@ export const serve_rendered = {
* @param {object} req - Express request object. * @param {object} req - Express request object.
* @param {object} res - Express response object. * @param {object} res - Express response object.
* @param {string} req.params.id - The id of the style. * @param {string} req.params.id - The id of the style.
* @param {string} req.params.p1 - The tile size or static parameter, if available * @param {string} [req.params.p1] - The tile size or static parameter, if available.
* @param {string} req.params.p2 - The z, static, or raw parameter. * @param {string} req.params.p2 - The z, static, or raw parameter.
* @param {string} req.params.p3 - The x or staticType parameter. * @param {string} req.params.p3 - The x or staticType parameter.
* @param {string} req.params.p4 - The y or width parameter. * @param {string} req.params.p4 - The y or width parameter.
@ -906,14 +953,24 @@ export const serve_rendered = {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
app.get( app.get(
`/:id{/:p1}/:p2/:p3/:p4{x:p5}{@:scale}{.:format}`, `/:id{/:p1}/:p2/:p3/:p4{@:scale}{.:format}`,
async (req, res, next) => { async (req, res, next) => {
try { try {
const { p1, p2, id, p3, p4, p5, scale, format } = req.params;
const requestType =
(!p1 && p2 === 'static') || (p1 === 'static' && p2 === 'raw')
? 'static'
: 'tile';
if (verbose) { if (verbose) {
console.log(req.params); console.log(
`Handling rendered ${requestType} request for: /styles/${id}${p1 ? '/' + p1 : ''}/${p2}/${p3}/${p4}${p5 ? 'x' + p5 : ''}${
scale ? '@' + scale : ''
}.${format}`,
);
} }
const { p1, p2 } = req.params;
if ((!p1 && p2 === 'static') || (p1 === 'static' && p2 === 'raw')) { if (requestType === 'static') {
// Route to static if p2 is static // Route to static if p2 is static
if (options.serveStaticMaps !== false) { if (options.serveStaticMaps !== false) {
return handleStaticRequest( return handleStaticRequest(
@ -923,6 +980,7 @@ export const serve_rendered = {
res, res,
next, next,
maxScaleFactor, maxScaleFactor,
defailtTileSize,
); );
} }
return res.sendStatus(404); return res.sendStatus(404);
@ -935,6 +993,7 @@ export const serve_rendered = {
res, res,
next, next,
maxScaleFactor, maxScaleFactor,
defailtTileSize,
); );
} catch (e) { } catch (e) {
console.log(e); console.log(e);
@ -944,7 +1003,7 @@ export const serve_rendered = {
); );
/** /**
* Handles requests for tile json endpoint. * Handles requests for rendered tilejson endpoint.
* @param {object} req - Express request object. * @param {object} req - Express request object.
* @param {object} res - Express response object. * @param {object} res - Express response object.
* @param {string} req.params.id - The id of the tilejson * @param {string} req.params.id - The id of the tilejson
@ -957,6 +1016,11 @@ export const serve_rendered = {
return res.sendStatus(404); return res.sendStatus(404);
} }
const tileSize = parseInt(req.params.tileSize, 10) || undefined; const tileSize = parseInt(req.params.tileSize, 10) || undefined;
if (verbose) {
console.log(
`Handling rendered tilejson request for: /styles/${tileSize ? tileSize + '/' : ''}${req.params.id}.json`,
);
}
const info = clone(item.tileJSON); const info = clone(item.tileJSON);
info.tiles = getTileUrls( info.tiles = getTileUrls(
req, req,

View file

@ -35,59 +35,95 @@ export const serve_style = {
init: function (options, repo, programOpts) { init: function (options, repo, programOpts) {
const { verbose } = programOpts; const { verbose } = programOpts;
const app = express().disable('x-powered-by'); const app = express().disable('x-powered-by');
/**
* Handles requests for style.json files.
* @param {express.Request} req - Express request object.
* @param {express.Response} res - Express response object.
* @param {express.NextFunction} next - Express next function.
* @param {string} req.params.id - ID of the style.
* @returns {Promise<void>}
*/
app.get('/:id/style.json', (req, res, next) => { app.get('/:id/style.json', (req, res, next) => {
const item = repo[req.params.id]; const { id } = req.params;
if (!item) { if (verbose) {
return res.sendStatus(404); console.log(`Handling style request for: /styles/${id}/style.json`);
} }
const styleJSON_ = clone(item.styleJSON); try {
for (const name of Object.keys(styleJSON_.sources)) { const item = repo[id];
const source = styleJSON_.sources[name]; if (!item) {
source.url = fixUrl(req, source.url, item.publicUrl); return res.sendStatus(404);
if (typeof source.data == 'string') {
source.data = fixUrl(req, source.data, item.publicUrl);
} }
} const styleJSON_ = clone(item.styleJSON);
// mapbox-gl-js viewer cannot handle sprite urls with query for (const name of Object.keys(styleJSON_.sources)) {
if (styleJSON_.sprite) { const source = styleJSON_.sources[name];
if (Array.isArray(styleJSON_.sprite)) { source.url = fixUrl(req, source.url, item.publicUrl);
styleJSON_.sprite.forEach((spriteItem) => { if (typeof source.data == 'string') {
spriteItem.url = fixUrl(req, spriteItem.url, item.publicUrl); source.data = fixUrl(req, source.data, item.publicUrl);
}); }
} else {
styleJSON_.sprite = fixUrl(req, styleJSON_.sprite, item.publicUrl);
} }
if (styleJSON_.sprite) {
if (Array.isArray(styleJSON_.sprite)) {
styleJSON_.sprite.forEach((spriteItem) => {
spriteItem.url = fixUrl(req, spriteItem.url, item.publicUrl);
});
} else {
styleJSON_.sprite = fixUrl(req, styleJSON_.sprite, item.publicUrl);
}
}
if (styleJSON_.glyphs) {
styleJSON_.glyphs = fixUrl(req, styleJSON_.glyphs, item.publicUrl);
}
return res.send(styleJSON_);
} catch (e) {
next(e);
} }
if (styleJSON_.glyphs) {
styleJSON_.glyphs = fixUrl(req, styleJSON_.glyphs, item.publicUrl);
}
return res.send(styleJSON_);
}); });
/**
* Handles GET requests for sprite images and JSON files.
* @param {express.Request} req - Express request object.
* @param {express.Response} res - Express response object.
* @param {express.NextFunction} next - Express next function.
* @param {string} req.params.id - ID of the sprite.
* @param {string} [req.params.spriteID='default'] - ID of the specific sprite image, defaults to 'default'.
* @param {string} [req.params.scale] - Scale of the sprite image, defaults to ''.
* @param {string} req.params.format - Format of the sprite file, 'png' or 'json'.
* @returns {Promise<void>}
*/
app.get(`/:id/sprite{/:spriteID}{@:scale}{.:format}`, (req, res, next) => { app.get(`/:id/sprite{/:spriteID}{@:scale}{.:format}`, (req, res, next) => {
if (verbose) { const { spriteID = 'default', id, format, scale } = req.params;
console.log(req.params); const spriteScale = allowedSpriteScales(scale);
}
const { spriteID = 'default', id, format } = req.params;
const spriteScale = allowedSpriteScales(req.params.scale);
if (verbose) {
console.log(
`Handling sprite request for: /${id}/sprite/${spriteID}${scale}.${format}`,
);
}
const item = repo[id]; const item = repo[id];
if (!item || !allowedSpriteFormats(format)) { if (!item || !allowedSpriteFormats(format)) {
if (verbose)
console.error(
`Sprite item or format not found for: /${id}/sprite/${spriteID}${scale}.${format}`,
);
return res.sendStatus(404); return res.sendStatus(404);
} }
const sprite = item.spritePaths.find((sprite) => sprite.id === spriteID); const sprite = item.spritePaths.find((sprite) => sprite.id === spriteID);
if (!sprite) { if (!sprite) {
if (verbose)
console.error(
`Sprite not found for: /${id}/sprite/${spriteID}${scale}.${format}`,
);
return res.status(400).send('Bad Sprite ID or Scale'); return res.status(400).send('Bad Sprite ID or Scale');
} }
const filename = `${sprite.path}${spriteScale}.${format}`; const filename = `${sprite.path}${spriteScale}.${format}`;
if (verbose) console.log(`Loading sprite from: ${filename}`);
// eslint-disable-next-line security/detect-non-literal-fs-filename // eslint-disable-next-line security/detect-non-literal-fs-filename
fs.readFile(filename, (err, data) => { fs.readFile(filename, (err, data) => {
if (err) { if (err) {
console.error('Sprite load error: %s, Error: %s', filename, err); if (verbose)
console.error('Sprite load error: %s, Error: %s', filename, err);
return res.sendStatus(404); return res.sendStatus(404);
} }
@ -96,6 +132,10 @@ export const serve_style = {
} else if (format === 'png') { } else if (format === 'png') {
res.header('Content-type', 'image/png'); res.header('Content-type', 'image/png');
} }
if (verbose)
console.log(
`Responding with sprite data for /${id}/sprite/${spriteID}${scale}.${format}`,
);
return res.send(data); return res.send(data);
}); });
}); });

View file

@ -135,7 +135,7 @@ describe('Static endpoints', function () {
testStatic(prefix, '0,0,1,1/1x1', 'gif', 400); testStatic(prefix, '0,0,1,1/1x1', 'gif', 400);
testStatic(prefix, '-180,-80,180,80/0.5x2.6', 'png', 404); testStatic(prefix, '-180,-80,180,80/0.5x2.6', 'png', 400);
}); });
}); });

View file

@ -60,16 +60,16 @@ describe('Raster tiles', function () {
describe('invalid requests return 4xx', function () { describe('invalid requests return 4xx', function () {
testTile('non_existent', 256, 0, 0, 0, 'png', 404); testTile('non_existent', 256, 0, 0, 0, 'png', 404);
testTile(prefix, 256, -1, 0, 0, 'png', 404); testTile(prefix, 256, -1, 0, 0, 'png', 400);
testTile(prefix, 256, 25, 0, 0, 'png', 404); testTile(prefix, 256, 25, 0, 0, 'png', 400);
testTile(prefix, 256, 0, 1, 0, 'png', 404); testTile(prefix, 256, 0, 1, 0, 'png', 400);
testTile(prefix, 256, 0, 0, 1, 'png', 404); testTile(prefix, 256, 0, 0, 1, 'png', 400);
testTile(prefix, 256, 0, 0, 0, 'gif', 400); testTile(prefix, 256, 0, 0, 0, 'gif', 400);
testTile(prefix, 256, 0, 0, 0, 'pbf', 400); testTile(prefix, 256, 0, 0, 0, 'pbf', 400);
testTile(prefix, 256, 0, 0, 0, 'png', 404, 1); testTile(prefix, 256, 0, 0, 0, 'png', 400, 1);
testTile(prefix, 256, 0, 0, 0, 'png', 404, 5); testTile(prefix, 256, 0, 0, 0, 'png', 400, 5);
testTile(prefix, 300, 0, 0, 0, 'png', 404); testTile(prefix, 300, 0, 0, 0, 'png', 400);
}); });
}); });