Upgrade Express to v5 +Canvas v3 + code cleanup (#1429)
* first attempt to upgrade express to v5 Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * try to fix https://github.com/maptiler/tileserver-gl/issues/1411 Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * cleanup server.js Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * cleanup serve_font.js Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * cleanup sever_rendered.js Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * cleanup server_data.js Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * cleanup serve_style Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * Update serve_style.js Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * Move UV_THREADPOOL_SIZE to main thred Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * cleanup utils.js Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * Use common app.get for images and static images Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * add allowedTileSizes and option Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * cleanup error responses Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * fix /style/id.json with next('route') Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * improve sprite path Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * add parseFloadts around zxy Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * simplify server_data Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * move tile fetch and add fix verbose logging Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * add Handling request to verbose logging Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * first attempt to upgrade express to v5 Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * try to fix https://github.com/maptiler/tileserver-gl/issues/1411 Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * cleanup server.js Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * cleanup serve_font.js Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * cleanup sever_rendered.js Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * cleanup server_data.js Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * cleanup serve_style Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * Update serve_style.js Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * Move UV_THREADPOOL_SIZE to main thred Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * cleanup utils.js Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * Use common app.get for images and static images Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * add allowedTileSizes and option Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * cleanup error responses Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * fix /style/id.json with next('route') Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * improve sprite path Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * add parseFloadts around zxy Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * simplify server_data Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * move tile fetch and add fix verbose logging Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * add Handling request to verbose logging Co-Authored-By: Andrew Calcutt <acalcutt@techidiots.net> * merge elevation changes * lint format * add verbose logging, improve headers * try to fix codeql Information exposure through a stack trace * test * all tests passing * cleanup unneeded changes * cleanup * try to fix codeql error * font fixes * fix tile size issue * try to improve scale + codeql * codeql for sprite logging * codeql serve fonts * codeql fixes * fix failing test with multiple fonts * Update serve_font.js * codeql * codeql * codeql * Update utils.js * codeql * codeql * codeql * codeql * codeql sanitize * Update serve_font.js * Update serve_font.js * remove useless assignment * move isGzipped * add if-modified-since and cache-control * use consistent cache control * reformat * codeql * codeql * codeql * codeql * codeql * codeql * codeql * Update serve_font.js * Update serve_font.js * Update serve_font.js * Update serve_style.js * Update serve_style.js * Update serve_style.js * Revert "Update serve_style.js" This reverts commite0574b1887
. * Revert "Update serve_style.js" This reverts commitb1e1d72f25
. * Revert "Update serve_style.js" This reverts commit0f3629c752
. * Add readFile function * use readFile, add path.normalize * Update serve_rendered.js * simplify input checking * Update utils.js * codeql * Revert "codeql" This reverts commite18874fda0
. * Revert "Update utils.js" This reverts commit5de617dfe2
. * Revert "simplify input checking" This reverts commit62a3212629
. * move allowed functions to utils.js * use xy[0],xy[1], * uprade canvas per https://github.com/maptiler/tileserver-gl/issues/1433 * make font regex less restrictive * fix regex error * Add version and changelog * Update CHANGELOG.md * Update CHANGELOG.md
This commit is contained in:
parent
3abbb39633
commit
97be9db6b7
14 changed files with 2271 additions and 1198 deletions
|
@ -1,5 +1,10 @@
|
||||||
# tileserver-gl changelog
|
# tileserver-gl changelog
|
||||||
|
|
||||||
|
## 5.1.0-pre.0
|
||||||
|
* Upgrade Express to v5 + Canvas to v3 + code cleanup (https://github.com/maptiler/tileserver-gl/pull/1429) by @acalcutt
|
||||||
|
* Terrain Preview and simple Elevation Query (https://github.com/maptiler/tileserver-gl/pull/1425 and https://github.com/maptiler/tileserver-gl/pull/1432) by @okimiko
|
||||||
|
* add progressive rendering option for static jpeg images (#1397) by @samuel-git
|
||||||
|
|
||||||
## 5.0.0
|
## 5.0.0
|
||||||
* Update Maplibre-Native to [v6.0.0](https://github.com/maplibre/maplibre-native/releases/tag/node-v6.0.0) release by @acalcutt in https://github.com/maptiler/tileserver-gl/pull/1376 and @dependabot in https://github.com/maptiler/tileserver-gl/pull/1381
|
* Update Maplibre-Native to [v6.0.0](https://github.com/maplibre/maplibre-native/releases/tag/node-v6.0.0) release by @acalcutt in https://github.com/maptiler/tileserver-gl/pull/1376 and @dependabot in https://github.com/maptiler/tileserver-gl/pull/1381
|
||||||
* This first release that use Metal for rendering instead of OpenGL (ES) for macOS.
|
* This first release that use Metal for rendering instead of OpenGL (ES) for macOS.
|
||||||
|
|
854
package-lock.json
generated
854
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "tileserver-gl",
|
"name": "tileserver-gl",
|
||||||
"version": "5.0.0",
|
"version": "5.1.0-pre.0",
|
||||||
"description": "Map tile server for JSON GL styles - vector and server side generated raster tiles",
|
"description": "Map tile server for JSON GL styles - vector and server side generated raster tiles",
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
"bin": "src/main.js",
|
"bin": "src/main.js",
|
||||||
|
@ -28,13 +28,13 @@
|
||||||
"@sindresorhus/fnv1a": "3.1.0",
|
"@sindresorhus/fnv1a": "3.1.0",
|
||||||
"advanced-pool": "0.3.3",
|
"advanced-pool": "0.3.3",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"canvas": "2.11.2",
|
"canvas": "3.0.1",
|
||||||
"chokidar": "3.6.0",
|
"chokidar": "3.6.0",
|
||||||
"clone": "2.1.2",
|
"clone": "2.1.2",
|
||||||
"color": "4.2.3",
|
"color": "4.2.3",
|
||||||
"commander": "12.1.0",
|
"commander": "12.1.0",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"express": "4.19.2",
|
"express": "5.0.1",
|
||||||
"handlebars": "4.7.8",
|
"handlebars": "4.7.8",
|
||||||
"http-shutdown": "1.2.2",
|
"http-shutdown": "1.2.2",
|
||||||
"morgan": "1.10.0",
|
"morgan": "1.10.0",
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
import os from 'os';
|
||||||
|
|
||||||
|
const envSize = parseInt(process.env.UV_THREADPOOL_SIZE, 10);
|
||||||
|
process.env.UV_THREADPOOL_SIZE = Math.ceil(
|
||||||
|
Math.max(4, isNaN(envSize) ? os.cpus().length * 1.5 : envSize),
|
||||||
|
);
|
||||||
|
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import fsp from 'node:fs/promises';
|
import fsp from 'node:fs/promises';
|
||||||
|
|
|
@ -11,30 +11,62 @@ import SphericalMercator from '@mapbox/sphericalmercator';
|
||||||
import { Image, createCanvas } from 'canvas';
|
import { Image, createCanvas } from 'canvas';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
|
|
||||||
import { fixTileJSONCenter, getTileUrls, isValidHttpUrl } from './utils.js';
|
|
||||||
import {
|
import {
|
||||||
getPMtilesInfo,
|
fixTileJSONCenter,
|
||||||
getPMtilesTile,
|
getTileUrls,
|
||||||
openPMtiles,
|
isValidHttpUrl,
|
||||||
} from './pmtiles_adapter.js';
|
fetchTileData,
|
||||||
|
} from './utils.js';
|
||||||
|
import { getPMtilesInfo, openPMtiles } from './pmtiles_adapter.js';
|
||||||
import { gunzipP, gzipP } from './promises.js';
|
import { gunzipP, gzipP } from './promises.js';
|
||||||
import { openMbTilesWrapper } from './mbtiles_wrapper.js';
|
import { openMbTilesWrapper } from './mbtiles_wrapper.js';
|
||||||
|
|
||||||
export const serve_data = {
|
export const serve_data = {
|
||||||
init: (options, repo) => {
|
/**
|
||||||
|
* Initializes the serve_data module.
|
||||||
|
* @param {object} options Configuration options.
|
||||||
|
* @param {object} repo Repository object.
|
||||||
|
* @param {object} programOpts - An object containing the program options
|
||||||
|
* @returns {express.Application} The initialized Express application.
|
||||||
|
*/
|
||||||
|
init: function (options, repo, programOpts) {
|
||||||
|
const { verbose } = programOpts;
|
||||||
const app = express().disable('x-powered-by');
|
const app = express().disable('x-powered-by');
|
||||||
|
|
||||||
app.get(
|
/**
|
||||||
'/:id/:z(\\d+)/:x(\\d+)/:y(\\d+).:format([\\w.]+)',
|
* Handles requests for tile data, responding with the tile image.
|
||||||
async (req, res, next) => {
|
* @param {object} req - Express request object.
|
||||||
|
* @param {object} res - Express response object.
|
||||||
|
* @param {string} req.params.id - ID of the tile.
|
||||||
|
* @param {string} req.params.z - Z coordinate of the tile.
|
||||||
|
* @param {string} req.params.x - X coordinate of the tile.
|
||||||
|
* @param {string} req.params.y - Y coordinate of the tile.
|
||||||
|
* @param {string} req.params.format - Format of the tile.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
app.get('/:id/:z/:x/:y.:format', async (req, res) => {
|
||||||
|
if (verbose) {
|
||||||
|
console.log(
|
||||||
|
`Handling tile request for: /data/%s/%s/%s/%s.%s`,
|
||||||
|
String(req.params.id).replace(/\n|\r/g, ''),
|
||||||
|
String(req.params.z).replace(/\n|\r/g, ''),
|
||||||
|
String(req.params.x).replace(/\n|\r/g, ''),
|
||||||
|
String(req.params.y).replace(/\n|\r/g, ''),
|
||||||
|
String(req.params.format).replace(/\n|\r/g, ''),
|
||||||
|
);
|
||||||
|
}
|
||||||
const item = repo[req.params.id];
|
const item = repo[req.params.id];
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return res.sendStatus(404);
|
return res.sendStatus(404);
|
||||||
}
|
}
|
||||||
const tileJSONFormat = item.tileJSON.format;
|
const tileJSONFormat = item.tileJSON.format;
|
||||||
const z = req.params.z | 0;
|
const z = parseInt(req.params.z, 10);
|
||||||
const x = req.params.x | 0;
|
const x = parseInt(req.params.x, 10);
|
||||||
const y = req.params.y | 0;
|
const y = parseInt(req.params.y, 10);
|
||||||
|
if (isNaN(z) || isNaN(x) || isNaN(y)) {
|
||||||
|
return res.status(404).send('Invalid Tile');
|
||||||
|
}
|
||||||
|
|
||||||
let format = req.params.format;
|
let format = req.params.format;
|
||||||
if (format === options.pbfAlias) {
|
if (format === options.pbfAlias) {
|
||||||
format = 'pbf';
|
format = 'pbf';
|
||||||
|
@ -47,7 +79,6 @@ export const serve_data = {
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
z < item.tileJSON.minzoom ||
|
z < item.tileJSON.minzoom ||
|
||||||
0 ||
|
|
||||||
x < 0 ||
|
x < 0 ||
|
||||||
y < 0 ||
|
y < 0 ||
|
||||||
z > item.tileJSON.maxzoom ||
|
z > item.tileJSON.maxzoom ||
|
||||||
|
@ -56,18 +87,37 @@ export const serve_data = {
|
||||||
) {
|
) {
|
||||||
return res.status(404).send('Out of bounds');
|
return res.status(404).send('Out of bounds');
|
||||||
}
|
}
|
||||||
if (item.sourceType === 'pmtiles') {
|
|
||||||
let tileinfo = await getPMtilesTile(item.source, z, x, y);
|
const fetchTile = await fetchTileData(
|
||||||
if (tileinfo == undefined || tileinfo.data == undefined) {
|
item.source,
|
||||||
return res.status(404).send('Not found');
|
item.sourceType,
|
||||||
} else {
|
z,
|
||||||
let data = tileinfo.data;
|
x,
|
||||||
let headers = tileinfo.header;
|
y,
|
||||||
|
);
|
||||||
|
if (fetchTile == null) return res.status(204).send();
|
||||||
|
|
||||||
|
let data = fetchTile.data;
|
||||||
|
let headers = fetchTile.headers;
|
||||||
|
let isGzipped = data.slice(0, 2).indexOf(Buffer.from([0x1f, 0x8b])) === 0;
|
||||||
|
|
||||||
if (tileJSONFormat === 'pbf') {
|
if (tileJSONFormat === 'pbf') {
|
||||||
if (options.dataDecoratorFunc) {
|
if (options.dataDecoratorFunc) {
|
||||||
data = options.dataDecoratorFunc(id, 'data', data, z, x, y);
|
if (isGzipped) {
|
||||||
|
data = await gunzipP(data);
|
||||||
|
isGzipped = false;
|
||||||
|
}
|
||||||
|
data = options.dataDecoratorFunc(
|
||||||
|
req.params.id,
|
||||||
|
'data',
|
||||||
|
data,
|
||||||
|
z,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format === 'pbf') {
|
if (format === 'pbf') {
|
||||||
headers['Content-Type'] = 'application/x-protobuf';
|
headers['Content-Type'] = 'application/x-protobuf';
|
||||||
} else if (format === 'geojson') {
|
} else if (format === 'geojson') {
|
||||||
|
@ -88,68 +138,9 @@ export const serve_data = {
|
||||||
}
|
}
|
||||||
data = JSON.stringify(geojson);
|
data = JSON.stringify(geojson);
|
||||||
}
|
}
|
||||||
delete headers['ETag']; // do not trust the tile ETag -- regenerate
|
if (headers) {
|
||||||
headers['Content-Encoding'] = 'gzip';
|
delete headers['ETag'];
|
||||||
res.set(headers);
|
|
||||||
|
|
||||||
data = await gzipP(data);
|
|
||||||
|
|
||||||
return res.status(200).send(data);
|
|
||||||
}
|
}
|
||||||
} else if (item.sourceType === 'mbtiles') {
|
|
||||||
item.source.getTile(z, x, y, async (err, data, headers) => {
|
|
||||||
let isGzipped;
|
|
||||||
if (err) {
|
|
||||||
if (/does not exist/.test(err.message)) {
|
|
||||||
return res.status(204).send();
|
|
||||||
} else {
|
|
||||||
return res
|
|
||||||
.status(500)
|
|
||||||
.header('Content-Type', 'text/plain')
|
|
||||||
.send(err.message);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (data == null) {
|
|
||||||
return res.status(404).send('Not found');
|
|
||||||
} else {
|
|
||||||
if (tileJSONFormat === 'pbf') {
|
|
||||||
isGzipped =
|
|
||||||
data.slice(0, 2).indexOf(Buffer.from([0x1f, 0x8b])) === 0;
|
|
||||||
if (options.dataDecoratorFunc) {
|
|
||||||
if (isGzipped) {
|
|
||||||
data = await gunzipP(data);
|
|
||||||
isGzipped = false;
|
|
||||||
}
|
|
||||||
data = options.dataDecoratorFunc(id, 'data', data, z, x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (format === 'pbf') {
|
|
||||||
headers['Content-Type'] = 'application/x-protobuf';
|
|
||||||
} else if (format === 'geojson') {
|
|
||||||
headers['Content-Type'] = 'application/json';
|
|
||||||
|
|
||||||
if (isGzipped) {
|
|
||||||
data = await gunzipP(data);
|
|
||||||
isGzipped = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tile = new VectorTile(new Pbf(data));
|
|
||||||
const geojson = {
|
|
||||||
type: 'FeatureCollection',
|
|
||||||
features: [],
|
|
||||||
};
|
|
||||||
for (const layerName in tile.layers) {
|
|
||||||
const layer = tile.layers[layerName];
|
|
||||||
for (let i = 0; i < layer.length; i++) {
|
|
||||||
const feature = layer.feature(i);
|
|
||||||
const featureGeoJSON = feature.toGeoJSON(x, y, z);
|
|
||||||
featureGeoJSON.properties.layer = layerName;
|
|
||||||
geojson.features.push(featureGeoJSON);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data = JSON.stringify(geojson);
|
|
||||||
}
|
|
||||||
delete headers['ETag']; // do not trust the tile ETag -- regenerate
|
|
||||||
headers['Content-Encoding'] = 'gzip';
|
headers['Content-Encoding'] = 'gzip';
|
||||||
res.set(headers);
|
res.set(headers);
|
||||||
|
|
||||||
|
@ -158,32 +149,40 @@ export const serve_data = {
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send(data);
|
return res.status(200).send(data);
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
app.get(
|
/**
|
||||||
'^/:id/elevation/:z([0-9]+)/:x([-.0-9]+)/:y([-.0-9]+)',
|
* Handles requests for elevation data.
|
||||||
async (req, res, next) => {
|
* @param {object} req - Express request object.
|
||||||
|
* @param {object} res - Express response object.
|
||||||
|
* @param {string} req.params.id - ID of the elevation data.
|
||||||
|
* @param {string} req.params.z - Z coordinate of the tile.
|
||||||
|
* @param {string} req.params.x - X coordinate of the tile (either integer or float).
|
||||||
|
* @param {string} req.params.y - Y coordinate of the tile (either integer or float).
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
app.get('/:id/elevation/:z/:x/:y', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
if (verbose) {
|
||||||
|
console.log(
|
||||||
|
`Handling elevation request for: /data/%s/elevation/%s/%s/%s`,
|
||||||
|
String(req.params.id).replace(/\n|\r/g, ''),
|
||||||
|
String(req.params.z).replace(/\n|\r/g, ''),
|
||||||
|
String(req.params.x).replace(/\n|\r/g, ''),
|
||||||
|
String(req.params.y).replace(/\n|\r/g, ''),
|
||||||
|
);
|
||||||
|
}
|
||||||
const item = repo?.[req.params.id];
|
const item = repo?.[req.params.id];
|
||||||
if (!item) return res.sendStatus(404);
|
if (!item) return res.sendStatus(404);
|
||||||
if (!item.source) return res.status(404).send('Missing source');
|
if (!item.source) return res.status(404).send('Missing source');
|
||||||
if (!item.tileJSON) return res.status(404).send('Missing tileJSON');
|
if (!item.tileJSON) return res.status(404).send('Missing tileJSON');
|
||||||
if (!item.sourceType)
|
if (!item.sourceType) return res.status(404).send('Missing sourceType');
|
||||||
return res.status(404).send('Missing sourceType');
|
|
||||||
|
|
||||||
const { source, tileJSON, sourceType } = item;
|
const { source, tileJSON, sourceType } = item;
|
||||||
|
|
||||||
if (sourceType !== 'pmtiles' && sourceType !== 'mbtiles') {
|
if (sourceType !== 'pmtiles' && sourceType !== 'mbtiles') {
|
||||||
return res
|
return res
|
||||||
.status(400)
|
.status(400)
|
||||||
.send('Invalid sourceType. Must be pmtiles or mbtiles.');
|
.send('Invalid sourceType. Must be pmtiles or mbtiles.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const encoding = tileJSON?.encoding;
|
const encoding = tileJSON?.encoding;
|
||||||
if (encoding == null) {
|
if (encoding == null) {
|
||||||
return res.status(400).send('Missing tileJSON.encoding');
|
return res.status(400).send('Missing tileJSON.encoding');
|
||||||
|
@ -192,22 +191,18 @@ export const serve_data = {
|
||||||
.status(400)
|
.status(400)
|
||||||
.send('Invalid encoding. Must be terrarium or mapbox.');
|
.send('Invalid encoding. Must be terrarium or mapbox.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const format = tileJSON?.format;
|
const format = tileJSON?.format;
|
||||||
if (format == null) {
|
if (format == null) {
|
||||||
return res.status(400).send('Missing tileJSON.format');
|
return res.status(400).send('Missing tileJSON.format');
|
||||||
} else if (format !== 'webp' && format !== 'png') {
|
} else if (format !== 'webp' && format !== 'png') {
|
||||||
return res.status(400).send('Invalid format. Must be webp or png.');
|
return res.status(400).send('Invalid format. Must be webp or png.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const z = parseInt(req.params.z, 10);
|
const z = parseInt(req.params.z, 10);
|
||||||
const x = parseFloat(req.params.x);
|
const x = parseFloat(req.params.x);
|
||||||
const y = parseFloat(req.params.y);
|
const y = parseFloat(req.params.y);
|
||||||
|
|
||||||
if (tileJSON.minzoom == null || tileJSON.maxzoom == null) {
|
if (tileJSON.minzoom == null || tileJSON.maxzoom == null) {
|
||||||
return res.status(404).send(JSON.stringify(tileJSON));
|
return res.status(404).send(JSON.stringify(tileJSON));
|
||||||
}
|
}
|
||||||
|
|
||||||
const TILE_SIZE = tileJSON.tileSize || 512;
|
const TILE_SIZE = tileJSON.tileSize || 512;
|
||||||
let bbox;
|
let bbox;
|
||||||
let xy;
|
let xy;
|
||||||
|
@ -216,7 +211,6 @@ export const serve_data = {
|
||||||
if (Number.isInteger(x) && Number.isInteger(y)) {
|
if (Number.isInteger(x) && Number.isInteger(y)) {
|
||||||
const intX = parseInt(req.params.x, 10);
|
const intX = parseInt(req.params.x, 10);
|
||||||
const intY = parseInt(req.params.y, 10);
|
const intY = parseInt(req.params.y, 10);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
zoom < tileJSON.minzoom ||
|
zoom < tileJSON.minzoom ||
|
||||||
zoom > tileJSON.maxzoom ||
|
zoom > tileJSON.maxzoom ||
|
||||||
|
@ -237,39 +231,27 @@ export const serve_data = {
|
||||||
if (zoom > tileJSON.maxzoom) {
|
if (zoom > tileJSON.maxzoom) {
|
||||||
zoom = tileJSON.maxzoom;
|
zoom = tileJSON.maxzoom;
|
||||||
}
|
}
|
||||||
|
|
||||||
bbox = [x, y, x + 0.1, y + 0.1];
|
bbox = [x, y, x + 0.1, y + 0.1];
|
||||||
const { minX, minY } = new SphericalMercator().xyz(bbox, zoom);
|
const { minX, minY } = new SphericalMercator().xyz(bbox, zoom);
|
||||||
xy = [minX, minY];
|
xy = [minX, minY];
|
||||||
}
|
}
|
||||||
|
|
||||||
let data;
|
const fetchTile = await fetchTileData(
|
||||||
if (sourceType === 'pmtiles') {
|
source,
|
||||||
const tileinfo = await getPMtilesTile(source, zoom, xy[0], xy[1]);
|
sourceType,
|
||||||
if (!tileinfo?.data) return res.status(204).send();
|
zoom,
|
||||||
data = tileinfo.data;
|
xy[0],
|
||||||
} else {
|
xy[1],
|
||||||
data = await new Promise((resolve, reject) => {
|
);
|
||||||
source.getTile(zoom, xy[0], xy[1], (err, tileData) => {
|
if (fetchTile == null) return res.status(204).send();
|
||||||
if (err) {
|
|
||||||
return /does not exist/.test(err.message)
|
|
||||||
? resolve(null)
|
|
||||||
: reject(err);
|
|
||||||
}
|
|
||||||
resolve(tileData);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (data == null) return res.status(204).send();
|
|
||||||
if (!data) return res.status(404).send('Not found');
|
|
||||||
|
|
||||||
|
let data = fetchTile.data;
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
await new Promise(async (resolve, reject) => {
|
await new Promise(async (resolve, reject) => {
|
||||||
image.onload = async () => {
|
image.onload = async () => {
|
||||||
const canvas = createCanvas(TILE_SIZE, TILE_SIZE);
|
const canvas = createCanvas(TILE_SIZE, TILE_SIZE);
|
||||||
const context = canvas.getContext('2d');
|
const context = canvas.getContext('2d');
|
||||||
context.drawImage(image, 0, 0);
|
context.drawImage(image, 0, 0);
|
||||||
|
|
||||||
const long = bbox[0];
|
const long = bbox[0];
|
||||||
const lat = bbox[1];
|
const lat = bbox[1];
|
||||||
|
|
||||||
|
@ -279,7 +261,6 @@ export const serve_data = {
|
||||||
// Truncating to 0.9999 effectively limits latitude to 89.189. This is
|
// Truncating to 0.9999 effectively limits latitude to 89.189. This is
|
||||||
// about a third of a tile past the edge of the world tile.
|
// about a third of a tile past the edge of the world tile.
|
||||||
siny = Math.min(Math.max(siny, -0.9999), 0.9999);
|
siny = Math.min(Math.max(siny, -0.9999), 0.9999);
|
||||||
|
|
||||||
const xWorld = TILE_SIZE * (0.5 + long / 360);
|
const xWorld = TILE_SIZE * (0.5 + long / 360);
|
||||||
const yWorld =
|
const yWorld =
|
||||||
TILE_SIZE *
|
TILE_SIZE *
|
||||||
|
@ -292,31 +273,26 @@ export const serve_data = {
|
||||||
|
|
||||||
const xPixel = Math.floor(xWorld * scale) - xTile * TILE_SIZE;
|
const xPixel = Math.floor(xWorld * scale) - xTile * TILE_SIZE;
|
||||||
const yPixel = Math.floor(yWorld * scale) - yTile * TILE_SIZE;
|
const yPixel = Math.floor(yWorld * scale) - yTile * TILE_SIZE;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
xPixel < 0 ||
|
xPixel < 0 ||
|
||||||
yPixel < 0 ||
|
yPixel < 0 ||
|
||||||
xPixel >= TILE_SIZE ||
|
xPixel >= TILE_SIZE ||
|
||||||
yPixel >= TILE_SIZE
|
yPixel >= TILE_SIZE
|
||||||
) {
|
) {
|
||||||
return reject('Pixel is out of bounds');
|
return reject('Out of bounds Pixel');
|
||||||
}
|
}
|
||||||
|
|
||||||
const imgdata = context.getImageData(xPixel, yPixel, 1, 1);
|
const imgdata = context.getImageData(xPixel, yPixel, 1, 1);
|
||||||
const red = imgdata.data[0];
|
const red = imgdata.data[0];
|
||||||
const green = imgdata.data[1];
|
const green = imgdata.data[1];
|
||||||
const blue = imgdata.data[2];
|
const blue = imgdata.data[2];
|
||||||
|
|
||||||
let elevation;
|
let elevation;
|
||||||
if (encoding === 'mapbox') {
|
if (encoding === 'mapbox') {
|
||||||
elevation =
|
elevation = -10000 + (red * 256 * 256 + green * 256 + blue) * 0.1;
|
||||||
-10000 + (red * 256 * 256 + green * 256 + blue) * 0.1;
|
|
||||||
} else if (encoding === 'terrarium') {
|
} else if (encoding === 'terrarium') {
|
||||||
elevation = red * 256 + green + blue / 256 - 32768;
|
elevation = red * 256 + green + blue / 256 - 32768;
|
||||||
} else {
|
} else {
|
||||||
elevation = 'invalid encoding';
|
elevation = 'invalid encoding';
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
res.status(200).send({
|
res.status(200).send({
|
||||||
z: zoom,
|
z: zoom,
|
||||||
|
@ -331,9 +307,7 @@ export const serve_data = {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
image.onerror = (err) => reject(err);
|
image.onerror = (err) => reject(err);
|
||||||
|
|
||||||
if (format === 'webp') {
|
if (format === 'webp') {
|
||||||
try {
|
try {
|
||||||
const img = await sharp(data).toFormat('png').toBuffer();
|
const img = await sharp(data).toFormat('png').toBuffer();
|
||||||
|
@ -351,10 +325,22 @@ export const serve_data = {
|
||||||
.header('Content-Type', 'text/plain')
|
.header('Content-Type', 'text/plain')
|
||||||
.send(err.message);
|
.send(err.message);
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
app.get('/:id.json', (req, res, next) => {
|
/**
|
||||||
|
* Handles requests for tilejson for the data tiles.
|
||||||
|
* @param {object} req - Express request object.
|
||||||
|
* @param {object} res - Express response object.
|
||||||
|
* @param {string} req.params.id - ID of the data source.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
app.get('/:id.json', (req, res) => {
|
||||||
|
if (verbose) {
|
||||||
|
console.log(
|
||||||
|
`Handling tilejson request for: /data/%s.json`,
|
||||||
|
String(req.params.id).replace(/\n|\r/g, ''),
|
||||||
|
);
|
||||||
|
}
|
||||||
const item = repo[req.params.id];
|
const item = repo[req.params.id];
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return res.sendStatus(404);
|
return res.sendStatus(404);
|
||||||
|
@ -377,7 +363,20 @@ export const serve_data = {
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
},
|
},
|
||||||
add: async (options, repo, params, id, publicUrl) => {
|
/**
|
||||||
|
* Adds a new data source to the repository.
|
||||||
|
* @param {object} options Configuration options.
|
||||||
|
* @param {object} repo Repository object.
|
||||||
|
* @param {object} params Parameters object.
|
||||||
|
* @param {string} id ID of the data source.
|
||||||
|
* @param {object} programOpts - An object containing the program options
|
||||||
|
* @param {string} programOpts.publicUrl Public URL for the data.
|
||||||
|
* @param {boolean} programOpts.verbose Whether verbose logging should be used.
|
||||||
|
* @param {Function} dataResolver Function to resolve data.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
add: async function (options, repo, params, id, programOpts) {
|
||||||
|
const { publicUrl } = programOpts;
|
||||||
let inputFile;
|
let inputFile;
|
||||||
let inputType;
|
let inputType;
|
||||||
if (params.pmtiles) {
|
if (params.pmtiles) {
|
||||||
|
|
|
@ -4,7 +4,15 @@ import express from 'express';
|
||||||
|
|
||||||
import { getFontsPbf, listFonts } from './utils.js';
|
import { getFontsPbf, listFonts } from './utils.js';
|
||||||
|
|
||||||
export const serve_font = async (options, allowedFonts) => {
|
/**
|
||||||
|
* Initializes and returns an Express app that serves font files.
|
||||||
|
* @param {object} options - Configuration options for the server.
|
||||||
|
* @param {object} allowedFonts - An object containing allowed fonts.
|
||||||
|
* @param {object} programOpts - An object containing the program options.
|
||||||
|
* @returns {Promise<express.Application>} - A promise that resolves to the Express app.
|
||||||
|
*/
|
||||||
|
export async function serve_font(options, allowedFonts, programOpts) {
|
||||||
|
const { verbose } = programOpts;
|
||||||
const app = express().disable('x-powered-by');
|
const app = express().disable('x-powered-by');
|
||||||
|
|
||||||
const lastModified = new Date().toUTCString();
|
const lastModified = new Date().toUTCString();
|
||||||
|
@ -13,31 +21,74 @@ export const serve_font = async (options, allowedFonts) => {
|
||||||
|
|
||||||
const existingFonts = {};
|
const existingFonts = {};
|
||||||
|
|
||||||
app.get(
|
/**
|
||||||
'/fonts/:fontstack/:range([\\d]+-[\\d]+).pbf',
|
* Handles requests for a font file.
|
||||||
async (req, res, next) => {
|
* @param {object} req - Express request object.
|
||||||
const fontstack = decodeURI(req.params.fontstack);
|
* @param {object} res - Express response object.
|
||||||
const range = req.params.range;
|
* @param {string} req.params.fontstack - Name of the font stack.
|
||||||
|
* @param {string} req.params.range - The range of the font (e.g. 0-255).
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
app.get('/fonts/:fontstack/:range.pbf', async (req, res) => {
|
||||||
|
const sRange = String(req.params.range).replace(/\n|\r/g, '');
|
||||||
|
const sFontStack = String(decodeURI(req.params.fontstack)).replace(
|
||||||
|
/\n|\r/g,
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (verbose) {
|
||||||
|
console.log(
|
||||||
|
`Handling font request for: /fonts/%s/%s.pbf`,
|
||||||
|
sFontStack,
|
||||||
|
sRange,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifiedSince = req.get('if-modified-since');
|
||||||
|
const cc = req.get('cache-control');
|
||||||
|
if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) {
|
||||||
|
if (
|
||||||
|
new Date(lastModified).getTime() === new Date(modifiedSince).getTime()
|
||||||
|
) {
|
||||||
|
return res.sendStatus(304);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const concatenated = await getFontsPbf(
|
const concatenated = await getFontsPbf(
|
||||||
options.serveAllFonts ? null : allowedFonts,
|
options.serveAllFonts ? null : allowedFonts,
|
||||||
fontPath,
|
fontPath,
|
||||||
fontstack,
|
sFontStack,
|
||||||
range,
|
sRange,
|
||||||
existingFonts,
|
existingFonts,
|
||||||
);
|
);
|
||||||
|
|
||||||
res.header('Content-type', 'application/x-protobuf');
|
res.header('Content-type', 'application/x-protobuf');
|
||||||
res.header('Last-Modified', lastModified);
|
res.header('Last-Modified', lastModified);
|
||||||
return res.send(concatenated);
|
return res.send(concatenated);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(400).header('Content-Type', 'text/plain').send(err);
|
console.error(
|
||||||
}
|
`Error serving font: %s/%s.pbf, Error: %s`,
|
||||||
},
|
sFontStack,
|
||||||
|
sRange,
|
||||||
|
String(err),
|
||||||
);
|
);
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.header('Content-Type', 'text/plain')
|
||||||
|
.send('Error serving font');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/fonts.json', (req, res, next) => {
|
/**
|
||||||
|
* Handles requests for a list of all available fonts.
|
||||||
|
* @param {object} req - Express request object.
|
||||||
|
* @param {object} res - Express response object.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
app.get('/fonts.json', (req, res) => {
|
||||||
|
if (verbose) {
|
||||||
|
console.log('Handling list font request for /fonts.json');
|
||||||
|
}
|
||||||
res.header('Content-type', 'application/json');
|
res.header('Content-type', 'application/json');
|
||||||
return res.send(
|
return res.send(
|
||||||
Object.keys(options.serveAllFonts ? existingFonts : allowedFonts).sort(),
|
Object.keys(options.serveAllFonts ? existingFonts : allowedFonts).sort(),
|
||||||
|
@ -47,4 +98,4 @@ export const serve_font = async (options, allowedFonts) => {
|
||||||
const fonts = await listFonts(options.paths.fonts);
|
const fonts = await listFonts(options.paths.fonts);
|
||||||
Object.assign(existingFonts, fonts);
|
Object.assign(existingFonts, fonts);
|
||||||
return app;
|
return app;
|
||||||
};
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
export const serve_rendered = {
|
export const serve_rendered = {
|
||||||
init: (options, repo) => {},
|
init: (options, repo, programOpts) => {},
|
||||||
add: (options, repo, params, id, publicUrl, dataResolver) => {},
|
add: (options, repo, params, id, programOpts, dataResolver) => {},
|
||||||
remove: (repo, id) => {},
|
remove: (repo, id) => {},
|
||||||
};
|
};
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -7,18 +7,44 @@ import clone from 'clone';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { validateStyleMin } from '@maplibre/maplibre-gl-style-spec';
|
import { validateStyleMin } from '@maplibre/maplibre-gl-style-spec';
|
||||||
|
|
||||||
import { fixUrl, allowedOptions } from './utils.js';
|
import {
|
||||||
|
allowedSpriteScales,
|
||||||
|
allowedSpriteFormats,
|
||||||
|
fixUrl,
|
||||||
|
readFile,
|
||||||
|
} from './utils.js';
|
||||||
|
|
||||||
const httpTester = /^https?:\/\//i;
|
const httpTester = /^https?:\/\//i;
|
||||||
const allowedSpriteScales = allowedOptions(['', '@2x', '@3x']);
|
|
||||||
const allowedSpriteFormats = allowedOptions(['png', 'json']);
|
|
||||||
|
|
||||||
export const serve_style = {
|
export const serve_style = {
|
||||||
init: (options, repo) => {
|
/**
|
||||||
|
* Initializes the serve_style module.
|
||||||
|
* @param {object} options Configuration options.
|
||||||
|
* @param {object} repo Repository object.
|
||||||
|
* @param {object} programOpts - An object containing the program options.
|
||||||
|
* @returns {express.Application} The initialized Express application.
|
||||||
|
*/
|
||||||
|
init: function (options, repo, programOpts) {
|
||||||
|
const { verbose } = programOpts;
|
||||||
const app = express().disable('x-powered-by');
|
const app = express().disable('x-powered-by');
|
||||||
|
/**
|
||||||
|
* Handles requests for style.json files.
|
||||||
|
* @param {express.Request} req - Express request object.
|
||||||
|
* @param {express.Response} res - Express response object.
|
||||||
|
* @param {express.NextFunction} next - Express next function.
|
||||||
|
* @param {string} req.params.id - ID of the style.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
app.get('/:id/style.json', (req, res, next) => {
|
app.get('/:id/style.json', (req, res, next) => {
|
||||||
const item = repo[req.params.id];
|
const { id } = req.params;
|
||||||
|
if (verbose) {
|
||||||
|
console.log(
|
||||||
|
'Handling style request for: /styles/%s/style.json',
|
||||||
|
String(id).replace(/\n|\r/g, ''),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const item = repo[id];
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return res.sendStatus(404);
|
return res.sendStatus(404);
|
||||||
}
|
}
|
||||||
|
@ -30,7 +56,6 @@ export const serve_style = {
|
||||||
source.data = fixUrl(req, source.data, item.publicUrl);
|
source.data = fixUrl(req, source.data, item.publicUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// mapbox-gl-js viewer cannot handle sprite urls with query
|
|
||||||
if (styleJSON_.sprite) {
|
if (styleJSON_.sprite) {
|
||||||
if (Array.isArray(styleJSON_.sprite)) {
|
if (Array.isArray(styleJSON_.sprite)) {
|
||||||
styleJSON_.sprite.forEach((spriteItem) => {
|
styleJSON_.sprite.forEach((spriteItem) => {
|
||||||
|
@ -44,48 +69,147 @@ export const serve_style = {
|
||||||
styleJSON_.glyphs = fixUrl(req, styleJSON_.glyphs, item.publicUrl);
|
styleJSON_.glyphs = fixUrl(req, styleJSON_.glyphs, item.publicUrl);
|
||||||
}
|
}
|
||||||
return res.send(styleJSON_);
|
return res.send(styleJSON_);
|
||||||
|
} catch (e) {
|
||||||
|
next(e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles GET requests for sprite images and JSON files.
|
||||||
|
* @param {express.Request} req - Express request object.
|
||||||
|
* @param {express.Response} res - Express response object.
|
||||||
|
* @param {express.NextFunction} next - Express next function.
|
||||||
|
* @param {string} req.params.id - ID of the sprite.
|
||||||
|
* @param {string} [req.params.spriteID='default'] - ID of the specific sprite image, defaults to 'default'.
|
||||||
|
* @param {string} [req.params.scale] - Scale of the sprite image, defaults to ''.
|
||||||
|
* @param {string} req.params.format - Format of the sprite file, 'png' or 'json'.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
app.get(
|
app.get(
|
||||||
'/:id/sprite(/:spriteID)?:scale(@[23]x)?.:format([\\w]+)',
|
`/:id/sprite{/:spriteID}{@:scale}{.:format}`,
|
||||||
(req, res, next) => {
|
async (req, res, next) => {
|
||||||
const { spriteID = 'default', id } = req.params;
|
const { spriteID = 'default', id, format, scale } = req.params;
|
||||||
const scale = allowedSpriteScales(req.params.scale) || '';
|
const sanitizedId = String(id).replace(/\n|\r/g, '');
|
||||||
const format = allowedSpriteFormats(req.params.format);
|
const sanitizedScale = scale ? String(scale).replace(/\n|\r/g, '') : '';
|
||||||
|
const sanitizedSpriteID = String(spriteID).replace(/\n|\r/g, '');
|
||||||
if (format) {
|
const sanitizedFormat = format
|
||||||
|
? '.' + String(format).replace(/\n|\r/g, '')
|
||||||
|
: '';
|
||||||
|
if (verbose) {
|
||||||
|
console.log(
|
||||||
|
`Handling sprite request for: /styles/%s/sprite/%s%s%s`,
|
||||||
|
sanitizedId,
|
||||||
|
sanitizedSpriteID,
|
||||||
|
sanitizedScale,
|
||||||
|
sanitizedFormat,
|
||||||
|
);
|
||||||
|
}
|
||||||
const item = repo[id];
|
const item = repo[id];
|
||||||
|
const validatedFormat = allowedSpriteFormats(format);
|
||||||
|
if (!item || !validatedFormat) {
|
||||||
|
if (verbose)
|
||||||
|
console.error(
|
||||||
|
`Sprite item or format not found for: /styles/%s/sprite/%s%s%s`,
|
||||||
|
sanitizedId,
|
||||||
|
sanitizedSpriteID,
|
||||||
|
sanitizedScale,
|
||||||
|
sanitizedFormat,
|
||||||
|
);
|
||||||
|
return res.sendStatus(404);
|
||||||
|
}
|
||||||
const sprite = item.spritePaths.find(
|
const sprite = item.spritePaths.find(
|
||||||
(sprite) => sprite.id === spriteID,
|
(sprite) => sprite.id === spriteID,
|
||||||
);
|
);
|
||||||
if (sprite) {
|
const spriteScale = allowedSpriteScales(scale);
|
||||||
const filename = `${sprite.path + scale}.${format}`;
|
if (!sprite || spriteScale === null) {
|
||||||
return fs.readFile(filename, (err, data) => {
|
if (verbose)
|
||||||
if (err) {
|
console.error(
|
||||||
console.log('Sprite load error:', filename);
|
`Bad Sprite ID or Scale for: /styles/%s/sprite/%s%s%s`,
|
||||||
return res.sendStatus(404);
|
sanitizedId,
|
||||||
} else {
|
sanitizedSpriteID,
|
||||||
if (format === 'json')
|
sanitizedScale,
|
||||||
res.header('Content-type', 'application/json');
|
sanitizedFormat,
|
||||||
if (format === 'png') res.header('Content-type', 'image/png');
|
);
|
||||||
return res.send(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return res.status(400).send('Bad Sprite ID or Scale');
|
return res.status(400).send('Bad Sprite ID or Scale');
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return res.status(400).send('Bad Sprite Format');
|
const modifiedSince = req.get('if-modified-since');
|
||||||
|
const cc = req.get('cache-control');
|
||||||
|
if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) {
|
||||||
|
if (
|
||||||
|
new Date(item.lastModified).getTime() ===
|
||||||
|
new Date(modifiedSince).getTime()
|
||||||
|
) {
|
||||||
|
return res.sendStatus(304);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedSpritePath = sprite.path.replace(/^(\.\.\/)+/, '');
|
||||||
|
const filename = `${sanitizedSpritePath}${spriteScale}.${validatedFormat}`;
|
||||||
|
if (verbose) console.log(`Loading sprite from: %s`, filename);
|
||||||
|
try {
|
||||||
|
const data = await readFile(filename);
|
||||||
|
|
||||||
|
if (validatedFormat === 'json') {
|
||||||
|
res.header('Content-type', 'application/json');
|
||||||
|
} else if (validatedFormat === 'png') {
|
||||||
|
res.header('Content-type', 'image/png');
|
||||||
|
}
|
||||||
|
if (verbose)
|
||||||
|
console.log(
|
||||||
|
`Responding with sprite data for /styles/%s/sprite/%s%s%s`,
|
||||||
|
sanitizedId,
|
||||||
|
sanitizedSpriteID,
|
||||||
|
sanitizedScale,
|
||||||
|
sanitizedFormat,
|
||||||
|
);
|
||||||
|
res.set({ 'Last-Modified': item.lastModified });
|
||||||
|
return res.send(data);
|
||||||
|
} catch (err) {
|
||||||
|
if (verbose) {
|
||||||
|
console.error(
|
||||||
|
'Sprite load error: %s, Error: %s',
|
||||||
|
filename,
|
||||||
|
String(err),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return res.sendStatus(404);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
},
|
},
|
||||||
remove: (repo, id) => {
|
/**
|
||||||
|
* Removes an item from the repository.
|
||||||
|
* @param {object} repo Repository object.
|
||||||
|
* @param {string} id ID of the item to remove.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
remove: function (repo, id) {
|
||||||
delete repo[id];
|
delete repo[id];
|
||||||
},
|
},
|
||||||
add: (options, repo, params, id, publicUrl, reportTiles, reportFont) => {
|
/**
|
||||||
|
* Adds a new style to the repository.
|
||||||
|
* @param {object} options Configuration options.
|
||||||
|
* @param {object} repo Repository object.
|
||||||
|
* @param {object} params Parameters object containing style path
|
||||||
|
* @param {string} id ID of the style.
|
||||||
|
* @param {object} programOpts - An object containing the program options
|
||||||
|
* @param {Function} reportTiles Function for reporting tile sources.
|
||||||
|
* @param {Function} reportFont Function for reporting font usage
|
||||||
|
* @returns {boolean} true if add is succesful
|
||||||
|
*/
|
||||||
|
add: function (
|
||||||
|
options,
|
||||||
|
repo,
|
||||||
|
params,
|
||||||
|
id,
|
||||||
|
programOpts,
|
||||||
|
reportTiles,
|
||||||
|
reportFont,
|
||||||
|
) {
|
||||||
|
const { publicUrl } = programOpts;
|
||||||
const styleFile = path.resolve(options.paths.styles, params.style);
|
const styleFile = path.resolve(options.paths.styles, params.style);
|
||||||
|
|
||||||
let styleFileData;
|
let styleFileData;
|
||||||
|
@ -199,6 +323,7 @@ export const serve_style = {
|
||||||
spritePaths,
|
spritePaths,
|
||||||
publicUrl,
|
publicUrl,
|
||||||
name: styleJSON.name,
|
name: styleJSON.name,
|
||||||
|
lastModified: new Date().toUTCString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
238
src/server.js
238
src/server.js
|
@ -1,9 +1,6 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import os from 'os';
|
|
||||||
process.env.UV_THREADPOOL_SIZE = Math.ceil(Math.max(4, os.cpus().length * 1.5));
|
|
||||||
|
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fnv1a from '@sindresorhus/fnv1a';
|
import fnv1a from '@sindresorhus/fnv1a';
|
||||||
|
@ -19,7 +16,12 @@ import morgan from 'morgan';
|
||||||
import { serve_data } from './serve_data.js';
|
import { serve_data } from './serve_data.js';
|
||||||
import { serve_style } from './serve_style.js';
|
import { serve_style } from './serve_style.js';
|
||||||
import { serve_font } from './serve_font.js';
|
import { serve_font } from './serve_font.js';
|
||||||
import { getTileUrls, getPublicUrl, isValidHttpUrl } from './utils.js';
|
import {
|
||||||
|
allowedTileSizes,
|
||||||
|
getTileUrls,
|
||||||
|
getPublicUrl,
|
||||||
|
isValidHttpUrl,
|
||||||
|
} from './utils.js';
|
||||||
|
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
@ -34,10 +36,11 @@ const serve_rendered = (
|
||||||
).serve_rendered;
|
).serve_rendered;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Starts the server.
|
||||||
* @param opts
|
* @param {object} opts - Configuration options for the server.
|
||||||
|
* @returns {Promise<object>} - A promise that resolves to the server object.
|
||||||
*/
|
*/
|
||||||
function start(opts) {
|
async function start(opts) {
|
||||||
console.log('Starting server');
|
console.log('Starting server');
|
||||||
|
|
||||||
const app = express().disable('x-powered-by');
|
const app = express().disable('x-powered-by');
|
||||||
|
@ -116,8 +119,9 @@ function start(opts) {
|
||||||
* Recursively get all files within a directory.
|
* Recursively get all files within a directory.
|
||||||
* Inspired by https://stackoverflow.com/a/45130990/10133863
|
* Inspired by https://stackoverflow.com/a/45130990/10133863
|
||||||
* @param {string} directory Absolute path to a directory to get files from.
|
* @param {string} directory Absolute path to a directory to get files from.
|
||||||
|
* @returns {Promise<string[]>} - A promise that resolves to an array of file paths relative to the icon directory.
|
||||||
*/
|
*/
|
||||||
const getFiles = async (directory) => {
|
async function getFiles(directory) {
|
||||||
// Fetch all entries of the directory and attach type information
|
// Fetch all entries of the directory and attach type information
|
||||||
const dirEntries = await fs.promises.readdir(directory, {
|
const dirEntries = await fs.promises.readdir(directory, {
|
||||||
withFileTypes: true,
|
withFileTypes: true,
|
||||||
|
@ -136,7 +140,7 @@ function start(opts) {
|
||||||
|
|
||||||
// Flatten the list of files to a single array
|
// Flatten the list of files to a single array
|
||||||
return files.flat();
|
return files.flat();
|
||||||
};
|
}
|
||||||
|
|
||||||
// Load all available icons into a settings object
|
// Load all available icons into a settings object
|
||||||
startupPromises.push(
|
startupPromises.push(
|
||||||
|
@ -159,18 +163,25 @@ function start(opts) {
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use('/data/', serve_data.init(options, serving.data));
|
app.use('/data/', serve_data.init(options, serving.data, opts));
|
||||||
app.use('/files/', express.static(paths.files));
|
app.use('/files/', express.static(paths.files));
|
||||||
app.use('/styles/', serve_style.init(options, serving.styles));
|
app.use('/styles/', serve_style.init(options, serving.styles, opts));
|
||||||
if (!isLight) {
|
if (!isLight) {
|
||||||
startupPromises.push(
|
startupPromises.push(
|
||||||
serve_rendered.init(options, serving.rendered).then((sub) => {
|
serve_rendered.init(options, serving.rendered, opts).then((sub) => {
|
||||||
app.use('/styles/', sub);
|
app.use('/styles/', sub);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
const addStyle = (id, item, allowMoreData, reportFonts) => {
|
* Adds a style to the server.
|
||||||
|
* @param {string} id - The ID of the style.
|
||||||
|
* @param {object} item - The style configuration object.
|
||||||
|
* @param {boolean} allowMoreData - Whether to allow adding more data sources.
|
||||||
|
* @param {boolean} reportFonts - Whether to report fonts.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function addStyle(id, item, allowMoreData, reportFonts) {
|
||||||
let success = true;
|
let success = true;
|
||||||
if (item.serve_data !== false) {
|
if (item.serve_data !== false) {
|
||||||
success = serve_style.add(
|
success = serve_style.add(
|
||||||
|
@ -178,7 +189,7 @@ function start(opts) {
|
||||||
serving.styles,
|
serving.styles,
|
||||||
item,
|
item,
|
||||||
id,
|
id,
|
||||||
opts.publicUrl,
|
opts,
|
||||||
(styleSourceId, protocol) => {
|
(styleSourceId, protocol) => {
|
||||||
let dataItemId;
|
let dataItemId;
|
||||||
for (const id of Object.keys(data)) {
|
for (const id of Object.keys(data)) {
|
||||||
|
@ -235,7 +246,7 @@ function start(opts) {
|
||||||
serving.rendered,
|
serving.rendered,
|
||||||
item,
|
item,
|
||||||
id,
|
id,
|
||||||
opts.publicUrl,
|
opts,
|
||||||
function dataResolver(styleSourceId) {
|
function dataResolver(styleSourceId) {
|
||||||
let fileType;
|
let fileType;
|
||||||
let inputFile;
|
let inputFile;
|
||||||
|
@ -261,7 +272,7 @@ function start(opts) {
|
||||||
item.serve_rendered = false;
|
item.serve_rendered = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
for (const id of Object.keys(config.styles || {})) {
|
for (const id of Object.keys(config.styles || {})) {
|
||||||
const item = config.styles[id];
|
const item = config.styles[id];
|
||||||
|
@ -272,13 +283,11 @@ function start(opts) {
|
||||||
|
|
||||||
addStyle(id, item, true, true);
|
addStyle(id, item, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
startupPromises.push(
|
startupPromises.push(
|
||||||
serve_font(options, serving.fonts).then((sub) => {
|
serve_font(options, serving.fonts, opts).then((sub) => {
|
||||||
app.use('/', sub);
|
app.use('/', sub);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const id of Object.keys(data)) {
|
for (const id of Object.keys(data)) {
|
||||||
const item = data[id];
|
const item = data[id];
|
||||||
const fileType = Object.keys(data[id])[0];
|
const fileType = Object.keys(data[id])[0];
|
||||||
|
@ -288,12 +297,8 @@ function start(opts) {
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
startupPromises.push(serve_data.add(options, serving.data, item, id, opts));
|
||||||
startupPromises.push(
|
|
||||||
serve_data.add(options, serving.data, item, id, opts.publicUrl),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.serveAllStyles) {
|
if (options.serveAllStyles) {
|
||||||
fs.readdir(options.paths.styles, { withFileTypes: true }, (err, files) => {
|
fs.readdir(options.paths.styles, { withFileTypes: true }, (err, files) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -333,7 +338,13 @@ function start(opts) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Handles requests for a list of available styles.
|
||||||
|
* @param {object} req - Express request object.
|
||||||
|
* @param {object} res - Express response object.
|
||||||
|
* @param {string} [req.query.key] - Optional API key.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
app.get('/styles.json', (req, res, next) => {
|
app.get('/styles.json', (req, res, next) => {
|
||||||
const result = [];
|
const result = [];
|
||||||
const query = req.query.key
|
const query = req.query.key
|
||||||
|
@ -354,7 +365,15 @@ function start(opts) {
|
||||||
res.send(result);
|
res.send(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
const addTileJSONs = (arr, req, type, tileSize) => {
|
/**
|
||||||
|
* Adds TileJSON metadata to an array.
|
||||||
|
* @param {Array} arr - The array to add TileJSONs to
|
||||||
|
* @param {object} req - The express request object.
|
||||||
|
* @param {string} type - The type of resource
|
||||||
|
* @param {number} tileSize - The tile size.
|
||||||
|
* @returns {Array} - An array of TileJSON objects.
|
||||||
|
*/
|
||||||
|
function addTileJSONs(arr, req, type, tileSize) {
|
||||||
for (const id of Object.keys(serving[type])) {
|
for (const id of Object.keys(serving[type])) {
|
||||||
const info = clone(serving[type][id].tileJSON);
|
const info = clone(serving[type][id].tileJSON);
|
||||||
let path = '';
|
let path = '';
|
||||||
|
@ -377,20 +396,42 @@ function start(opts) {
|
||||||
arr.push(info);
|
arr.push(info);
|
||||||
}
|
}
|
||||||
return arr;
|
return arr;
|
||||||
};
|
}
|
||||||
|
|
||||||
app.get('/(:tileSize(256|512)/)?rendered.json', (req, res, next) => {
|
/**
|
||||||
const tileSize = parseInt(req.params.tileSize, 10) || undefined;
|
* Handles requests for a rendered tilejson endpoint.
|
||||||
res.send(addTileJSONs([], req, 'rendered', tileSize));
|
* @param {object} req - Express request object.
|
||||||
|
* @param {object} res - Express response object.
|
||||||
|
* @param {string} req.params.tileSize - Optional tile size parameter.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
app.get('{/:tileSize}/rendered.json', (req, res, next) => {
|
||||||
|
const tileSize = allowedTileSizes(req.params['tileSize']);
|
||||||
|
res.send(addTileJSONs([], req, 'rendered', parseInt(tileSize, 10)));
|
||||||
});
|
});
|
||||||
app.get('/data.json', (req, res, next) => {
|
|
||||||
|
/**
|
||||||
|
* Handles requests for a data tilejson endpoint.
|
||||||
|
* @param {object} req - Express request object.
|
||||||
|
* @param {object} res - Express response object.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
app.get('/data.json', (req, res) => {
|
||||||
res.send(addTileJSONs([], req, 'data', undefined));
|
res.send(addTileJSONs([], req, 'data', undefined));
|
||||||
});
|
});
|
||||||
app.get('/(:tileSize(256|512)/)?index.json', (req, res, next) => {
|
|
||||||
const tileSize = parseInt(req.params.tileSize, 10) || undefined;
|
/**
|
||||||
|
* Handles requests for a combined rendered and data tilejson endpoint.
|
||||||
|
* @param {object} req - Express request object.
|
||||||
|
* @param {object} res - Express response object.
|
||||||
|
* @param {string} req.params.tileSize - Optional tile size parameter.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
app.get('{/:tileSize}/index.json', (req, res, next) => {
|
||||||
|
const tileSize = allowedTileSizes(req.params['tileSize']);
|
||||||
res.send(
|
res.send(
|
||||||
addTileJSONs(
|
addTileJSONs(
|
||||||
addTileJSONs([], req, 'rendered', tileSize),
|
addTileJSONs([], req, 'rendered', parseInt(tileSize, 10)),
|
||||||
req,
|
req,
|
||||||
'data',
|
'data',
|
||||||
undefined,
|
undefined,
|
||||||
|
@ -403,7 +444,15 @@ function start(opts) {
|
||||||
app.use('/', express.static(path.join(__dirname, '../public/resources')));
|
app.use('/', express.static(path.join(__dirname, '../public/resources')));
|
||||||
|
|
||||||
const templates = path.join(__dirname, '../public/templates');
|
const templates = path.join(__dirname, '../public/templates');
|
||||||
const serveTemplate = (urlPath, template, dataGetter) => {
|
|
||||||
|
/**
|
||||||
|
* Serves a Handlebars template.
|
||||||
|
* @param {string} urlPath - The URL path to serve the template at
|
||||||
|
* @param {string} template - The name of the template file
|
||||||
|
* @param {Function} dataGetter - A function to get data to be passed to the template.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function serveTemplate(urlPath, template, dataGetter) {
|
||||||
let templateFile = `${templates}/${template}.tmpl`;
|
let templateFile = `${templates}/${template}.tmpl`;
|
||||||
if (template === 'index') {
|
if (template === 'index') {
|
||||||
if (options.frontPage === false) {
|
if (options.frontPage === false) {
|
||||||
|
@ -415,24 +464,17 @@ function start(opts) {
|
||||||
templateFile = path.resolve(paths.root, options.frontPage);
|
templateFile = path.resolve(paths.root, options.frontPage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
startupPromises.push(
|
try {
|
||||||
new Promise((resolve, reject) => {
|
const content = fs.readFileSync(templateFile, 'utf-8');
|
||||||
fs.readFile(templateFile, (err, content) => {
|
|
||||||
if (err) {
|
|
||||||
err = new Error(`Template not found: ${err.message}`);
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const compiled = handlebars.compile(content.toString());
|
const compiled = handlebars.compile(content.toString());
|
||||||
|
app.get(urlPath, (req, res, next) => {
|
||||||
app.use(urlPath, (req, res, next) => {
|
if (opts.verbose) {
|
||||||
|
console.log(`Serving template at path: ${urlPath}`);
|
||||||
|
}
|
||||||
let data = {};
|
let data = {};
|
||||||
if (dataGetter) {
|
if (dataGetter) {
|
||||||
data = dataGetter(req);
|
data = dataGetter(req);
|
||||||
if (!data) {
|
if (data) {
|
||||||
return res.status(404).send('Not found');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data['server_version'] =
|
data['server_version'] =
|
||||||
`${packageJson.name} v${packageJson.version}`;
|
`${packageJson.name} v${packageJson.version}`;
|
||||||
data['public_url'] = opts.publicUrl || '/';
|
data['public_url'] = opts.publicUrl || '/';
|
||||||
|
@ -445,14 +487,27 @@ function start(opts) {
|
||||||
: '';
|
: '';
|
||||||
if (template === 'wmts') res.set('Content-Type', 'text/xml');
|
if (template === 'wmts') res.set('Content-Type', 'text/xml');
|
||||||
return res.status(200).send(compiled(data));
|
return res.status(200).send(compiled(data));
|
||||||
|
} else {
|
||||||
|
if (opts.verbose) {
|
||||||
|
console.log(`Forwarding request for: ${urlPath} to next route`);
|
||||||
|
}
|
||||||
|
next('route');
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
resolve();
|
} catch (err) {
|
||||||
});
|
console.error(`Error reading template file: ${templateFile}`, err);
|
||||||
}),
|
throw new Error(`Template not found: ${err.message}`); //throw an error so that the server doesnt start
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
serveTemplate('/$', 'index', (req) => {
|
/**
|
||||||
|
* Handles requests for the index page, providing a list of available styles and data.
|
||||||
|
* @param {object} req - Express request object.
|
||||||
|
* @param {object} res - Express response object.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
serveTemplate('/', 'index', (req) => {
|
||||||
let styles = {};
|
let styles = {};
|
||||||
for (const id of Object.keys(serving.styles || {})) {
|
for (const id of Object.keys(serving.styles || {})) {
|
||||||
let style = {
|
let style = {
|
||||||
|
@ -464,11 +519,15 @@ function start(opts) {
|
||||||
if (style.serving_rendered) {
|
if (style.serving_rendered) {
|
||||||
const { center } = style.serving_rendered.tileJSON;
|
const { center } = style.serving_rendered.tileJSON;
|
||||||
if (center) {
|
if (center) {
|
||||||
style.viewer_hash = `#${center[2]}/${center[1].toFixed(5)}/${center[0].toFixed(5)}`;
|
style.viewer_hash = `#${center[2]}/${center[1].toFixed(
|
||||||
|
5,
|
||||||
|
)}/${center[0].toFixed(5)}`;
|
||||||
|
|
||||||
const centerPx = mercator.px([center[0], center[1]], center[2]);
|
const centerPx = mercator.px([center[0], center[1]], center[2]);
|
||||||
// Set thumbnail default size to be 256px x 256px
|
// Set thumbnail default size to be 256px x 256px
|
||||||
style.thumbnail = `${Math.floor(center[2])}/${Math.floor(centerPx[0] / 256)}/${Math.floor(centerPx[1] / 256)}.png`;
|
style.thumbnail = `${Math.floor(center[2])}/${Math.floor(
|
||||||
|
centerPx[0] / 256,
|
||||||
|
)}/${Math.floor(centerPx[1] / 256)}.png`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tileSize = 512;
|
const tileSize = 512;
|
||||||
|
@ -484,7 +543,6 @@ function start(opts) {
|
||||||
|
|
||||||
styles[id] = style;
|
styles[id] = style;
|
||||||
}
|
}
|
||||||
|
|
||||||
let datas = {};
|
let datas = {};
|
||||||
for (const id of Object.keys(serving.data || {})) {
|
for (const id of Object.keys(serving.data || {})) {
|
||||||
let data = Object.assign({}, serving.data[id]);
|
let data = Object.assign({}, serving.data[id]);
|
||||||
|
@ -525,7 +583,9 @@ function start(opts) {
|
||||||
}
|
}
|
||||||
if (center) {
|
if (center) {
|
||||||
const centerPx = mercator.px([center[0], center[1]], center[2]);
|
const centerPx = mercator.px([center[0], center[1]], center[2]);
|
||||||
data.thumbnail = `${Math.floor(center[2])}/${Math.floor(centerPx[0] / 256)}/${Math.floor(centerPx[1] / 256)}.${tileJSON.format}`;
|
data.thumbnail = `${Math.floor(center[2])}/${Math.floor(
|
||||||
|
centerPx[0] / 256,
|
||||||
|
)}/${Math.floor(centerPx[1] / 256)}.${tileJSON.format}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -542,24 +602,28 @@ function start(opts) {
|
||||||
}
|
}
|
||||||
data.formatted_filesize = `${size.toFixed(2)} ${suffix}`;
|
data.formatted_filesize = `${size.toFixed(2)} ${suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
datas[id] = data;
|
datas[id] = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
styles: Object.keys(styles).length ? styles : null,
|
styles: Object.keys(styles).length ? styles : null,
|
||||||
data: Object.keys(datas).length ? datas : null,
|
data: Object.keys(datas).length ? datas : null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
serveTemplate('/styles/:id/$', 'viewer', (req) => {
|
/**
|
||||||
|
* Handles requests for a map viewer template for a specific style.
|
||||||
|
* @param {object} req - Express request object.
|
||||||
|
* @param {object} res - Express response object.
|
||||||
|
* @param {string} req.params.id - ID of the style.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
serveTemplate('/styles/:id/', 'viewer', (req) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const style = clone(((serving.styles || {})[id] || {}).styleJSON);
|
const style = clone(((serving.styles || {})[id] || {}).styleJSON);
|
||||||
|
|
||||||
if (!style) {
|
if (!style) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...style,
|
...style,
|
||||||
id,
|
id,
|
||||||
|
@ -569,10 +633,12 @@ function start(opts) {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/**
|
||||||
app.use('/rendered/:id/$', function(req, res, next) {
|
* Handles requests for a Web Map Tile Service (WMTS) XML template.
|
||||||
return res.redirect(301, '/styles/' + req.params.id + '/');
|
* @param {object} req - Express request object.
|
||||||
});
|
* @param {object} res - Express response object.
|
||||||
|
* @param {string} req.params.id - ID of the style.
|
||||||
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
serveTemplate('/styles/:id/wmts.xml', 'wmts', (req) => {
|
serveTemplate('/styles/:id/wmts.xml', 'wmts', (req) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
@ -605,9 +671,16 @@ function start(opts) {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
serveTemplate('^/data/(:preview(preview)/)?:id/$', 'data', (req) => {
|
/**
|
||||||
const id = req.params.id;
|
* Handles requests for a data view template for a specific data source.
|
||||||
const preview = req.params.preview || undefined;
|
* @param {object} req - Express request object.
|
||||||
|
* @param {object} res - Express response object.
|
||||||
|
* @param {string} req.params.id - ID of the data source.
|
||||||
|
* @param {string} [req.params.view] - Optional view type.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
serveTemplate('/data{/:view}/:id/', 'data', (req) => {
|
||||||
|
const { id, view } = req.params;
|
||||||
const data = serving.data[id];
|
const data = serving.data[id];
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
@ -616,7 +689,8 @@ function start(opts) {
|
||||||
const is_terrain =
|
const is_terrain =
|
||||||
(data.tileJSON.encoding === 'terrarium' ||
|
(data.tileJSON.encoding === 'terrarium' ||
|
||||||
data.tileJSON.encoding === 'mapbox') &&
|
data.tileJSON.encoding === 'mapbox') &&
|
||||||
preview === 'preview';
|
view === 'preview';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
id,
|
id,
|
||||||
|
@ -633,7 +707,13 @@ function start(opts) {
|
||||||
startupComplete = true;
|
startupComplete = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/health', (req, res, next) => {
|
/**
|
||||||
|
* Handles requests to see the health of the server.
|
||||||
|
* @param {object} req - Express request object.
|
||||||
|
* @param {object} res - Express response object.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
app.get('/health', (req, res) => {
|
||||||
if (startupComplete) {
|
if (startupComplete) {
|
||||||
return res.status(200).send('OK');
|
return res.status(200).send('OK');
|
||||||
} else {
|
} else {
|
||||||
|
@ -662,10 +742,10 @@ function start(opts) {
|
||||||
startupPromise,
|
startupPromise,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the server gracefully
|
* Stop the server gracefully
|
||||||
* @param {string} signal Name of the received signal
|
* @param {string} signal Name of the received signal
|
||||||
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
function stopGracefully(signal) {
|
function stopGracefully(signal) {
|
||||||
console.log(`Caught signal ${signal}, stopping gracefully`);
|
console.log(`Caught signal ${signal}, stopping gracefully`);
|
||||||
|
@ -673,11 +753,12 @@ function stopGracefully(signal) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Starts and manages the server
|
||||||
* @param opts
|
* @param {object} opts - Configuration options for the server.
|
||||||
|
* @returns {Promise<object>} - A promise that resolves to the running server
|
||||||
*/
|
*/
|
||||||
export function server(opts) {
|
export async function server(opts) {
|
||||||
const running = start(opts);
|
const running = await start(opts);
|
||||||
|
|
||||||
running.startupPromise.catch((err) => {
|
running.startupPromise.catch((err) => {
|
||||||
console.error(err.message);
|
console.error(err.message);
|
||||||
|
@ -697,6 +778,5 @@ export function server(opts) {
|
||||||
running.app = restarted.app;
|
running.app = restarted.app;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return running;
|
return running;
|
||||||
}
|
}
|
||||||
|
|
264
src/utils.js
264
src/utils.js
|
@ -6,12 +6,18 @@ import fs from 'node:fs';
|
||||||
import clone from 'clone';
|
import clone from 'clone';
|
||||||
import { combine } from '@jsse/pbfont';
|
import { combine } from '@jsse/pbfont';
|
||||||
import { existsP } from './promises.js';
|
import { existsP } from './promises.js';
|
||||||
|
import { getPMtilesTile } from './pmtiles_adapter.js';
|
||||||
|
|
||||||
|
export const allowedSpriteFormats = allowedOptions(['png', 'json']);
|
||||||
|
|
||||||
|
export const allowedTileSizes = allowedOptions(['256', '512']);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restrict user input to an allowed set of options.
|
* Restrict user input to an allowed set of options.
|
||||||
* @param opts
|
* @param {string[]} opts - An array of allowed option strings.
|
||||||
* @param root0
|
* @param {object} [config] - Optional configuration object.
|
||||||
* @param root0.defaultValue
|
* @param {string} [config.defaultValue] - The default value to return if input doesn't match.
|
||||||
|
* @returns {function(string): string} - A function that takes a value and returns it if valid or a default.
|
||||||
*/
|
*/
|
||||||
export function allowedOptions(opts, { defaultValue } = {}) {
|
export function allowedOptions(opts, { defaultValue } = {}) {
|
||||||
const values = Object.fromEntries(opts.map((key) => [key, key]));
|
const values = Object.fromEntries(opts.map((key) => [key, key]));
|
||||||
|
@ -19,10 +25,52 @@ export function allowedOptions(opts, { defaultValue } = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace local:// urls with public http(s):// urls
|
* Parses a scale string to a number.
|
||||||
* @param req
|
* @param {string} scale The scale string (e.g., '2x', '4x').
|
||||||
* @param url
|
* @param {number} maxScale Maximum allowed scale digit.
|
||||||
* @param publicUrl
|
* @returns {number|null} The parsed scale as a number or null if invalid.
|
||||||
|
*/
|
||||||
|
export function allowedScales(scale, maxScale = 9) {
|
||||||
|
if (scale === undefined) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line security/detect-non-literal-regexp
|
||||||
|
const regex = new RegExp(`^[2-${maxScale}]x$`);
|
||||||
|
if (!regex.test(scale)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseInt(scale.slice(0, -1), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a string is a valid sprite scale and returns it if it is within the allowed range, and null if it does not conform.
|
||||||
|
* @param {string} scale - The scale string to validate (e.g., '2x', '3x').
|
||||||
|
* @param {number} [maxScale] - The maximum scale value. If no value is passed in, it defaults to a value of 3.
|
||||||
|
* @returns {string|null} - The valid scale string or null if invalid.
|
||||||
|
*/
|
||||||
|
export function allowedSpriteScales(scale, maxScale = 3) {
|
||||||
|
if (!scale) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const match = scale?.match(/^([2-9]\d*)x$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const parsedScale = parseInt(match[1], 10);
|
||||||
|
if (parsedScale <= maxScale) {
|
||||||
|
return `@${parsedScale}x`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces local:// URLs with public http(s):// URLs.
|
||||||
|
* @param {object} req - Express request object.
|
||||||
|
* @param {string} url - The URL string to fix.
|
||||||
|
* @param {string} publicUrl - The public URL prefix to use for replacements.
|
||||||
|
* @returns {string} - The fixed URL string.
|
||||||
*/
|
*/
|
||||||
export function fixUrl(req, url, publicUrl) {
|
export function fixUrl(req, url, publicUrl) {
|
||||||
if (!url || typeof url !== 'string' || url.indexOf('local://') !== 0) {
|
if (!url || typeof url !== 'string' || url.indexOf('local://') !== 0) {
|
||||||
|
@ -40,12 +88,11 @@ export function fixUrl(req, url, publicUrl) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate new URL object
|
* Generates a new URL object from the Express request.
|
||||||
* @param req
|
* @param {object} req - Express request object.
|
||||||
* @params {object} req - Express request
|
* @returns {URL} - URL object with correct host and optionally path.
|
||||||
* @returns {URL} object
|
|
||||||
*/
|
*/
|
||||||
const getUrlObject = (req) => {
|
function getUrlObject(req) {
|
||||||
const urlObject = new URL(`${req.protocol}://${req.headers.host}/`);
|
const urlObject = new URL(`${req.protocol}://${req.headers.host}/`);
|
||||||
// support overriding hostname by sending X-Forwarded-Host http header
|
// support overriding hostname by sending X-Forwarded-Host http header
|
||||||
urlObject.hostname = req.hostname;
|
urlObject.hostname = req.hostname;
|
||||||
|
@ -62,16 +109,33 @@ const getUrlObject = (req) => {
|
||||||
urlObject.pathname = path.posix.join(xForwardedPath, urlObject.pathname);
|
urlObject.pathname = path.posix.join(xForwardedPath, urlObject.pathname);
|
||||||
}
|
}
|
||||||
return urlObject;
|
return urlObject;
|
||||||
};
|
}
|
||||||
|
|
||||||
export const getPublicUrl = (publicUrl, req) => {
|
/**
|
||||||
|
* Gets the public URL, either from a provided publicUrl or generated from the request.
|
||||||
|
* @param {string} publicUrl - The optional public URL to use.
|
||||||
|
* @param {object} req - The Express request object.
|
||||||
|
* @returns {string} - The final public URL string.
|
||||||
|
*/
|
||||||
|
export function getPublicUrl(publicUrl, req) {
|
||||||
if (publicUrl) {
|
if (publicUrl) {
|
||||||
return publicUrl;
|
return publicUrl;
|
||||||
}
|
}
|
||||||
return getUrlObject(req).toString();
|
return getUrlObject(req).toString();
|
||||||
};
|
}
|
||||||
|
|
||||||
export const getTileUrls = (
|
/**
|
||||||
|
* Generates an array of tile URLs based on given parameters.
|
||||||
|
* @param {object} req - Express request object.
|
||||||
|
* @param {string | string[]} domains - Domain(s) to use for tile URLs.
|
||||||
|
* @param {string} path - The base path for the tiles.
|
||||||
|
* @param {number} [tileSize] - The size of the tile (optional).
|
||||||
|
* @param {string} format - The format of the tiles (e.g., 'png', 'jpg').
|
||||||
|
* @param {string} publicUrl - The public URL to use (if not using domains).
|
||||||
|
* @param {object} [aliases] - Aliases for format extensions.
|
||||||
|
* @returns {string[]} An array of tile URL strings.
|
||||||
|
*/
|
||||||
|
export function getTileUrls(
|
||||||
req,
|
req,
|
||||||
domains,
|
domains,
|
||||||
path,
|
path,
|
||||||
|
@ -79,7 +143,7 @@ export const getTileUrls = (
|
||||||
format,
|
format,
|
||||||
publicUrl,
|
publicUrl,
|
||||||
aliases,
|
aliases,
|
||||||
) => {
|
) {
|
||||||
const urlObject = getUrlObject(req);
|
const urlObject = getUrlObject(req);
|
||||||
if (domains) {
|
if (domains) {
|
||||||
if (domains.constructor === String && domains.length > 0) {
|
if (domains.constructor === String && domains.length > 0) {
|
||||||
|
@ -144,9 +208,14 @@ export const getTileUrls = (
|
||||||
}
|
}
|
||||||
|
|
||||||
return uris;
|
return uris;
|
||||||
};
|
}
|
||||||
|
|
||||||
export const fixTileJSONCenter = (tileJSON) => {
|
/**
|
||||||
|
* Fixes the center in the tileJSON if no center is available.
|
||||||
|
* @param {object} tileJSON - The tileJSON object to process.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function fixTileJSONCenter(tileJSON) {
|
||||||
if (tileJSON.bounds && !tileJSON.center) {
|
if (tileJSON.bounds && !tileJSON.center) {
|
||||||
const fitWidth = 1024;
|
const fitWidth = 1024;
|
||||||
const tiles = fitWidth / 256;
|
const tiles = fitWidth / 256;
|
||||||
|
@ -159,19 +228,77 @@ export const fixTileJSONCenter = (tileJSON) => {
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const getFontPbf = (allowedFonts, fontPath, name, range, fallbacks) =>
|
/**
|
||||||
new Promise((resolve, reject) => {
|
* Reads a file and returns a Promise with the file data.
|
||||||
|
* @param {string} filename - Path to the file to read.
|
||||||
|
* @returns {Promise<Buffer>} - A Promise that resolves with the file data as a Buffer or rejects with an error.
|
||||||
|
*/
|
||||||
|
export function readFile(filename) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const sanitizedFilename = path.normalize(filename); // Normalize path, remove ..
|
||||||
|
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
||||||
|
fs.readFile(String(sanitizedFilename), (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves font data for a given font and range.
|
||||||
|
* @param {object} allowedFonts - An object of allowed fonts.
|
||||||
|
* @param {string} fontPath - The path to the font directory.
|
||||||
|
* @param {string} name - The name of the font.
|
||||||
|
* @param {string} range - The range (e.g., '0-255') of the font to load.
|
||||||
|
* @param {object} [fallbacks] - Optional fallback font list.
|
||||||
|
* @returns {Promise<Buffer>} A promise that resolves with the font data Buffer or rejects with an error.
|
||||||
|
*/
|
||||||
|
async function getFontPbf(allowedFonts, fontPath, name, range, fallbacks) {
|
||||||
if (!allowedFonts || (allowedFonts[name] && fallbacks)) {
|
if (!allowedFonts || (allowedFonts[name] && fallbacks)) {
|
||||||
const filename = path.join(fontPath, name, `${range}.pbf`);
|
const fontMatch = name?.match(/^[\p{L}\p{N} \-\.~!*'()@&=+,#$\[\]]+$/u);
|
||||||
|
const sanitizedName = fontMatch?.[0] || 'invalid';
|
||||||
|
if (!name || typeof name !== 'string' || name.trim() === '' || !fontMatch) {
|
||||||
|
console.error(
|
||||||
|
'ERROR: Invalid font name: %s',
|
||||||
|
sanitizedName.replace(/\n|\r/g, ''),
|
||||||
|
);
|
||||||
|
throw new Error('Invalid font name');
|
||||||
|
}
|
||||||
|
|
||||||
|
const rangeMatch = range?.match(/^[\d-]+$/);
|
||||||
|
const sanitizedRange = rangeMatch?.[0] || 'invalid';
|
||||||
|
if (!/^\d+-\d+$/.test(range)) {
|
||||||
|
console.error(
|
||||||
|
'ERROR: Invalid range: %s',
|
||||||
|
sanitizedRange.replace(/\n|\r/g, ''),
|
||||||
|
);
|
||||||
|
throw new Error('Invalid range');
|
||||||
|
}
|
||||||
|
const filename = path.join(
|
||||||
|
fontPath,
|
||||||
|
sanitizedName,
|
||||||
|
`${sanitizedRange}.pbf`,
|
||||||
|
);
|
||||||
|
|
||||||
if (!fallbacks) {
|
if (!fallbacks) {
|
||||||
fallbacks = clone(allowedFonts || {});
|
fallbacks = clone(allowedFonts || {});
|
||||||
}
|
}
|
||||||
delete fallbacks[name];
|
delete fallbacks[name];
|
||||||
fs.readFile(filename, (err, data) => {
|
|
||||||
if (err) {
|
try {
|
||||||
console.error(`ERROR: Font not found: ${name}`);
|
const data = await readFile(filename);
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
'ERROR: Font not found: %s, Error: %s',
|
||||||
|
filename.replace(/\n|\r/g, ''),
|
||||||
|
String(err),
|
||||||
|
);
|
||||||
if (fallbacks && Object.keys(fallbacks).length) {
|
if (fallbacks && Object.keys(fallbacks).length) {
|
||||||
let fallbackName;
|
let fallbackName;
|
||||||
|
|
||||||
|
@ -186,32 +313,37 @@ const getFontPbf = (allowedFonts, fontPath, name, range, fallbacks) =>
|
||||||
fallbackName = Object.keys(fallbacks)[0];
|
fallbackName = Object.keys(fallbacks)[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.error(
|
||||||
console.error(`ERROR: Trying to use ${fallbackName} as a fallback`);
|
`ERROR: Trying to use %s as a fallback for: %s`,
|
||||||
delete fallbacks[fallbackName];
|
fallbackName,
|
||||||
getFontPbf(null, fontPath, fallbackName, range, fallbacks).then(
|
sanitizedName,
|
||||||
resolve,
|
|
||||||
reject,
|
|
||||||
);
|
);
|
||||||
|
delete fallbacks[fallbackName];
|
||||||
|
return getFontPbf(null, fontPath, fallbackName, range, fallbacks);
|
||||||
} else {
|
} else {
|
||||||
reject(`Font load error: ${name}`);
|
throw new Error('Font load error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resolve(data);
|
throw new Error('Font not allowed');
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
} else {
|
/**
|
||||||
reject(`Font not allowed: ${name}`);
|
* Combines multiple font pbf buffers into one.
|
||||||
}
|
* @param {object} allowedFonts - An object of allowed fonts.
|
||||||
});
|
* @param {string} fontPath - The path to the font directory.
|
||||||
|
* @param {string} names - Comma-separated font names.
|
||||||
export const getFontsPbf = async (
|
* @param {string} range - The range of the font (e.g., '0-255').
|
||||||
|
* @param {object} [fallbacks] - Fallback font list.
|
||||||
|
* @returns {Promise<Buffer>} - A promise that resolves to the combined font data buffer.
|
||||||
|
*/
|
||||||
|
export async function getFontsPbf(
|
||||||
allowedFonts,
|
allowedFonts,
|
||||||
fontPath,
|
fontPath,
|
||||||
names,
|
names,
|
||||||
range,
|
range,
|
||||||
fallbacks,
|
fallbacks,
|
||||||
) => {
|
) {
|
||||||
const fonts = names.split(',');
|
const fonts = names.split(',');
|
||||||
const queue = [];
|
const queue = [];
|
||||||
for (const font of fonts) {
|
for (const font of fonts) {
|
||||||
|
@ -228,9 +360,14 @@ export const getFontsPbf = async (
|
||||||
|
|
||||||
const combined = combine(await Promise.all(queue), names);
|
const combined = combine(await Promise.all(queue), names);
|
||||||
return Buffer.from(combined.buffer, 0, combined.buffer.length);
|
return Buffer.from(combined.buffer, 0, combined.buffer.length);
|
||||||
};
|
}
|
||||||
|
|
||||||
export const listFonts = async (fontPath) => {
|
/**
|
||||||
|
* Lists available fonts in a given font directory.
|
||||||
|
* @param {string} fontPath - The path to the font directory.
|
||||||
|
* @returns {Promise<object>} - Promise that resolves with an object where keys are the font names.
|
||||||
|
*/
|
||||||
|
export async function listFonts(fontPath) {
|
||||||
const existingFonts = {};
|
const existingFonts = {};
|
||||||
|
|
||||||
const files = await fsPromises.readdir(fontPath);
|
const files = await fsPromises.readdir(fontPath);
|
||||||
|
@ -245,9 +382,14 @@ export const listFonts = async (fontPath) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return existingFonts;
|
return existingFonts;
|
||||||
};
|
}
|
||||||
|
|
||||||
export const isValidHttpUrl = (string) => {
|
/**
|
||||||
|
* Checks if a string is a valid HTTP or HTTPS URL.
|
||||||
|
* @param {string} string - The string to validate.
|
||||||
|
* @returns {boolean} True if the string is a valid HTTP/HTTPS URL, false otherwise.
|
||||||
|
*/
|
||||||
|
export function isValidHttpUrl(string) {
|
||||||
let url;
|
let url;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -257,4 +399,32 @@ export const isValidHttpUrl = (string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return url.protocol === 'http:' || url.protocol === 'https:';
|
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||||
};
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches tile data from either PMTiles or MBTiles source.
|
||||||
|
* @param {object} source - The source object, which may contain a mbtiles object, or pmtiles object.
|
||||||
|
* @param {string} sourceType - The source type, which should be `pmtiles` or `mbtiles`
|
||||||
|
* @param {number} z - The zoom level.
|
||||||
|
* @param {number} x - The x coordinate of the tile.
|
||||||
|
* @param {number} y - The y coordinate of the tile.
|
||||||
|
* @returns {Promise<object | null>} - A promise that resolves to an object with data and headers or null if no data is found.
|
||||||
|
*/
|
||||||
|
export async function fetchTileData(source, sourceType, z, x, y) {
|
||||||
|
if (sourceType === 'pmtiles') {
|
||||||
|
return await new Promise(async (resolve) => {
|
||||||
|
const tileinfo = await getPMtilesTile(source, z, x, y);
|
||||||
|
if (!tileinfo?.data) return resolve(null);
|
||||||
|
resolve({ data: tileinfo.data, headers: tileinfo.header });
|
||||||
|
});
|
||||||
|
} else if (sourceType === 'mbtiles') {
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
source.getTile(z, x, y, (err, tileData, tileHeader) => {
|
||||||
|
if (err) {
|
||||||
|
return resolve(null);
|
||||||
|
}
|
||||||
|
resolve({ data: tileData, headers: tileHeader });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,10 +7,10 @@ import { server } from '../src/server.js';
|
||||||
global.expect = expect;
|
global.expect = expect;
|
||||||
global.supertest = supertest;
|
global.supertest = supertest;
|
||||||
|
|
||||||
before(function () {
|
before(async function () {
|
||||||
console.log('global setup');
|
console.log('global setup');
|
||||||
process.chdir('test_data');
|
process.chdir('test_data');
|
||||||
const running = server({
|
const running = await server({
|
||||||
configPath: 'config.json',
|
configPath: 'config.json',
|
||||||
port: 8888,
|
port: 8888,
|
||||||
publicUrl: '/test/',
|
publicUrl: '/test/',
|
||||||
|
|
|
@ -78,7 +78,7 @@ describe('Static endpoints', function () {
|
||||||
testStatic(prefix, '0,0,0/256x256', 'png', 404, 1);
|
testStatic(prefix, '0,0,0/256x256', 'png', 404, 1);
|
||||||
|
|
||||||
testStatic(prefix, '0,0,-1/256x256', 'png', 404);
|
testStatic(prefix, '0,0,-1/256x256', 'png', 404);
|
||||||
testStatic(prefix, '0,0,0/256.5x256.5', 'png', 404);
|
testStatic(prefix, '0,0,0/256.5x256.5', 'png', 400);
|
||||||
|
|
||||||
testStatic(prefix, '0,0,0,/256x256', 'png', 404);
|
testStatic(prefix, '0,0,0,/256x256', 'png', 404);
|
||||||
testStatic(prefix, '0,0,0,0,/256x256', 'png', 404);
|
testStatic(prefix, '0,0,0,0,/256x256', 'png', 404);
|
||||||
|
@ -135,7 +135,7 @@ describe('Static endpoints', function () {
|
||||||
|
|
||||||
testStatic(prefix, '0,0,1,1/1x1', 'gif', 400);
|
testStatic(prefix, '0,0,1,1/1x1', 'gif', 400);
|
||||||
|
|
||||||
testStatic(prefix, '-180,-80,180,80/0.5x2.6', 'png', 404);
|
testStatic(prefix, '-180,-80,180,80/0.5x2.6', 'png', 400);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -60,16 +60,16 @@ describe('Raster tiles', function () {
|
||||||
|
|
||||||
describe('invalid requests return 4xx', function () {
|
describe('invalid requests return 4xx', function () {
|
||||||
testTile('non_existent', 256, 0, 0, 0, 'png', 404);
|
testTile('non_existent', 256, 0, 0, 0, 'png', 404);
|
||||||
testTile(prefix, 256, -1, 0, 0, 'png', 404);
|
testTile(prefix, 256, -1, 0, 0, 'png', 400);
|
||||||
testTile(prefix, 256, 25, 0, 0, 'png', 404);
|
testTile(prefix, 256, 25, 0, 0, 'png', 400);
|
||||||
testTile(prefix, 256, 0, 1, 0, 'png', 404);
|
testTile(prefix, 256, 0, 1, 0, 'png', 400);
|
||||||
testTile(prefix, 256, 0, 0, 1, 'png', 404);
|
testTile(prefix, 256, 0, 0, 1, 'png', 400);
|
||||||
testTile(prefix, 256, 0, 0, 0, 'gif', 400);
|
testTile(prefix, 256, 0, 0, 0, 'gif', 400);
|
||||||
testTile(prefix, 256, 0, 0, 0, 'pbf', 400);
|
testTile(prefix, 256, 0, 0, 0, 'pbf', 400);
|
||||||
|
|
||||||
testTile(prefix, 256, 0, 0, 0, 'png', 404, 1);
|
testTile(prefix, 256, 0, 0, 0, 'png', 400, 1);
|
||||||
testTile(prefix, 256, 0, 0, 0, 'png', 404, 5);
|
testTile(prefix, 256, 0, 0, 0, 'png', 400, 5);
|
||||||
|
|
||||||
testTile(prefix, 300, 0, 0, 0, 'png', 404);
|
testTile(prefix, 300, 0, 0, 0, 'png', 400);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue