Extract nested functions and simplify a little (#1062)

* chore: make sure error exit codes of tests are returned

Signed-off-by: Martin d'Allens <martin.dallens@liberty-rider.com>

* chore: replace the last 'var' with 'const'

Signed-off-by: Martin d'Allens <martin.dallens@liberty-rider.com>

* chore: extract duplicated font listing

Signed-off-by: Martin d'Allens <martin.dallens@liberty-rider.com>

* chore: extract rendering functions to a new file

Signed-off-by: Martin d'Allens <martin.dallens@liberty-rider.com>

* chore: move nested respondImage() function to top level

Signed-off-by: Martin d'Allens <martin.dallens@liberty-rider.com>

* chore: simplify respondImage() args

Signed-off-by: Martin d'Allens <martin.dallens@liberty-rider.com>

* chore: fix typo in rendeAttribution

Signed-off-by: Martin d'Allens <martin.dallens@liberty-rider.com>

---------

Signed-off-by: Martin d'Allens <martin.dallens@liberty-rider.com>
This commit is contained in:
Martin d'Allens 2023-11-23 16:11:53 +01:00 committed by GitHub
parent cef150431b
commit 526766c8f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 521 additions and 515 deletions

View File

@ -6,7 +6,7 @@
"bin": "src/main.js",
"type": "module",
"scripts": {
"test": "mocha test/**.js --timeout 10000",
"test": "mocha test/**.js --timeout 10000 --exit",
"lint:yml": "yamllint --schema=CORE_SCHEMA *.{yml,yaml}",
"lint:js": "npm run lint:eslint && npm run lint:prettier",
"lint:js:fix": "npm run lint:eslint:fix && npm run lint:prettier:fix",
@ -14,7 +14,7 @@
"lint:eslint:fix": "eslint --fix \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs}\" --ignore-path .gitignore",
"lint:prettier": "prettier --check \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs,json}\" --ignore-path .gitignore",
"lint:prettier:fix": "prettier --write \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs,json}\" --ignore-path .gitignore",
"docker": "docker build -f Dockerfile . && docker run --rm -i -p 8080:8080 $(docker build -q .)",
"docker": "docker build . && docker run --rm -i -p 8080:8080 $(docker build -q .)",
"prepare": "node -e \"if (process.env.NODE_ENV !== 'production'){ process.exit(1) } \" || husky install"
},
"dependencies": {

View File

@ -1,9 +1,9 @@
import * as http from 'http';
var options = {
const options = {
timeout: 2000,
};
var url = 'http://localhost:8080/health';
var request = http.request(url, options, (res) => {
const url = 'http://localhost:8080/health';
const request = http.request(url, options, (res) => {
console.log(`STATUS: ${res.statusCode}`);
if (res.statusCode == 200) {
process.exit(0);

303
src/render.js Normal file
View File

@ -0,0 +1,303 @@
'use strict';
import { createCanvas, Image } from 'canvas';
import SphericalMercator from '@mapbox/sphericalmercator';
const mercator = new SphericalMercator();
/**
* 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 canvas 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 {string} pathQuery Path query parameter.
* @param {number} z Map zoom level.
*/
const drawPath = (ctx, path, query, pathQuery, z) => {
const splitPaths = pathQuery.split('|');
if (!path || path.length < 2) {
return null;
}
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
const pathHasFill = splitPaths.filter((x) => x.startsWith('fill')).length > 0;
if (query.fill !== undefined || pathHasFill) {
if ('fill' in query) {
ctx.fillStyle = query.fill || 'rgba(255,255,255,0.4)';
}
if (pathHasFill) {
ctx.fillStyle = splitPaths
.find((x) => x.startsWith('fill:'))
.replace('fill:', '');
}
ctx.fill();
}
// Get line width from query and fall back to 1 if not provided
const pathHasWidth =
splitPaths.filter((x) => x.startsWith('width')).length > 0;
if (query.width !== undefined || pathHasWidth) {
let lineWidth = 1;
// Get line width from query
if ('width' in query) {
lineWidth = Number(query.width);
}
// Get line width from path in query
if (pathHasWidth) {
lineWidth = Number(
splitPaths.find((x) => x.startsWith('width:')).replace('width:', ''),
);
}
// 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;
}
const pathHasStroke =
splitPaths.filter((x) => x.startsWith('stroke')).length > 0;
if (query.stroke !== undefined || pathHasStroke) {
if ('stroke' in query) {
ctx.strokeStyle = query.stroke;
}
// Path Stroke gets higher priority
if (pathHasStroke) {
ctx.strokeStyle = splitPaths
.find((x) => x.startsWith('stroke:'))
.replace('stroke:', '');
}
} else {
ctx.strokeStyle = 'rgba(0,64,255,0.7)';
}
ctx.stroke();
};
export 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);
const mapHeight = 512 * (1 << z);
const maxEdge = center[1] + h / 2;
const minEdge = center[1] - h / 2;
if (maxEdge > mapHeight) {
center[1] -= maxEdge - mapHeight;
} else if (minEdge < 0) {
center[1] -= minEdge;
}
const canvas = createCanvas(scale * w, scale * h);
const ctx = canvas.getContext('2d');
ctx.scale(scale, scale);
if (bearing) {
ctx.translate(w / 2, h / 2);
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);
}
// Draw provided paths if any
paths.forEach((path, i) => {
const pathQuery = Array.isArray(query.path) ? query.path.at(i) : query.path;
drawPath(ctx, path, query, pathQuery, z);
});
// Await drawing of markers before rendering the canvas
await drawMarkers(ctx, markers, z);
return canvas.toBuffer();
};
export const renderWatermark = (width, height, scale, text) => {
const canvas = createCanvas(scale * width, scale * height);
const ctx = canvas.getContext('2d');
ctx.scale(scale, scale);
ctx.font = '10px sans-serif';
ctx.strokeWidth = '1px';
ctx.strokeStyle = 'rgba(255,255,255,.4)';
ctx.strokeText(text, 5, height - 5);
ctx.fillStyle = 'rgba(0,0,0,.4)';
ctx.fillText(text, 5, height - 5);
return canvas;
};
export const renderAttribution = (width, height, scale, text) => {
const canvas = createCanvas(scale * width, scale * height);
const ctx = canvas.getContext('2d');
ctx.scale(scale, scale);
ctx.font = '10px sans-serif';
const textMetrics = ctx.measureText(text);
const textWidth = textMetrics.width;
const textHeight = 14;
const padding = 6;
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
ctx.fillRect(
width - textWidth - padding,
height - textHeight - padding,
textWidth + padding,
textHeight + padding,
);
ctx.fillStyle = 'rgba(0,0,0,.8)';
ctx.fillText(text, width - textWidth - padding / 2, height - textHeight + 8);
return canvas;
};

View File

@ -1,10 +1,8 @@
'use strict';
import express from 'express';
import fs from 'node:fs';
import path from 'path';
import { getFontsPbf } from './utils.js';
import { getFontsPbf, listFonts } from './utils.js';
export const serve_font = (options, allowedFonts) => {
const app = express().disable('x-powered-by');
@ -14,29 +12,6 @@ export const serve_font = (options, allowedFonts) => {
const fontPath = options.paths.fonts;
const existingFonts = {};
const fontListingPromise = new Promise((resolve, reject) => {
fs.readdir(options.paths.fonts, (err, files) => {
if (err) {
reject(err);
return;
}
for (const file of files) {
fs.stat(path.join(fontPath, file), (err, stats) => {
if (err) {
reject(err);
return;
}
if (
stats.isDirectory() &&
fs.existsSync(path.join(fontPath, file, '0-255.pbf'))
) {
existingFonts[path.basename(file)] = true;
}
});
}
resolve();
});
});
app.get('/fonts/:fontstack/:range([\\d]+-[\\d]+).pbf', (req, res, next) => {
const fontstack = decodeURI(req.params.fontstack);
@ -65,5 +40,8 @@ export const serve_font = (options, allowedFonts) => {
);
});
return fontListingPromise.then(() => app);
return listFonts(options.paths.fonts).then((fonts) => {
Object.assign(existingFonts, fonts);
return app;
});
};

View File

@ -7,7 +7,6 @@ import url from 'url';
import util from 'util';
import zlib from 'zlib';
import sharp from 'sharp'; // sharp has to be required before node-canvas on linux but after it on windows. see https://github.com/lovell/sharp/issues/371
import { createCanvas, Image } from 'canvas';
import clone from 'clone';
import Color from 'color';
import express from 'express';
@ -20,6 +19,7 @@ import proj4 from 'proj4';
import axios from 'axios';
import {
getFontsPbf,
listFonts,
getTileUrls,
isValidHttpUrl,
fixTileJSONCenter,
@ -29,6 +29,7 @@ import {
GetPMtilesInfo,
GetPMtilesTile,
} from './pmtiles_adapter.js';
import { renderOverlay, renderWatermark, renderAttribution } from './render.js';
const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+.?\\d+)';
const PATH_PATTERN =
@ -330,263 +331,6 @@ const extractMarkersFromQuery = (query, options, transformer) => {
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 {string} pathQuery Path query parameter.
* @param {number} z Map zoom level.
*/
const drawPath = (ctx, path, query, pathQuery, z) => {
const splitPaths = pathQuery.split('|');
if (!path || path.length < 2) {
return null;
}
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
const pathHasFill = splitPaths.filter((x) => x.startsWith('fill')).length > 0;
if (query.fill !== undefined || pathHasFill) {
if ('fill' in query) {
ctx.fillStyle = query.fill || 'rgba(255,255,255,0.4)';
}
if (pathHasFill) {
ctx.fillStyle = splitPaths
.find((x) => x.startsWith('fill:'))
.replace('fill:', '');
}
ctx.fill();
}
// Get line width from query and fall back to 1 if not provided
const pathHasWidth =
splitPaths.filter((x) => x.startsWith('width')).length > 0;
if (query.width !== undefined || pathHasWidth) {
let lineWidth = 1;
// Get line width from query
if ('width' in query) {
lineWidth = Number(query.width);
}
// Get line width from path in query
if (pathHasWidth) {
lineWidth = Number(
splitPaths.find((x) => x.startsWith('width:')).replace('width:', ''),
);
}
// 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;
}
const pathHasStroke =
splitPaths.filter((x) => x.startsWith('stroke')).length > 0;
if (query.stroke !== undefined || pathHasStroke) {
if ('stroke' in query) {
ctx.strokeStyle = query.stroke;
}
// Path Stroke gets higher priority
if (pathHasStroke) {
ctx.strokeStyle = splitPaths
.find((x) => x.startsWith('stroke:'))
.replace('stroke:', '');
}
} else {
ctx.strokeStyle = '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);
const mapHeight = 512 * (1 << z);
const maxEdge = center[1] + h / 2;
const minEdge = center[1] - h / 2;
if (maxEdge > mapHeight) {
center[1] -= maxEdge - mapHeight;
} else if (minEdge < 0) {
center[1] -= minEdge;
}
const canvas = createCanvas(scale * w, scale * h);
const ctx = canvas.getContext('2d');
ctx.scale(scale, scale);
if (bearing) {
ctx.translate(w / 2, h / 2);
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);
}
// Draw provided paths if any
paths.forEach((path, i) => {
const pathQuery = Array.isArray(query.path) ? query.path.at(i) : query.path;
drawPath(ctx, path, query, pathQuery, z);
});
// Await drawing of markers before rendering the canvas
await drawMarkers(ctx, markers, z);
return canvas.toBuffer();
};
const calcZForBBox = (bbox, w, h, query) => {
let z = 25;
@ -608,32 +352,179 @@ const calcZForBBox = (bbox, w, h, query) => {
return z;
};
const respondImage = (
options,
item,
z,
lon,
lat,
bearing,
pitch,
width,
height,
scale,
format,
res,
overlay = null,
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 ||
Math.max(width, height) * scale > (options.maxSize || 2048) ||
width !== width ||
height !== height
) {
return res.status(400).send('Invalid size');
}
if (format === 'png' || format === 'webp') {
} else if (format === 'jpg' || format === 'jpeg') {
format = 'jpeg';
} else {
return res.status(400).send('Invalid format');
}
const tileMargin = Math.max(options.tileMargin || 0, 0);
let pool;
if (mode === 'tile' && tileMargin === 0) {
pool = item.map.renderers[scale];
} else {
pool = item.map.renderers_static[scale];
}
pool.acquire((err, renderer) => {
const mlglZ = Math.max(0, z - 1);
const params = {
zoom: mlglZ,
center: [lon, lat],
bearing: bearing,
pitch: pitch,
width: width,
height: height,
};
if (z === 0) {
params.width *= 2;
params.height *= 2;
}
if (z > 2 && tileMargin > 0) {
params.width += tileMargin * 2;
params.height += tileMargin * 2;
}
renderer.render(params, (err, data) => {
pool.release(renderer);
if (err) {
console.error(err);
return res.status(500).header('Content-Type', 'text/plain').send(err);
}
// Fix semi-transparent outlines on raw, premultiplied input
// https://github.com/maptiler/tileserver-gl/issues/350#issuecomment-477857040
for (let i = 0; i < data.length; i += 4) {
const alpha = data[i + 3];
const norm = alpha / 255;
if (alpha === 0) {
data[i] = 0;
data[i + 1] = 0;
data[i + 2] = 0;
} else {
data[i] = data[i] / norm;
data[i + 1] = data[i + 1] / norm;
data[i + 2] = data[i + 2] / norm;
}
}
const image = sharp(data, {
raw: {
width: params.width * scale,
height: params.height * scale,
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),
);
image.extract({
left: tileMargin * scale,
top: (tileMargin + yoffset) * scale,
width: width * scale,
height: height * scale,
});
}
if (z === 0) {
// HACK: when serving zoom 0, resize the 0 tile from 512 to 256
image.resize(width * scale, height * scale);
}
const composite_array = [];
if (overlay) {
composite_array.push({ input: overlay });
}
if (item.watermark) {
const canvas = renderWatermark(width, height, scale, item.watermark);
composite_array.push({ input: canvas.toBuffer() });
}
if (mode === 'static' && item.staticAttributionText) {
const canvas = renderAttribution(
width,
height,
scale,
item.staticAttributionText,
);
composite_array.push({ input: canvas.toBuffer() });
}
if (composite_array.length > 0) {
image.composite(composite_array);
}
const formatQuality = (options.formatQuality || {})[format];
if (format === 'png') {
image.png({ adaptiveFiltering: false });
} else if (format === 'jpeg') {
image.jpeg({ quality: formatQuality || 80 });
} else if (format === 'webp') {
image.webp({ quality: formatQuality || 90 });
}
image.toBuffer((err, buffer, info) => {
if (!buffer) {
return res.status(404).send('Not found');
}
res.set({
'Last-Modified': item.lastModified,
'Content-Type': `image/${format}`,
});
return res.status(200).send(buffer);
});
});
});
};
const existingFonts = {};
let maxScaleFactor = 2;
export const serve_rendered = {
init: (options, repo) => {
const fontListingPromise = new Promise((resolve, reject) => {
fs.readdir(options.paths.fonts, (err, files) => {
if (err) {
reject(err);
return;
}
for (const file of files) {
fs.stat(path.join(options.paths.fonts, file), (err, stats) => {
if (err) {
reject(err);
return;
}
if (stats.isDirectory()) {
existingFonts[path.basename(file)] = true;
}
});
}
resolve();
});
});
maxScaleFactor = Math.min(Math.floor(options.maxScaleFactor || 3), 9);
let scalePattern = '';
for (let i = 2; i <= maxScaleFactor; i++) {
@ -643,203 +534,6 @@ 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
) {
return res.status(400).send('Invalid center');
}
if (
Math.min(width, height) <= 0 ||
Math.max(width, height) * scale > (options.maxSize || 2048) ||
width !== width ||
height !== height
) {
return res.status(400).send('Invalid size');
}
if (format === 'png' || format === 'webp') {
} else if (format === 'jpg' || format === 'jpeg') {
format = 'jpeg';
} else {
return res.status(400).send('Invalid format');
}
const tileMargin = Math.max(options.tileMargin || 0, 0);
let pool;
if (opt_mode === 'tile' && tileMargin === 0) {
pool = item.map.renderers[scale];
} else {
pool = item.map.renderers_static[scale];
}
pool.acquire((err, renderer) => {
const mlglZ = Math.max(0, z - 1);
const params = {
zoom: mlglZ,
center: [lon, lat],
bearing: bearing,
pitch: pitch,
width: width,
height: height,
};
if (z === 0) {
params.width *= 2;
params.height *= 2;
}
if (z > 2 && tileMargin > 0) {
params.width += tileMargin * 2;
params.height += tileMargin * 2;
}
renderer.render(params, (err, data) => {
pool.release(renderer);
if (err) {
console.error(err);
return res
.status(500)
.header('Content-Type', 'text/plain')
.send(err);
}
// Fix semi-transparent outlines on raw, premultiplied input
// https://github.com/maptiler/tileserver-gl/issues/350#issuecomment-477857040
for (let i = 0; i < data.length; i += 4) {
const alpha = data[i + 3];
const norm = alpha / 255;
if (alpha === 0) {
data[i] = 0;
data[i + 1] = 0;
data[i + 2] = 0;
} else {
data[i] = data[i] / norm;
data[i + 1] = data[i + 1] / norm;
data[i + 2] = data[i + 2] / norm;
}
}
const image = sharp(data, {
raw: {
width: params.width * scale,
height: params.height * scale,
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),
);
image.extract({
left: tileMargin * scale,
top: (tileMargin + yoffset) * scale,
width: width * scale,
height: height * scale,
});
}
if (z === 0) {
// HACK: when serving zoom 0, resize the 0 tile from 512 to 256
image.resize(width * scale, height * scale);
}
var composite_array = [];
if (opt_overlay) {
composite_array.push({ input: opt_overlay });
}
if (item.watermark) {
const canvas = createCanvas(scale * width, scale * height);
const ctx = canvas.getContext('2d');
ctx.scale(scale, scale);
ctx.font = '10px sans-serif';
ctx.strokeWidth = '1px';
ctx.strokeStyle = 'rgba(255,255,255,.4)';
ctx.strokeText(item.watermark, 5, height - 5);
ctx.fillStyle = 'rgba(0,0,0,.4)';
ctx.fillText(item.watermark, 5, height - 5);
composite_array.push({ input: canvas.toBuffer() });
}
if (opt_mode === 'static' && item.staticAttributionText) {
const canvas = createCanvas(scale * width, scale * height);
const ctx = canvas.getContext('2d');
ctx.scale(scale, scale);
ctx.font = '10px sans-serif';
const text = item.staticAttributionText;
const textMetrics = ctx.measureText(text);
const textWidth = textMetrics.width;
const textHeight = 14;
const padding = 6;
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
ctx.fillRect(
width - textWidth - padding,
height - textHeight - padding,
textWidth + padding,
textHeight + padding,
);
ctx.fillStyle = 'rgba(0,0,0,.8)';
ctx.fillText(
item.staticAttributionText,
width - textWidth - padding / 2,
height - textHeight + 8,
);
composite_array.push({ input: canvas.toBuffer() });
}
if (composite_array.length > 0) {
image.composite(composite_array);
}
const formatQuality = (options.formatQuality || {})[format];
if (format === 'png') {
image.png({ adaptiveFiltering: false });
} else if (format === 'jpeg') {
image.jpeg({ quality: formatQuality || 80 });
} else if (format === 'webp') {
image.webp({ quality: formatQuality || 90 });
}
image.toBuffer((err, buffer, info) => {
if (!buffer) {
return res.status(404).send('Not found');
}
res.set({
'Last-Modified': item.lastModified,
'Content-Type': `image/${format}`,
});
return res.status(200).send(buffer);
});
});
});
};
app.get(
`/:id/:z(\\d+)/:x(\\d+)/:y(\\d+):scale(${scalePattern})?.:format([\\w]+)`,
(req, res, next) => {
@ -880,6 +574,7 @@ export const serve_rendered = {
z,
);
return respondImage(
options,
item,
z,
tileCenter[0],
@ -891,7 +586,6 @@ export const serve_rendered = {
scale,
format,
res,
next,
);
},
);
@ -962,6 +656,7 @@ export const serve_rendered = {
);
return respondImage(
options,
item,
z,
x,
@ -973,7 +668,6 @@ export const serve_rendered = {
scale,
format,
res,
next,
overlay,
'static',
);
@ -1043,6 +737,7 @@ export const serve_rendered = {
req.query,
);
return respondImage(
options,
item,
z,
x,
@ -1054,7 +749,6 @@ export const serve_rendered = {
scale,
format,
res,
next,
overlay,
'static',
);
@ -1177,6 +871,7 @@ export const serve_rendered = {
);
return respondImage(
options,
item,
z,
x,
@ -1188,7 +883,6 @@ export const serve_rendered = {
scale,
format,
res,
next,
overlay,
'static',
);
@ -1215,7 +909,10 @@ export const serve_rendered = {
return res.send(info);
});
return Promise.all([fontListingPromise]).then(() => app);
return listFonts(options.paths.fonts).then((fonts) => {
Object.assign(existingFonts, fonts);
return app;
});
},
add: async (options, repo, params, id, publicUrl, dataResolver) => {
const map = {
@ -1618,7 +1315,7 @@ export const serve_rendered = {
}
});
return Promise.all([renderersReadyPromise]);
return renderersReadyPromise;
},
remove: (repo, id) => {
const item = repo[id];

View File

@ -163,6 +163,35 @@ export const getFontsPbf = (
return Promise.all(queue).then((values) => glyphCompose.combine(values));
};
export const listFonts = async (fontPath) => {
const existingFonts = {};
const fontListingPromise = new Promise((resolve, reject) => {
fs.readdir(fontPath, (err, files) => {
if (err) {
reject(err);
return;
}
for (const file of files) {
fs.stat(path.join(fontPath, file), (err, stats) => {
if (err) {
reject(err);
return;
}
if (
stats.isDirectory() &&
fs.existsSync(path.join(fontPath, file, '0-255.pbf'))
) {
existingFonts[path.basename(file)] = true;
}
});
}
resolve();
});
});
await fontListingPromise;
return existingFonts;
};
export const isValidHttpUrl = (string) => {
let url;

View File

@ -24,6 +24,5 @@ after(function () {
console.log('global teardown');
global.server.close(function () {
console.log('Done');
process.exit();
});
});