mirror of
https://github.com/maptiler/tileserver-gl.git
synced 2024-10-26 14:08:12 +03:00
Extended Static-Images Endpoint (#619)
* extended staticmap capabilities * added allowRemoteMarkerIcons configuration option and restricted fetching of remote marker icons only when option is set to true; asynchronously load all available icons in a settings object on server startup; replaced fs.existsSync() call in serve_rendered when drawing marker icons with a check against available icons settings object; * updated documentation for linecap parameter in staticmaps endpoint; added linejoin parameter to staticmaps endpoint; * added capability of staticmap endpoint to draw multiple paths
This commit is contained in:
parent
f8a0ab6d3c
commit
f3f63498a8
@ -14,6 +14,7 @@ Example:
|
||||
"root": "",
|
||||
"fonts": "fonts",
|
||||
"sprites": "sprites",
|
||||
"icons": "icons",
|
||||
"styles": "styles",
|
||||
"mbtiles": ""
|
||||
},
|
||||
@ -31,6 +32,7 @@ Example:
|
||||
"serveAllFonts": false,
|
||||
"serveAllStyles": false,
|
||||
"serveStaticMaps": true,
|
||||
"allowRemoteMarkerIcons": true,
|
||||
"tileMargin": 0
|
||||
},
|
||||
"styles": {
|
||||
@ -141,6 +143,13 @@ Optional string to be rendered into the raster tiles (and static maps) as waterm
|
||||
Can be used for hard-coding attributions etc. (can also be specified per-style).
|
||||
Not used by default.
|
||||
|
||||
``allowRemoteMarkerIcons``
|
||||
--------------
|
||||
|
||||
Allows the rendering of marker icons fetched via http(s) hyperlinks.
|
||||
For security reasons only allow this if you can control the origins from where the markers are fetched!
|
||||
Default is to disallow fetching of icons from remote sources.
|
||||
|
||||
``styles``
|
||||
==========
|
||||
|
||||
|
@ -38,15 +38,41 @@ Static images
|
||||
* ``path`` - comma-separated ``lng,lat``, pipe-separated pairs
|
||||
|
||||
* e.g. ``5.9,45.8|5.9,47.8|10.5,47.8|10.5,45.8|5.9,45.8``
|
||||
* can be provided multiple times
|
||||
|
||||
* ``latlng`` - indicates the ``path`` coordinates are in ``lat,lng`` order rather than the usual ``lng,lat``
|
||||
* ``latlng`` - indicates coordinates are in ``lat,lng`` order rather than the usual ``lng,lat``
|
||||
* ``fill`` - color to use as the fill (e.g. ``red``, ``rgba(255,255,255,0.5)``, ``#0000ff``)
|
||||
* ``stroke`` - color of the path stroke
|
||||
* ``width`` - width of the stroke
|
||||
* ``linecap`` - rendering style for the start and end points of the path
|
||||
* ``linejoin`` - rendering style for overlapping segments of the path with differing directions
|
||||
* ``border`` - color of the optional border path stroke
|
||||
* ``borderwidth`` - width of the border stroke (default 10% of width)
|
||||
* ``marker`` - Marker in format ``lng,lat|iconPath|option|option|...``
|
||||
|
||||
* Will be rendered with the bottom center at the provided location
|
||||
* ``lng,lat`` and ``iconPath`` are mandatory and icons won't be rendered without them
|
||||
* ``iconPath`` is either a link to an image served via http(s) or a path to a file relative to the configured icon path
|
||||
* ``option`` must adhere to the format ``optionName:optionValue`` and supports the following names
|
||||
|
||||
* ``scale`` - Factor to scale image by
|
||||
|
||||
* e.g. ``0.5`` - Scales the image to half it's original size
|
||||
|
||||
* ``offset`` - Image offset as positive or negative pixel value in format ``[offsetX],[offsetY]``
|
||||
|
||||
* scales with ``scale`` parameter since image placement is relative to it's size
|
||||
* e.g. ``2,-4`` - Image will be moved 2 pixel to the right and 4 pixel in the upwards direction from the provided location
|
||||
|
||||
* e.g. ``5.9,45.8|marker-start.svg|scale:0.5|offset:2,-4``
|
||||
* can be provided multiple times
|
||||
|
||||
* ``padding`` - "percentage" padding for fitted endpoints (area-based and path autofit)
|
||||
|
||||
* value of ``0.1`` means "add 10% size to each side to make sure the area of interest is nicely visible"
|
||||
|
||||
* ``maxzoom`` - Maximum zoom level (only for auto endpoint where zoom level is calculated and not provided)
|
||||
|
||||
* You can also use (experimental) ``/styles/{id}/static/raw/...`` endpoints with raw spherical mercator coordinates (EPSG:3857) instead of WGS84.
|
||||
|
||||
* The static images are not available in the ``tileserver-gl-light`` version.
|
||||
|
@ -39,7 +39,8 @@
|
||||
"proj4": "2.8.0",
|
||||
"request": "2.88.2",
|
||||
"sharp": "0.31.0",
|
||||
"tileserver-gl-styles": "2.0.0"
|
||||
"tileserver-gl-styles": "2.0.0",
|
||||
"sanitize-filename": "1.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "4.3.6",
|
||||
|
@ -11,6 +11,7 @@ import pkg from 'canvas';
|
||||
import clone from 'clone';
|
||||
import Color from 'color';
|
||||
import express from 'express';
|
||||
import sanitize from "sanitize-filename";
|
||||
import SphericalMercator from '@mapbox/sphericalmercator';
|
||||
import mlgl from '@maplibre/maplibre-gl-native';
|
||||
import MBTiles from '@mapbox/mbtiles';
|
||||
@ -21,7 +22,7 @@ import {getFontsPbf, getTileUrls, fixTileJSONCenter} from './utils.js';
|
||||
const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+\.?\\d+)';
|
||||
const httpTester = /^(http(s)?:)?\/\//;
|
||||
|
||||
const {createCanvas} = pkg;
|
||||
const {createCanvas, Image} = pkg;
|
||||
const mercator = new SphericalMercator();
|
||||
const getScale = (scale) => (scale || '@1x').slice(1, 2) | 0;
|
||||
|
||||
@ -93,37 +94,385 @@ function createEmptyResponse(format, color, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
const extractPathFromQuery = (query, transformer) => {
|
||||
const pathParts = (query.path || '').split('|');
|
||||
const path = [];
|
||||
for (const pair of pathParts) {
|
||||
const pairParts = pair.split(',');
|
||||
if (pairParts.length === 2) {
|
||||
let pair;
|
||||
if (query.latlng === '1' || query.latlng === 'true') {
|
||||
pair = [+(pairParts[1]), +(pairParts[0])];
|
||||
} else {
|
||||
pair = [+(pairParts[0]), +(pairParts[1])];
|
||||
}
|
||||
if (transformer) {
|
||||
pair = transformer(pair);
|
||||
}
|
||||
path.push(pair);
|
||||
}
|
||||
/**
|
||||
* Parses coordinate pair provided to pair of floats and ensures the resulting
|
||||
* pair is a longitude/latitude combination depending on lnglat query parameter.
|
||||
* @param {List} coordinatePair Coordinate pair.
|
||||
* @param {Object} query Request query parameters.
|
||||
*/
|
||||
const parseCoordinatePair = (coordinates, query) => {
|
||||
const firstCoordinate = parseFloat(coordinates[0]);
|
||||
const secondCoordinate = parseFloat(coordinates[1]);
|
||||
|
||||
// Ensure provided coordinates could be parsed and abort if not
|
||||
if (isNaN(firstCoordinate) || isNaN(secondCoordinate)) {
|
||||
return null;
|
||||
}
|
||||
return path;
|
||||
|
||||
// Check if coordinates have been provided as lat/lng pair instead of the
|
||||
// ususal lng/lat pair and ensure resulting pair is lng/lat
|
||||
if (query.latlng === '1' || query.latlng === 'true') {
|
||||
return [secondCoordinate, firstCoordinate];
|
||||
}
|
||||
|
||||
return [firstCoordinate, secondCoordinate];
|
||||
};
|
||||
|
||||
const renderOverlay = (z, x, y, bearing, pitch, w, h, scale,
|
||||
path, query) => {
|
||||
/**
|
||||
* Parses a coordinate pair from query arguments and optionally transforms it.
|
||||
* @param {List} coordinatePair Coordinate pair.
|
||||
* @param {Object} query Request query parameters.
|
||||
* @param {Function} transformer Optional transform function.
|
||||
*/
|
||||
const parseCoordinates = (coordinatePair, query, transformer) => {
|
||||
const parsedCoordinates = parseCoordinatePair(coordinatePair, query);
|
||||
|
||||
// Transform coordinates
|
||||
if (transformer) {
|
||||
return transformer(parsedCoordinates);
|
||||
}
|
||||
|
||||
return parsedCoordinates;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Parses paths provided via query into a list of path objects.
|
||||
* @param {Object} query Request query parameters.
|
||||
* @param {Function} transformer Optional transform function.
|
||||
*/
|
||||
const extractPathsFromQuery = (query, transformer) => {
|
||||
// Return an empty list if no paths have been provided
|
||||
if (!query.path) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const paths = [];
|
||||
|
||||
// Check if multiple paths have been provided and mimic a list if it's a
|
||||
// single path.
|
||||
const providedPaths = Array.isArray(query.path) ? query.path : [query.path];
|
||||
|
||||
// Iterate through paths, parse and validate them
|
||||
for (const provided_path of providedPaths) {
|
||||
const currentPath = [];
|
||||
|
||||
// Extract coordinate-list from path
|
||||
const pathParts = (provided_path || '').split('|');
|
||||
|
||||
// Iterate through coordinate-list, parse the coordinates and validate them
|
||||
for (const pair of pathParts) {
|
||||
// Extract coordinates from coordinate pair
|
||||
const pairParts = pair.split(',');
|
||||
|
||||
// Ensure we have two coordinates
|
||||
if (pairParts.length === 2) {
|
||||
const pair = parseCoordinates(pairParts, query, transformer);
|
||||
|
||||
// Ensure coordinates could be parsed and skip them if not
|
||||
if (pair === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add the coordinate-pair to the current path if they are valid
|
||||
currentPath.push(pair);
|
||||
}
|
||||
}
|
||||
|
||||
// Extend list of paths with current path if it contains coordinates
|
||||
if (currentPath.length) {
|
||||
paths.push(currentPath)
|
||||
}
|
||||
|
||||
}
|
||||
return paths;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses marker options provided via query and sets corresponding attributes
|
||||
* on marker object.
|
||||
* Options adhere to the following format
|
||||
* [optionName]:[optionValue]
|
||||
* @param {List[String]} optionsList List of option strings.
|
||||
* @param {Object} marker Marker object to configure.
|
||||
*/
|
||||
const parseMarkerOptions = (optionsList, marker) => {
|
||||
for (const options of optionsList) {
|
||||
const optionParts = options.split(':');
|
||||
// Ensure we got an option name and value
|
||||
if (optionParts.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (optionParts[0]) {
|
||||
// Scale factor to up- or downscale icon
|
||||
case 'scale':
|
||||
// Scale factors must not be negative
|
||||
marker.scale = Math.abs(parseFloat(optionParts[1]))
|
||||
break;
|
||||
// Icon offset as positive or negative pixel value in the following
|
||||
// format [offsetX],[offsetY] where [offsetY] is optional
|
||||
case 'offset':
|
||||
const providedOffset = optionParts[1].split(',');
|
||||
// Set X-axis offset
|
||||
marker.offsetX = parseFloat(providedOffset[0]);
|
||||
// Check if an offset has been provided for Y-axis
|
||||
if (providedOffset.length > 1) {
|
||||
marker.offsetY = parseFloat(providedOffset[1]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses markers provided via query into a list of marker objects.
|
||||
* @param {Object} query Request query parameters.
|
||||
* @param {Object} options Configuration options.
|
||||
* @param {Function} transformer Optional transform function.
|
||||
*/
|
||||
const extractMarkersFromQuery = (query, options, transformer) => {
|
||||
// Return an empty list if no markers have been provided
|
||||
if (!query.marker) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const markers = [];
|
||||
|
||||
// Check if multiple markers have been provided and mimic a list if it's a
|
||||
// single maker.
|
||||
const providedMarkers = Array.isArray(query.marker) ?
|
||||
query.marker : [query.marker];
|
||||
|
||||
// Iterate through provided markers which can have one of the following
|
||||
// formats
|
||||
// [location]|[pathToFileTelativeToConfiguredIconPath]
|
||||
// [location]|[pathToFile...]|[option]|[option]|...
|
||||
for (const providedMarker of providedMarkers) {
|
||||
const markerParts = providedMarker.split('|');
|
||||
// Ensure we got at least a location and an icon uri
|
||||
if (markerParts.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const locationParts = markerParts[0].split(',');
|
||||
// Ensure the locationParts contains two items
|
||||
if (locationParts.length !== 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let iconURI = markerParts[1];
|
||||
// Check if icon is served via http otherwise marker icons are expected to
|
||||
// be provided as filepaths relative to configured icon path
|
||||
if (!(iconURI.startsWith('http://') || iconURI.startsWith('https://'))) {
|
||||
// Sanitize URI with sanitize-filename
|
||||
// https://www.npmjs.com/package/sanitize-filename#details
|
||||
iconURI = sanitize(iconURI)
|
||||
|
||||
// If the selected icon is not part of available icons skip it
|
||||
if (!options.paths.availableIcons.includes(iconURI)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
iconURI = path.resolve(options.paths.icons, iconURI);
|
||||
|
||||
// When we encounter a remote icon check if the configuration explicitly allows them.
|
||||
} else if (options.allowRemoteMarkerIcons !== true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure marker location could be parsed
|
||||
const location = parseCoordinates(locationParts, query, transformer);
|
||||
if (location === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const marker = {};
|
||||
|
||||
marker.location = location;
|
||||
marker.icon = iconURI;
|
||||
|
||||
// Check if options have been provided
|
||||
if (markerParts.length > 2) {
|
||||
parseMarkerOptions(markerParts.slice(2), marker);
|
||||
}
|
||||
|
||||
// Add marker to list
|
||||
markers.push(marker);
|
||||
|
||||
}
|
||||
return markers;
|
||||
};
|
||||
|
||||
/**
|
||||
* Transforms coordinates to pixels.
|
||||
* @param {List[Number]} ll Longitude/Latitude coordinate pair.
|
||||
* @param {Number} zoom Map zoom level.
|
||||
*/
|
||||
const precisePx = (ll, zoom) => {
|
||||
const px = mercator.px(ll, 20);
|
||||
const scale = Math.pow(2, zoom - 20);
|
||||
return [px[0] * scale, px[1] * scale];
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a marker in cavans context.
|
||||
* @param {Object} ctx Canvas context object.
|
||||
* @param {Object} marker Marker object parsed by extractMarkersFromQuery.
|
||||
* @param {Number} z Map zoom level.
|
||||
*/
|
||||
const drawMarker = (ctx, marker, z) => {
|
||||
return new Promise(resolve => {
|
||||
const img = new Image();
|
||||
const pixelCoords = precisePx(marker.location, z);
|
||||
|
||||
const getMarkerCoordinates = (imageWidth, imageHeight, scale) => {
|
||||
// Images are placed with their top-left corner at the provided location
|
||||
// within the canvas but we expect icons to be centered and above it.
|
||||
|
||||
// Substract half of the images width from the x-coordinate to center
|
||||
// the image in relation to the provided location
|
||||
let xCoordinate = pixelCoords[0] - imageWidth / 2;
|
||||
// Substract the images height from the y-coordinate to place it above
|
||||
// the provided location
|
||||
let yCoordinate = pixelCoords[1] - imageHeight;
|
||||
|
||||
// Since image placement is dependent on the size offsets have to be
|
||||
// scaled as well. Additionally offsets are provided as either positive or
|
||||
// negative values so we always add them
|
||||
if (marker.offsetX) {
|
||||
xCoordinate = xCoordinate + (marker.offsetX * scale);
|
||||
}
|
||||
if (marker.offsetY) {
|
||||
yCoordinate = yCoordinate + (marker.offsetY * scale);
|
||||
}
|
||||
|
||||
return {
|
||||
'x': xCoordinate,
|
||||
'y': yCoordinate
|
||||
};
|
||||
};
|
||||
|
||||
const drawOnCanvas = () => {
|
||||
// Check if the images should be resized before beeing drawn
|
||||
const defaultScale = 1;
|
||||
const scale = marker.scale ? marker.scale : defaultScale;
|
||||
|
||||
// Calculate scaled image sizes
|
||||
const imageWidth = img.width * scale;
|
||||
const imageHeight = img.height * scale;
|
||||
|
||||
// Pass the desired sizes to get correlating coordinates
|
||||
const coords = getMarkerCoordinates(imageWidth, imageHeight, scale);
|
||||
|
||||
// Draw the image on canvas
|
||||
if (scale != defaultScale) {
|
||||
ctx.drawImage(img, coords.x, coords.y, imageWidth, imageHeight);
|
||||
} else {
|
||||
ctx.drawImage(img, coords.x, coords.y);
|
||||
}
|
||||
// Resolve the promise when image has been drawn
|
||||
resolve();
|
||||
};
|
||||
|
||||
img.onload = drawOnCanvas;
|
||||
img.onerror = err => { throw err };
|
||||
img.src = marker.icon;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a list of markers onto a canvas.
|
||||
* Wraps drawing of markers into list of promises and awaits them.
|
||||
* It's required because images are expected to load asynchronous in canvas js
|
||||
* even when provided from a local disk.
|
||||
* @param {Object} ctx Canvas context object.
|
||||
* @param {List[Object]} markers Marker objects parsed by extractMarkersFromQuery.
|
||||
* @param {Number} z Map zoom level.
|
||||
*/
|
||||
const drawMarkers = async (ctx, markers, z) => {
|
||||
const markerPromises = [];
|
||||
|
||||
for (const marker of markers) {
|
||||
// Begin drawing marker
|
||||
markerPromises.push(drawMarker(ctx, marker, z));
|
||||
}
|
||||
|
||||
// Await marker drawings before continuing
|
||||
await Promise.all(markerPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a list of coordinates onto a canvas and styles the resulting path.
|
||||
* @param {Object} ctx Canvas context object.
|
||||
* @param {List[Number]} path List of coordinates.
|
||||
* @param {Object} query Request query parameters.
|
||||
* @param {Number} z Map zoom level.
|
||||
*/
|
||||
const drawPath = (ctx, path, query, z) => {
|
||||
if (!path || path.length < 2) {
|
||||
return null;
|
||||
}
|
||||
const precisePx = (ll, zoom) => {
|
||||
const px = mercator.px(ll, 20);
|
||||
const scale = Math.pow(2, zoom - 20);
|
||||
return [px[0] * scale, px[1] * scale];
|
||||
};
|
||||
|
||||
ctx.beginPath();
|
||||
|
||||
// Transform coordinates to pixel on canvas and draw lines between points
|
||||
for (const pair of path) {
|
||||
const px = precisePx(pair, z);
|
||||
ctx.lineTo(px[0], px[1]);
|
||||
}
|
||||
|
||||
// Check if first coordinate matches last coordinate
|
||||
if (path[0][0] === path[path.length - 1][0] &&
|
||||
path[0][1] === path[path.length - 1][1]) {
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
// Optionally fill drawn shape with a rgba color from query
|
||||
if (query.fill !== undefined) {
|
||||
ctx.fillStyle = query.fill;
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Get line width from query and fall back to 1 if not provided
|
||||
const lineWidth = query.width !== undefined ?
|
||||
parseFloat(query.width) : 1;
|
||||
|
||||
// Ensure line width is valid
|
||||
if (lineWidth > 0) {
|
||||
// Get border width from query and fall back to 10% of line width
|
||||
const borderWidth = query.borderwidth !== undefined ?
|
||||
parseFloat(query.borderwidth) : lineWidth * 0.1;
|
||||
|
||||
// Set rendering style for the start and end points of the path
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap
|
||||
ctx.lineCap = query.linecap || 'butt';
|
||||
|
||||
// Set rendering style for overlapping segments of the path with differing directions
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin
|
||||
ctx.lineJoin = query.linejoin || 'miter';
|
||||
|
||||
// In order to simulate a border we draw the path two times with the first
|
||||
// beeing the wider border part.
|
||||
if (query.border !== undefined && borderWidth > 0) {
|
||||
// We need to double the desired border width and add it to the line width
|
||||
// in order to get the desired border on each side of the line.
|
||||
ctx.lineWidth = lineWidth + (borderWidth * 2);
|
||||
// Set border style as rgba
|
||||
ctx.strokeStyle = query.border;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.strokeStyle = query.stroke || 'rgba(0,64,255,0.7)';
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
const renderOverlay = async (z, x, y, bearing, pitch, w, h, scale, paths, markers, query) => {
|
||||
if ((!paths || paths.length === 0) && (!markers || markers.length === 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const center = precisePx([x, y], z);
|
||||
|
||||
@ -147,25 +496,15 @@ const renderOverlay = (z, x, y, bearing, pitch, w, h, scale,
|
||||
// optimized path
|
||||
ctx.translate(-center[0] + w / 2, -center[1] + h / 2);
|
||||
}
|
||||
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)';
|
||||
ctx.beginPath();
|
||||
for (const pair of path) {
|
||||
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]) {
|
||||
ctx.closePath();
|
||||
}
|
||||
ctx.fill();
|
||||
if (lineWidth > 0) {
|
||||
ctx.stroke();
|
||||
|
||||
// Draw provided paths if any
|
||||
for (const path of paths) {
|
||||
drawPath(ctx, path, query, z);
|
||||
}
|
||||
|
||||
// Await drawing of markers before rendering the canvas
|
||||
await drawMarkers(ctx, markers, z);
|
||||
|
||||
return canvas.toBuffer();
|
||||
};
|
||||
|
||||
@ -396,7 +735,7 @@ export const serve_rendered = {
|
||||
FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN,
|
||||
FLOAT_PATTERN, FLOAT_PATTERN);
|
||||
|
||||
app.get(util.format(staticPattern, centerPattern), (req, res, next) => {
|
||||
app.get(util.format(staticPattern, centerPattern), async (req, res, next) => {
|
||||
const item = repo[req.params.id];
|
||||
if (!item) {
|
||||
return res.sendStatus(404);
|
||||
@ -425,13 +764,14 @@ export const serve_rendered = {
|
||||
y = ll[1];
|
||||
}
|
||||
|
||||
const path = extractPathFromQuery(req.query, transformer);
|
||||
const overlay = renderOverlay(z, x, y, bearing, pitch, w, h, scale, path, req.query);
|
||||
const paths = extractPathsFromQuery(req.query, transformer);
|
||||
const markers = extractMarkersFromQuery(req.query, options, transformer);
|
||||
const overlay = await renderOverlay(z, x, y, bearing, pitch, w, h, scale, paths, markers, req.query);
|
||||
|
||||
return respondImage(item, z, x, y, bearing, pitch, w, h, scale, format, res, next, overlay, 'static');
|
||||
});
|
||||
|
||||
const serveBounds = (req, res, next) => {
|
||||
const serveBounds = async (req, res, next) => {
|
||||
const item = repo[req.params.id];
|
||||
if (!item) {
|
||||
return res.sendStatus(404);
|
||||
@ -464,9 +804,9 @@ export const serve_rendered = {
|
||||
const bearing = 0;
|
||||
const pitch = 0;
|
||||
|
||||
const path = extractPathFromQuery(req.query, transformer);
|
||||
const overlay = renderOverlay(z, x, y, bearing, pitch, w, h, scale, path, req.query);
|
||||
|
||||
const paths = extractPathsFromQuery(req.query, transformer);
|
||||
const markers = extractMarkersFromQuery(req.query, options, transformer);
|
||||
const overlay = await renderOverlay(z, x, y, bearing, pitch, w, h, scale, paths, markers, req.query);
|
||||
return respondImage(item, z, x, y, bearing, pitch, w, h, scale, format, res, next, overlay, 'static');
|
||||
};
|
||||
|
||||
@ -500,7 +840,7 @@ export const serve_rendered = {
|
||||
|
||||
const autoPattern = 'auto';
|
||||
|
||||
app.get(util.format(staticPattern, autoPattern), (req, res, next) => {
|
||||
app.get(util.format(staticPattern, autoPattern), async (req, res, next) => {
|
||||
const item = repo[req.params.id];
|
||||
if (!item) {
|
||||
return res.sendStatus(404);
|
||||
@ -516,13 +856,25 @@ export const serve_rendered = {
|
||||
const transformer = raw ?
|
||||
mercator.inverse.bind(mercator) : item.dataProjWGStoInternalWGS;
|
||||
|
||||
const path = extractPathFromQuery(req.query, transformer);
|
||||
if (path.length < 2) {
|
||||
return res.status(400).send('Invalid path');
|
||||
const paths = extractPathsFromQuery(req.query, transformer);
|
||||
const markers = extractMarkersFromQuery(req.query, options, transformer);
|
||||
|
||||
// Extract coordinates from markers
|
||||
const markerCoordinates = [];
|
||||
for (const marker of markers) {
|
||||
markerCoordinates.push(marker.location);
|
||||
}
|
||||
|
||||
// Create array with coordinates from markers and path
|
||||
const coords = new Array().concat(paths.flat()).concat(markerCoordinates);
|
||||
|
||||
// Check if we have at least one coordinate to calculate a bounding box
|
||||
if (coords.length < 1) {
|
||||
return res.status(400).send('No coordinates provided');
|
||||
}
|
||||
|
||||
const bbox = [Infinity, Infinity, -Infinity, -Infinity];
|
||||
for (const pair of path) {
|
||||
for (const pair of coords) {
|
||||
bbox[0] = Math.min(bbox[0], pair[0]);
|
||||
bbox[1] = Math.min(bbox[1], pair[1]);
|
||||
bbox[2] = Math.max(bbox[2], pair[0]);
|
||||
@ -534,11 +886,17 @@ export const serve_rendered = {
|
||||
[(bbox_[0] + bbox_[2]) / 2, (bbox_[1] + bbox_[3]) / 2]
|
||||
);
|
||||
|
||||
const z = calcZForBBox(bbox, w, h, req.query);
|
||||
// Calculate zoom level
|
||||
const maxZoom = parseFloat(req.query.maxzoom);
|
||||
let z = calcZForBBox(bbox, w, h, req.query);
|
||||
if (maxZoom > 0) {
|
||||
z = Math.min(z, maxZoom);
|
||||
}
|
||||
|
||||
const x = center[0];
|
||||
const y = center[1];
|
||||
|
||||
const overlay = renderOverlay(z, x, y, bearing, pitch, w, h, scale, path, req.query);
|
||||
const overlay = await renderOverlay(z, x, y, bearing, pitch, w, h, scale, paths, markers, req.query);
|
||||
|
||||
return respondImage(item, z, x, y, bearing, pitch, w, h, scale, format, res, next, overlay, 'static');
|
||||
});
|
||||
|
@ -79,6 +79,7 @@ export function server(opts) {
|
||||
paths.fonts = path.resolve(paths.root, paths.fonts || '');
|
||||
paths.sprites = path.resolve(paths.root, paths.sprites || '');
|
||||
paths.mbtiles = path.resolve(paths.root, paths.mbtiles || '');
|
||||
paths.icons = path.resolve(paths.root, paths.icons || '');
|
||||
|
||||
const startupPromises = [];
|
||||
|
||||
@ -92,6 +93,36 @@ export function server(opts) {
|
||||
checkPath('fonts');
|
||||
checkPath('sprites');
|
||||
checkPath('mbtiles');
|
||||
checkPath('icons');
|
||||
|
||||
/**
|
||||
* Recursively get all files within a directory.
|
||||
* Inspired by https://stackoverflow.com/a/45130990/10133863
|
||||
* @param {String} directory Absolute path to a directory to get files from.
|
||||
*/
|
||||
const getFiles = async (directory) => {
|
||||
// Fetch all entries of the directory and attach type information
|
||||
const dirEntries = await fs.promises.readdir(directory, { withFileTypes: true });
|
||||
|
||||
// Iterate through entries and return the relative file-path to the icon directory if it is not a directory
|
||||
// otherwise initiate a recursive call
|
||||
const files = await Promise.all(dirEntries.map((dirEntry) => {
|
||||
const entryPath = path.resolve(directory, dirEntry.name);
|
||||
return dirEntry.isDirectory() ?
|
||||
getFiles(entryPath) : entryPath.replace(paths.icons + path.sep, "");
|
||||
}));
|
||||
|
||||
// Flatten the list of files to a single array
|
||||
return files.flat();
|
||||
}
|
||||
|
||||
// Load all available icons into a settings object
|
||||
startupPromises.push(new Promise(resolve => {
|
||||
getFiles(paths.icons).then((files) => {
|
||||
paths.availableIcons = files;
|
||||
resolve();
|
||||
});
|
||||
}));
|
||||
|
||||
if (options.dataDecorator) {
|
||||
try {
|
||||
|
@ -95,7 +95,7 @@ describe('Static endpoints', 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/256x256', 'png', 400, undefined, undefined, '?path=invalid');
|
||||
testStatic(prefix, 'auto/2560x2560', 'png', 400, undefined, undefined, '?path=10,10|20,20');
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user