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) => {
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];
if (!item) {
@ -156,6 +158,11 @@ export const serve_data = {
*/
app.get('/:id/elevation/:z/:x/:y', async (req, res, next) => {
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];
if (!item) return res.sendStatus(404);
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} res - Express response object.
* @param {string} req.params.id - ID of the data source.
* @returns {Promise<void>}
*/
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];
if (!item) {
return res.sendStatus(404);

View file

@ -23,7 +23,6 @@ export async function serve_font(options, allowedFonts, programOpts) {
/**
* Handles requests for a font file.
*
* @param {object} req - Express request object.
* @param {object} res - Express response object.
* @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) => {
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 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.
*
* @param {object} req - Express request object.
* @param {object} res - Express response object.
* @returns {void}
*/
app.get('/fonts.json', (req, res) => {
if (verbose) {
console.log('Handling list font request for /fonts.json');
}
res.header('Content-type', 'application/json');
return res.send(
Object.keys(options.serveAllFonts ? existingFonts : allowedFonts).sort(),

View file

@ -34,6 +34,7 @@ import {
isValidHttpUrl,
fixTileJSONCenter,
fetchTileData,
allowedOptions,
} from './utils.js';
import { openPMtiles, getPMtilesInfo } from './pmtiles_adapter.js';
import { renderOverlay, renderWatermark, renderAttribution } from './render.js';
@ -635,6 +636,7 @@ const respondImage = async (
* @param {object} res - Express response object.
* @param {Function} next - Express next middleware function.
* @param {number} maxScaleFactor - The maximum scale factor allowed.
* @param defailtTileSize
* @returns {Promise<void>}
*/
async function handleTileRequest(
@ -644,6 +646,7 @@ async function handleTileRequest(
res,
next,
maxScaleFactor,
defailtTileSize,
) {
const {
id,
@ -658,6 +661,7 @@ async function handleTileRequest(
if (!item) {
return res.sendStatus(404);
}
console.log(req.params);
const modifiedSince = req.get('if-modified-since');
const cc = req.get('cache-control');
@ -670,7 +674,19 @@ async function handleTileRequest(
const x = parseFloat(xParam) | 0;
const y = parseFloat(yParam) | 0;
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 (
scale == null ||
z < 0 ||
@ -680,7 +696,7 @@ async function handleTileRequest(
x >= 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(
@ -722,14 +738,43 @@ async function handleStaticRequest(
id,
p2: raw,
p3: staticType,
p4: width,
p5: height,
p4: widthAndHeight,
scale: scaleParam,
format,
} = req.params;
console.log(req.params);
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);
let isRaw = raw === 'raw';
@ -740,11 +785,12 @@ async function handleStaticRequest(
const staticTypeMatch = staticType.match(staticTypeRegex);
if (staticTypeMatch.groups.lon) {
// Center Based Static Image
const z = parseFloat(staticTypeMatch.groups.zoom) || 0;
let x = parseFloat(staticTypeMatch.groups.lon) || 0;
let y = parseFloat(staticTypeMatch.groups.lat) || 0;
const bearing = parseFloat(staticTypeMatch.groups.bearing) || 0;
const pitch = parseInt(staticTypeMatch.groups.pitch) || 0;
const z = staticTypeMatch.groups.zoom;
let x = staticTypeMatch.groups.lon;
let y = staticTypeMatch.groups.lat;
const bearing = staticTypeMatch.groups.bearing;
const pitch = staticTypeMatch.groups.pitch;
if (z < 0) {
return res.status(404).send('Invalid zoom');
}
@ -802,6 +848,7 @@ async function handleStaticRequest(
const pitch = 0;
const paths = extractPathsFromQuery(req.query, transformer);
const markers = extractMarkersFromQuery(req.query, options, transformer);
// prettier-ignore
const overlay = await renderOverlay(
z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, paths, markers, req.query,
@ -887,7 +934,7 @@ export const serve_rendered = {
* @returns {Promise<express.Application>} A promise that resolves to the Express app.
*/
init: async function (options, repo, programOpts) {
const { verbose } = programOpts;
const { verbose, tileSize: defailtTileSize = 256 } = programOpts;
maxScaleFactor = Math.min(Math.floor(options.maxScaleFactor || 3), 9);
const app = express().disable('x-powered-by');
@ -896,7 +943,7 @@ export const serve_rendered = {
* @param {object} req - Express request object.
* @param {object} res - Express response object.
* @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.p3 - The x or staticType parameter.
* @param {string} req.params.p4 - The y or width parameter.
@ -906,14 +953,24 @@ export const serve_rendered = {
* @returns {Promise<void>}
*/
app.get(
`/:id{/:p1}/:p2/:p3/:p4{x:p5}{@:scale}{.:format}`,
`/:id{/:p1}/:p2/:p3/:p4{@:scale}{.:format}`,
async (req, res, next) => {
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) {
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
if (options.serveStaticMaps !== false) {
return handleStaticRequest(
@ -923,6 +980,7 @@ export const serve_rendered = {
res,
next,
maxScaleFactor,
defailtTileSize,
);
}
return res.sendStatus(404);
@ -935,6 +993,7 @@ export const serve_rendered = {
res,
next,
maxScaleFactor,
defailtTileSize,
);
} catch (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} res - Express response object.
* @param {string} req.params.id - The id of the tilejson
@ -957,6 +1016,11 @@ export const serve_rendered = {
return res.sendStatus(404);
}
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);
info.tiles = getTileUrls(
req,

View file

@ -35,9 +35,21 @@ export const serve_style = {
init: function (options, repo, programOpts) {
const { verbose } = programOpts;
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) => {
const item = repo[req.params.id];
const { id } = req.params;
if (verbose) {
console.log(`Handling style request for: /styles/${id}/style.json`);
}
try {
const item = repo[id];
if (!item) {
return res.sendStatus(404);
}
@ -49,7 +61,6 @@ export const serve_style = {
source.data = fixUrl(req, source.data, item.publicUrl);
}
}
// mapbox-gl-js viewer cannot handle sprite urls with query
if (styleJSON_.sprite) {
if (Array.isArray(styleJSON_.sprite)) {
styleJSON_.sprite.forEach((spriteItem) => {
@ -63,30 +74,55 @@ export const serve_style = {
styleJSON_.glyphs = fixUrl(req, styleJSON_.glyphs, item.publicUrl);
}
return res.send(styleJSON_);
} catch (e) {
next(e);
}
});
/**
* 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) => {
if (verbose) {
console.log(req.params);
}
const { spriteID = 'default', id, format } = req.params;
const spriteScale = allowedSpriteScales(req.params.scale);
const { spriteID = 'default', id, format, scale } = req.params;
const spriteScale = allowedSpriteScales(scale);
if (verbose) {
console.log(
`Handling sprite request for: /${id}/sprite/${spriteID}${scale}.${format}`,
);
}
const item = repo[id];
if (!item || !allowedSpriteFormats(format)) {
if (verbose)
console.error(
`Sprite item or format not found for: /${id}/sprite/${spriteID}${scale}.${format}`,
);
return res.sendStatus(404);
}
const sprite = item.spritePaths.find((sprite) => sprite.id === spriteID);
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');
}
const filename = `${sprite.path}${spriteScale}.${format}`;
if (verbose) console.log(`Loading sprite from: ${filename}`);
// eslint-disable-next-line security/detect-non-literal-fs-filename
fs.readFile(filename, (err, data) => {
if (err) {
if (verbose)
console.error('Sprite load error: %s, Error: %s', filename, err);
return res.sendStatus(404);
}
@ -96,6 +132,10 @@ export const serve_style = {
} else if (format === '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);
});
});

View file

@ -135,7 +135,7 @@ describe('Static endpoints', function () {
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 () {
testTile('non_existent', 256, 0, 0, 0, 'png', 404);
testTile(prefix, 256, -1, 0, 0, 'png', 404);
testTile(prefix, 256, 25, 0, 0, 'png', 404);
testTile(prefix, 256, 0, 1, 0, 'png', 404);
testTile(prefix, 256, 0, 0, 1, 'png', 404);
testTile(prefix, 256, -1, 0, 0, 'png', 400);
testTile(prefix, 256, 25, 0, 0, 'png', 400);
testTile(prefix, 256, 0, 1, 0, 'png', 400);
testTile(prefix, 256, 0, 0, 1, 'png', 400);
testTile(prefix, 256, 0, 0, 0, 'gif', 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', 404, 5);
testTile(prefix, 256, 0, 0, 0, 'png', 400, 1);
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);
});
});