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:
parent
cef150431b
commit
526766c8f4
7 changed files with 521 additions and 515 deletions
|
@ -6,7 +6,7 @@
|
||||||
"bin": "src/main.js",
|
"bin": "src/main.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha test/**.js --timeout 10000",
|
"test": "mocha test/**.js --timeout 10000 --exit",
|
||||||
"lint:yml": "yamllint --schema=CORE_SCHEMA *.{yml,yaml}",
|
"lint:yml": "yamllint --schema=CORE_SCHEMA *.{yml,yaml}",
|
||||||
"lint:js": "npm run lint:eslint && npm run lint:prettier",
|
"lint:js": "npm run lint:eslint && npm run lint:prettier",
|
||||||
"lint:js:fix": "npm run lint:eslint:fix && npm run lint:prettier:fix",
|
"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: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": "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",
|
"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"
|
"prepare": "node -e \"if (process.env.NODE_ENV !== 'production'){ process.exit(1) } \" || husky install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
var options = {
|
const options = {
|
||||||
timeout: 2000,
|
timeout: 2000,
|
||||||
};
|
};
|
||||||
var url = 'http://localhost:8080/health';
|
const url = 'http://localhost:8080/health';
|
||||||
var request = http.request(url, options, (res) => {
|
const request = http.request(url, options, (res) => {
|
||||||
console.log(`STATUS: ${res.statusCode}`);
|
console.log(`STATUS: ${res.statusCode}`);
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|
303
src/render.js
Normal file
303
src/render.js
Normal 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;
|
||||||
|
};
|
|
@ -1,10 +1,8 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import express from 'express';
|
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) => {
|
export const serve_font = (options, allowedFonts) => {
|
||||||
const app = express().disable('x-powered-by');
|
const app = express().disable('x-powered-by');
|
||||||
|
@ -14,29 +12,6 @@ export const serve_font = (options, allowedFonts) => {
|
||||||
const fontPath = options.paths.fonts;
|
const fontPath = options.paths.fonts;
|
||||||
|
|
||||||
const existingFonts = {};
|
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) => {
|
app.get('/fonts/:fontstack/:range([\\d]+-[\\d]+).pbf', (req, res, next) => {
|
||||||
const fontstack = decodeURI(req.params.fontstack);
|
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;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,6 @@ import url from 'url';
|
||||||
import util from 'util';
|
import util from 'util';
|
||||||
import zlib from 'zlib';
|
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 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 clone from 'clone';
|
||||||
import Color from 'color';
|
import Color from 'color';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
@ -20,6 +19,7 @@ import proj4 from 'proj4';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import {
|
import {
|
||||||
getFontsPbf,
|
getFontsPbf,
|
||||||
|
listFonts,
|
||||||
getTileUrls,
|
getTileUrls,
|
||||||
isValidHttpUrl,
|
isValidHttpUrl,
|
||||||
fixTileJSONCenter,
|
fixTileJSONCenter,
|
||||||
|
@ -29,6 +29,7 @@ import {
|
||||||
GetPMtilesInfo,
|
GetPMtilesInfo,
|
||||||
GetPMtilesTile,
|
GetPMtilesTile,
|
||||||
} from './pmtiles_adapter.js';
|
} from './pmtiles_adapter.js';
|
||||||
|
import { renderOverlay, renderWatermark, renderAttribution } from './render.js';
|
||||||
|
|
||||||
const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+.?\\d+)';
|
const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+.?\\d+)';
|
||||||
const PATH_PATTERN =
|
const PATH_PATTERN =
|
||||||
|
@ -330,263 +331,6 @@ const extractMarkersFromQuery = (query, options, transformer) => {
|
||||||
return markers;
|
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) => {
|
const calcZForBBox = (bbox, w, h, query) => {
|
||||||
let z = 25;
|
let z = 25;
|
||||||
|
|
||||||
|
@ -608,42 +352,8 @@ const calcZForBBox = (bbox, w, h, query) => {
|
||||||
return z;
|
return z;
|
||||||
};
|
};
|
||||||
|
|
||||||
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++) {
|
|
||||||
scalePattern += i.toFixed();
|
|
||||||
}
|
|
||||||
scalePattern = `@[${scalePattern}]x`;
|
|
||||||
|
|
||||||
const app = express().disable('x-powered-by');
|
|
||||||
|
|
||||||
const respondImage = (
|
const respondImage = (
|
||||||
|
options,
|
||||||
item,
|
item,
|
||||||
z,
|
z,
|
||||||
lon,
|
lon,
|
||||||
|
@ -655,9 +365,8 @@ export const serve_rendered = {
|
||||||
scale,
|
scale,
|
||||||
format,
|
format,
|
||||||
res,
|
res,
|
||||||
next,
|
overlay = null,
|
||||||
opt_overlay,
|
mode = 'tile',
|
||||||
opt_mode = 'tile',
|
|
||||||
) => {
|
) => {
|
||||||
if (
|
if (
|
||||||
Math.abs(lon) > 180 ||
|
Math.abs(lon) > 180 ||
|
||||||
|
@ -686,7 +395,7 @@ export const serve_rendered = {
|
||||||
|
|
||||||
const tileMargin = Math.max(options.tileMargin || 0, 0);
|
const tileMargin = Math.max(options.tileMargin || 0, 0);
|
||||||
let pool;
|
let pool;
|
||||||
if (opt_mode === 'tile' && tileMargin === 0) {
|
if (mode === 'tile' && tileMargin === 0) {
|
||||||
pool = item.map.renderers[scale];
|
pool = item.map.renderers[scale];
|
||||||
} else {
|
} else {
|
||||||
pool = item.map.renderers_static[scale];
|
pool = item.map.renderers_static[scale];
|
||||||
|
@ -716,10 +425,7 @@ export const serve_rendered = {
|
||||||
pool.release(renderer);
|
pool.release(renderer);
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return res
|
return res.status(500).header('Content-Type', 'text/plain').send(err);
|
||||||
.status(500)
|
|
||||||
.header('Content-Type', 'text/plain')
|
|
||||||
.send(err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix semi-transparent outlines on raw, premultiplied input
|
// Fix semi-transparent outlines on raw, premultiplied input
|
||||||
|
@ -765,48 +471,22 @@ export const serve_rendered = {
|
||||||
image.resize(width * scale, height * scale);
|
image.resize(width * scale, height * scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
var composite_array = [];
|
const composite_array = [];
|
||||||
if (opt_overlay) {
|
if (overlay) {
|
||||||
composite_array.push({ input: opt_overlay });
|
composite_array.push({ input: overlay });
|
||||||
}
|
}
|
||||||
if (item.watermark) {
|
if (item.watermark) {
|
||||||
const canvas = createCanvas(scale * width, scale * height);
|
const canvas = renderWatermark(width, height, scale, item.watermark);
|
||||||
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() });
|
composite_array.push({ input: canvas.toBuffer() });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opt_mode === 'static' && item.staticAttributionText) {
|
if (mode === 'static' && item.staticAttributionText) {
|
||||||
const canvas = createCanvas(scale * width, scale * height);
|
const canvas = renderAttribution(
|
||||||
const ctx = canvas.getContext('2d');
|
width,
|
||||||
ctx.scale(scale, scale);
|
height,
|
||||||
|
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,
|
item.staticAttributionText,
|
||||||
width - textWidth - padding / 2,
|
|
||||||
height - textHeight + 8,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
composite_array.push({ input: canvas.toBuffer() });
|
composite_array.push({ input: canvas.toBuffer() });
|
||||||
|
@ -840,6 +520,20 @@ export const serve_rendered = {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const existingFonts = {};
|
||||||
|
let maxScaleFactor = 2;
|
||||||
|
|
||||||
|
export const serve_rendered = {
|
||||||
|
init: (options, repo) => {
|
||||||
|
maxScaleFactor = Math.min(Math.floor(options.maxScaleFactor || 3), 9);
|
||||||
|
let scalePattern = '';
|
||||||
|
for (let i = 2; i <= maxScaleFactor; i++) {
|
||||||
|
scalePattern += i.toFixed();
|
||||||
|
}
|
||||||
|
scalePattern = `@[${scalePattern}]x`;
|
||||||
|
|
||||||
|
const app = express().disable('x-powered-by');
|
||||||
|
|
||||||
app.get(
|
app.get(
|
||||||
`/:id/:z(\\d+)/:x(\\d+)/:y(\\d+):scale(${scalePattern})?.:format([\\w]+)`,
|
`/:id/:z(\\d+)/:x(\\d+)/:y(\\d+):scale(${scalePattern})?.:format([\\w]+)`,
|
||||||
(req, res, next) => {
|
(req, res, next) => {
|
||||||
|
@ -880,6 +574,7 @@ export const serve_rendered = {
|
||||||
z,
|
z,
|
||||||
);
|
);
|
||||||
return respondImage(
|
return respondImage(
|
||||||
|
options,
|
||||||
item,
|
item,
|
||||||
z,
|
z,
|
||||||
tileCenter[0],
|
tileCenter[0],
|
||||||
|
@ -891,7 +586,6 @@ export const serve_rendered = {
|
||||||
scale,
|
scale,
|
||||||
format,
|
format,
|
||||||
res,
|
res,
|
||||||
next,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -962,6 +656,7 @@ export const serve_rendered = {
|
||||||
);
|
);
|
||||||
|
|
||||||
return respondImage(
|
return respondImage(
|
||||||
|
options,
|
||||||
item,
|
item,
|
||||||
z,
|
z,
|
||||||
x,
|
x,
|
||||||
|
@ -973,7 +668,6 @@ export const serve_rendered = {
|
||||||
scale,
|
scale,
|
||||||
format,
|
format,
|
||||||
res,
|
res,
|
||||||
next,
|
|
||||||
overlay,
|
overlay,
|
||||||
'static',
|
'static',
|
||||||
);
|
);
|
||||||
|
@ -1043,6 +737,7 @@ export const serve_rendered = {
|
||||||
req.query,
|
req.query,
|
||||||
);
|
);
|
||||||
return respondImage(
|
return respondImage(
|
||||||
|
options,
|
||||||
item,
|
item,
|
||||||
z,
|
z,
|
||||||
x,
|
x,
|
||||||
|
@ -1054,7 +749,6 @@ export const serve_rendered = {
|
||||||
scale,
|
scale,
|
||||||
format,
|
format,
|
||||||
res,
|
res,
|
||||||
next,
|
|
||||||
overlay,
|
overlay,
|
||||||
'static',
|
'static',
|
||||||
);
|
);
|
||||||
|
@ -1177,6 +871,7 @@ export const serve_rendered = {
|
||||||
);
|
);
|
||||||
|
|
||||||
return respondImage(
|
return respondImage(
|
||||||
|
options,
|
||||||
item,
|
item,
|
||||||
z,
|
z,
|
||||||
x,
|
x,
|
||||||
|
@ -1188,7 +883,6 @@ export const serve_rendered = {
|
||||||
scale,
|
scale,
|
||||||
format,
|
format,
|
||||||
res,
|
res,
|
||||||
next,
|
|
||||||
overlay,
|
overlay,
|
||||||
'static',
|
'static',
|
||||||
);
|
);
|
||||||
|
@ -1215,7 +909,10 @@ export const serve_rendered = {
|
||||||
return res.send(info);
|
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) => {
|
add: async (options, repo, params, id, publicUrl, dataResolver) => {
|
||||||
const map = {
|
const map = {
|
||||||
|
@ -1618,7 +1315,7 @@ export const serve_rendered = {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all([renderersReadyPromise]);
|
return renderersReadyPromise;
|
||||||
},
|
},
|
||||||
remove: (repo, id) => {
|
remove: (repo, id) => {
|
||||||
const item = repo[id];
|
const item = repo[id];
|
||||||
|
|
29
src/utils.js
29
src/utils.js
|
@ -163,6 +163,35 @@ export const getFontsPbf = (
|
||||||
return Promise.all(queue).then((values) => glyphCompose.combine(values));
|
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) => {
|
export const isValidHttpUrl = (string) => {
|
||||||
let url;
|
let url;
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,5 @@ after(function () {
|
||||||
console.log('global teardown');
|
console.log('global teardown');
|
||||||
global.server.close(function () {
|
global.server.close(function () {
|
||||||
console.log('Done');
|
console.log('Done');
|
||||||
process.exit();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue