Merge branch 'express-5-v2' into express-5

This commit is contained in:
acalcutt 2025-01-02 18:30:36 -05:00
commit 71335872bc
9 changed files with 340 additions and 66 deletions

View file

@ -238,9 +238,26 @@ For example::
} }
} }
The data source does not need to be specified here unless you explicitly want to serve the raw data. The data source does not need to be specified here unless you explicitly want to serve the raw data.
Serving Terrain Tiles
--------------
If you serve terrain tiles, it is possible to configure an ``encoding`` with ``mapbox`` or ``terrarium`` to enable a terrain preview mode and the ``elevation`` api for the ``data`` endpoint.
For example::
"data": {
"terrain1": {
"mbtiles": "terrain1.mbtiles",
"encoding": "mapbox"
},
"terrain2": {
"pmtiles": "terrain2.pmtiles"
"encoding": "terrarium"
}
}
Referencing local files from style JSON Referencing local files from style JSON
======================================= =======================================
@ -283,7 +300,7 @@ For example::
"source3": { "source3": {
"url": "pmtiles://https://foo.lan/source3.pmtiles", "url": "pmtiles://https://foo.lan/source3.pmtiles",
"type": "vector" "type": "vector"
}, }
} }
Alternatively, you can use ``pmtiles://{source2}`` to reference existing data object from the config. Alternatively, you can use ``pmtiles://{source2}`` to reference existing data object from the config.

View file

@ -100,6 +100,14 @@ Source data
* TileJSON at ``/data/{id}.json`` * TileJSON at ``/data/{id}.json``
* If terrain mbtile data is served and ``encoding`` is configured (see config) the elevation can be queried
* by ``/data/{id}/elevation/{z}/{x}/{y}`` for the tile
* or ``/data/{id}/elevation/{z}/{long}/{lat}`` for the coordinate
* the result will be a json object like ``{"z":7,"x":68,"y":45,"red":134,"green":66,"blue":0,"latitude":11.84069,"longitude":46.04798,"elevation":1602}``
Static files Static files
=========== ===========
* Static files are served at ``/files/{filename}`` * Static files are served at ``/files/{filename}``

View file

@ -7,6 +7,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"test": "mocha test/**.js --timeout 10000 --exit", "test": "mocha test/**.js --timeout 10000 --exit",
"test-docker": "xvfb-run npm test",
"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",

View file

@ -114,7 +114,7 @@ section {
} }
.details h3 { .details h3 {
font-size: 18px; font-size: 18px;
margin-top: 25px; margin-top: 5px;
} }
.details p { .details p {
padding: 0; padding: 0;

View file

@ -4,20 +4,25 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{name}} - TileServer GL</title> <title>{{name}} - TileServer GL</title>
{{#is_vector}} {{#use_maplibre}}
<link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl.css{{&key_query}}" /> <link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl.css{{&key_query}}" />
<link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl-inspect.css{{&key_query}}" /> <link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl-inspect.css{{&key_query}}" />
<script src="{{public_url}}maplibre-gl.js{{&key_query}}"></script> <script src="{{public_url}}maplibre-gl.js{{&key_query}}"></script>
<script src="{{public_url}}maplibre-gl-inspect.js{{&key_query}}"></script> <script src="{{public_url}}maplibre-gl-inspect.js{{&key_query}}"></script>
<style> <style>
body {background:#fff;color:#333;font-family:Arial, sans-serif;} body {background:#fff;color:#333;font-family:Arial, sans-serif;}
{{^is_terrain}}
#map {position:absolute;top:0;left:0;right:250px;bottom:0;} #map {position:absolute;top:0;left:0;right:250px;bottom:0;}
{{/is_terrain}}
{{#is_terrain}}
#map { position:absolute; top:0; bottom:0; width:100%; }
{{/is_terrain}}
h1 {position:absolute;top:5px;right:0;width:240px;margin:0;line-height:20px;font-size:20px;} h1 {position:absolute;top:5px;right:0;width:240px;margin:0;line-height:20px;font-size:20px;}
#layerList {position:absolute;top:35px;right:0;bottom:0;width:240px;overflow:auto;} #layerList {position:absolute;top:35px;right:0;bottom:0;width:240px;overflow:auto;}
#layerList div div {width:15px;height:15px;display:inline-block;} #layerList div div {width:15px;height:15px;display:inline-block;}
</style> </style>
{{/is_vector}} {{/use_maplibre}}
{{^is_vector}} {{^use_maplibre}}
<link rel="stylesheet" type="text/css" href="{{public_url}}leaflet.css{{&key_query}}" /> <link rel="stylesheet" type="text/css" href="{{public_url}}leaflet.css{{&key_query}}" />
<script src="{{public_url}}leaflet.js{{&key_query}}"></script> <script src="{{public_url}}leaflet.js{{&key_query}}"></script>
<script src="{{public_url}}leaflet-hash.js{{&key_query}}"></script> <script src="{{public_url}}leaflet-hash.js{{&key_query}}"></script>
@ -37,23 +42,22 @@
background-image: url({{public_url}}images/marker-icon.png{{&key_query}}); background-image: url({{public_url}}images/marker-icon.png{{&key_query}});
} }
</style> </style>
{{/is_vector}} {{/use_maplibre}}
</head> </head>
<body> <body>
{{#is_vector}} {{#use_maplibre}}
<h1>{{name}}</h1> <h1>{{name}}</h1>
<div id="map"></div> <div id="map"></div>
{{^is_terrain}}
<div id="layerList"></div> <div id="layerList"></div>
<pre id="propertyList"></pre> <pre id="propertyList"></pre>
{{/is_terrain}}
<script> <script>
var keyMatch = location.search.match(/[\?\&]key=([^&]+)/i); var keyMatch = location.search.match(/[\?\&]key=([^&]+)/i);
var keyParam = keyMatch ? '?key=' + keyMatch[1] : ''; var keyParam = keyMatch ? '?key=' + keyMatch[1] : '';
var map = new maplibregl.Map({ {{^is_terrain}}
container: 'map', var style = {
hash: true,
maxPitch: 85,
style: {
version: 8, version: 8,
sources: { sources: {
'vector_layer_': { 'vector_layer_': {
@ -62,37 +66,99 @@
} }
}, },
layers: [] layers: []
} };
}); {{/is_terrain}}
map.addControl(new maplibregl.NavigationControl()); {{#is_terrain}}
var inspect = new MaplibreInspect({ var style = {
showInspectMap: true, version: 8,
showInspectButton: false sources: {
}); "terrain": {
map.addControl(inspect); "type": "raster-dem",
map.on('styledata', function() { "url": "{{public_url}}data/{{id}}.json",
var layerList = document.getElementById('layerList'); "encoding": "{{terrain_encoding}}"
layerList.innerHTML = ''; },
Object.keys(inspect.sources).forEach(function(sourceId) { "hillshade": {
var layerIds = inspect.sources[sourceId]; "type": "raster-dem",
layerIds.forEach(function(layerId) { "url": "{{public_url}}data/{{id}}.json",
var item = document.createElement('div'); "encoding": "{{terrain_encoding}}"
item.innerHTML = '<div style="' + }
'background:' + inspect.assignLayerColor(layerId) + ';' + },
'"></div> ' + layerId; "terrain": {
layerList.appendChild(item); "source": "terrain"
}); },
}) layers: [
}); {
"id": "background",
"paint": {
{{^if is_terrainrgb}}
"background-color": "hsl(190, 99%, 63%)"
{{else}}
"background-color": "hsl(0, 100%, 25%)"
{{/if}}
},
"type": "background"
},
{
"id": "hillshade",
"source": "hillshade",
"type": "hillshade",
"paint": {
"hillshade-shadow-color": "hsl(39, 21%, 33%)",
"hillshade-illumination-direction": 315,
"hillshade-exaggeration": 0.8
}
}
]
};
{{/is_terrain}}
var map = new maplibregl.Map({
container: 'map',
hash: true,
maxPitch: 85,
style: style
});
map.addControl(new maplibregl.NavigationControl({
visualizePitch: true,
showZoom: true,
showCompass: true
}));
{{#is_terrain}}
map.addControl(
new maplibregl.TerrainControl({
source: "terrain",
})
);
{{/is_terrain}}
{{^is_terrain}}
var inspect = new MaplibreInspect({
showInspectMap: true,
showInspectButton: false
});
map.addControl(inspect);
map.on('styledata', function() {
var layerList = document.getElementById('layerList');
layerList.innerHTML = '';
Object.keys(inspect.sources).forEach(function(sourceId) {
var layerIds = inspect.sources[sourceId];
layerIds.forEach(function(layerId) {
var item = document.createElement('div');
item.innerHTML = '<div style="' +
'background:' + inspect.assignLayerColor(layerId) + ';' +
'"></div> ' + layerId;
layerList.appendChild(item);
});
})
});
{{/is_terrain}}
</script> </script>
{{/is_vector}} {{/use_maplibre}}
{{^is_vector}} {{^use_maplibre}}
<h1 style="display:none;">{{name}}</h1> <h1 style="display:none;">{{name}}</h1>
<div id='map'></div> <div id='map'></div>
<script> <script>
var keyMatch = location.search.match(/[\?\&]key=([^&]+)/i); var keyMatch = location.search.match(/[\?\&]key=([^&]+)/i);
var keyParam = keyMatch ? '?key=' + keyMatch[1] : ''; var keyParam = keyMatch ? '?key=' + keyMatch[1] : '';
var map = L.map('map', { zoomControl: false }); var map = L.map('map', { zoomControl: false });
new L.Control.Zoom({ position: 'topright' }).addTo(map); new L.Control.Zoom({ position: 'topright' }).addTo(map);
@ -129,7 +195,7 @@
attribution: tile_attribution attribution: tile_attribution
}).addTo(map); }).addTo(map);
} }
map.eachLayer(function(layer) { map.eachLayer(function(layer) {
// do not add scale prefix even if retina display is detected // do not add scale prefix even if retina display is detected
layer.scalePrefix = '.'; layer.scalePrefix = '.';
@ -141,6 +207,6 @@
new L.Hash(map); new L.Hash(map);
}, 0); }, 0);
</script> </script>
{{/is_vector}} {{/use_maplibre}}
</body> </body>
</html> </html>

View file

@ -6,10 +6,15 @@
<title>TileServer GL - Server for vector and raster maps with GL styles</title> <title>TileServer GL - Server for vector and raster maps with GL styles</title>
<link rel="stylesheet" type="text/css" href="{{public_url}}index.css{{&key_query}}" /> <link rel="stylesheet" type="text/css" href="{{public_url}}index.css{{&key_query}}" />
<script> <script>
function toggle_xyz(id) { function toggle_link(id, link) {
var el = document.getElementById(id); var el = document.getElementById(id);
var s = el.style; var s = el.style;
s.display = s.display == 'none' ? 'inline-block' : 'none'; if (s.display == 'none') {
s.display = 'inline-block';
} else if (el.value == link) {
s.display = 'none';
}
el.value = link;
el.setSelectionRange(0, el.value.length); el.setSelectionRange(0, el.value.length);
return false; return false;
} }
@ -37,7 +42,7 @@
<div class="filter-details"> <div class="filter-details">
<h3>Filter styles and data by name or identifier</h3> <h3>Filter styles and data by name or identifier</h3>
<!-- filter input , needs to call filter() when content changes...--> <!-- filter input , needs to call filter() when content changes...-->
<input id="filter" type="text" oninput="filter()" placeholder="Start typing name or identifier" /> <input id="filter" type="text" oninput="filter()" placeholder="Start typing name or identifier" autofocus />
</div> </div>
</div> </div>
</div> </div>
@ -66,8 +71,8 @@
| <a href="{{public_url}}styles/{{@key}}/wmts.xml{{&../key_query}}">WMTS</a> | <a href="{{public_url}}styles/{{@key}}/wmts.xml{{&../key_query}}">WMTS</a>
{{/if}} {{/if}}
{{#if xyz_link}} {{#if xyz_link}}
| <a href="#" onclick="return toggle_xyz('xyz_style_{{@key}}');">XYZ</a> | <a href="#" onclick="return toggle_link('xyz_style_{{@key}}', '{{&xyz_link}}');">XYZ</a>
<input id="xyz_style_{{@key}}" type="text" value="{{&xyz_link}}" style="display:none;" /> <input id="xyz_style_{{@key}}" type="text" value="" style="display:none;" />
{{/if}} {{/if}}
</p> </p>
</div> </div>
@ -105,9 +110,12 @@
<p class="services"> <p class="services">
services: <a href="{{public_url}}data/{{@key}}.json{{&../key_query}}">TileJSON</a> services: <a href="{{public_url}}data/{{@key}}.json{{&../key_query}}">TileJSON</a>
{{#if xyz_link}} {{#if xyz_link}}
| <a href="#" onclick="return toggle_xyz('xyz_data_{{@key}}');">XYZ</a> | <a href="#" onclick="return toggle_link('link_data_{{@key}}', '{{&xyz_link}}');">XYZ</a>
<input id="xyz_data_{{@key}}" type="text" value="{{&xyz_link}}" style="display:none;" />
{{/if}} {{/if}}
{{#if elevation_link}}
| <a href="#" onclick="return toggle_link('link_data_{{@key}}', '{{&elevation_link}}');">Elevation</a>
{{/if}}
<input id="link_data_{{@key}}" type="text" value="" style="display:none;" />
</p> </p>
</div> </div>
<div class="viewers"> <div class="viewers">
@ -116,6 +124,9 @@
{{/is_vector}} {{/is_vector}}
{{^is_vector}} {{^is_vector}}
<a class="btn" href="{{public_url}}data/{{@key}}/{{&../key_query}}{{viewer_hash}}">View</a> <a class="btn" href="{{public_url}}data/{{@key}}/{{&../key_query}}{{viewer_hash}}">View</a>
{{#elevation_link}}
<a class="btn" href="{{public_url}}data/preview/{{@key}}/{{&../key_query}}{{viewer_hash}}">Preview Terrain</a>
{{/elevation_link}}
{{/is_vector}} {{/is_vector}}
</div> </div>
</div> </div>

View file

@ -7,6 +7,9 @@ import clone from 'clone';
import express from 'express'; import express from 'express';
import Pbf from 'pbf'; import Pbf from 'pbf';
import { VectorTile } from '@mapbox/vector-tile'; import { VectorTile } from '@mapbox/vector-tile';
import SphericalMercator from '@mapbox/sphericalmercator';
import { Image, createCanvas } from 'canvas';
import sharp from 'sharp';
import { import {
fixTileJSONCenter, fixTileJSONCenter,
@ -120,6 +123,142 @@ export const serve_data = {
return res.status(200).send(data); return res.status(200).send(data);
}); });
app.get('/:id/elevation/:z/:x/:y', async (req, res, next) => {
try {
const item = repo?.[req.params.id];
if (!item) return res.sendStatus(404);
if (!item.source) return res.status(404).send('Missing source');
if (!item.tileJSON) return res.status(404).send('Missing tileJSON');
if (!item.sourceType) return res.status(404).send('Missing sourceType');
const { source, tileJSON, sourceType } = item;
if (sourceType !== 'pmtiles' && sourceType !== 'mbtiles') {
return res
.status(400)
.send('Invalid sourceType. Must be pmtiles or mbtiles.');
}
const encoding = tileJSON?.encoding;
if (encoding == null) {
return res.status(400).send('Missing tileJSON.encoding');
} else if (encoding !== 'terrarium' && encoding !== 'mapbox') {
return res
.status(400)
.send('Invalid encoding. Must be terrarium or mapbox.');
}
const format = tileJSON?.format;
if (format == null) {
return res.status(400).send('Missing tileJSON.format');
} else if (format !== 'webp' && format !== 'png') {
return res.status(400).send('Invalid format. Must be webp or png.');
}
const z = parseInt(req.params.z, 10);
const x = parseFloat(req.params.x);
const y = parseFloat(req.params.y);
if (tileJSON.minzoom == null || tileJSON.maxzoom == null) {
return res.status(404).send(JSON.stringify(tileJSON));
}
const TILE_SIZE = 256;
let tileCenter;
let xy;
if (Number.isInteger(x) && Number.isInteger(y)) {
const intX = parseInt(req.params.x, 10);
const intY = parseInt(req.params.y, 10);
if (
z < tileJSON.minzoom ||
z > tileJSON.maxzoom ||
intX < 0 ||
intY < 0 ||
intX >= Math.pow(2, z) ||
intY >= Math.pow(2, z)
) {
return res.status(404).send('Out of bounds');
}
xy = [intX, intY];
tileCenter = new SphericalMercator().bbox(intX, intY, z);
} else {
if (
z < tileJSON.minzoom ||
z > tileJSON.maxzoom ||
x < -180 ||
y < -90 ||
x > 180 ||
y > 90
) {
return res.status(404).send('Out of bounds');
}
tileCenter = [y, x, y + 0.1, x + 0.1];
const { minX, minY } = new SphericalMercator().xyz(tileCenter, z);
xy = [minX, minY];
}
const fetchTile = await fetchTileData(source, sourceType, z, x, y);
if (fetchTile == null) return res.status(204).send();
let data = fetchTile.data;
const image = new Image();
await new Promise(async (resolve, reject) => {
image.onload = async () => {
const canvas = createCanvas(TILE_SIZE, TILE_SIZE);
const context = canvas.getContext('2d');
context.drawImage(image, 0, 0);
const imgdata = context.getImageData(0, 0, TILE_SIZE, TILE_SIZE);
const arrayWidth = imgdata.width;
const arrayHeight = imgdata.height;
const bytesPerPixel = 4;
const xPixel = Math.floor(xy[0]);
const yPixel = Math.floor(xy[1]);
if (
xPixel < 0 ||
yPixel < 0 ||
xPixel >= arrayWidth ||
yPixel >= arrayHeight
) {
return reject('Out of bounds Pixel');
}
const index = (yPixel * arrayWidth + xPixel) * bytesPerPixel;
const red = imgdata.data[index];
const green = imgdata.data[index + 1];
const blue = imgdata.data[index + 2];
let elevation;
if (encoding === 'mapbox') {
elevation = -10000 + (red * 256 * 256 + green * 256 + blue) * 0.1;
} else if (encoding === 'terrarium') {
elevation = red * 256 + green + blue / 256 - 32768;
} else {
elevation = 'invalid encoding';
}
resolve(
res.status(200).send({
z,
x: xy[0],
y: xy[1],
red,
green,
blue,
latitude: tileCenter[0],
longitude: tileCenter[1],
elevation,
}),
);
};
image.onerror = (err) => reject(err);
if (format === 'webp') {
try {
const img = await sharp(data).toFormat('png').toBuffer();
image.src = img;
} catch (err) {
reject(err);
}
} else {
image.src = data;
}
});
} catch (err) {
return res
.status(500)
.header('Content-Type', 'text/plain')
.send(err.message);
}
});
app.get('/:id.json', (req, res) => { app.get('/:id.json', (req, res) => {
const item = repo[req.params.id]; const item = repo[req.params.id];
if (!item) { if (!item) {
@ -193,6 +332,7 @@ export const serve_data = {
sourceType = 'pmtiles'; sourceType = 'pmtiles';
const metadata = await getPMtilesInfo(source); const metadata = await getPMtilesInfo(source);
tileJSON['encoding'] = params['encoding'];
tileJSON['name'] = id; tileJSON['name'] = id;
tileJSON['format'] = 'pbf'; tileJSON['format'] = 'pbf';
Object.assign(tileJSON, metadata); Object.assign(tileJSON, metadata);
@ -213,6 +353,7 @@ export const serve_data = {
const mbw = await openMbTilesWrapper(inputFile); const mbw = await openMbTilesWrapper(inputFile);
const info = await mbw.getInfo(); const info = await mbw.getInfo();
source = mbw.getMbTiles(); source = mbw.getMbTiles();
tileJSON['encoding'] = params['encoding'];
tileJSON['name'] = id; tileJSON['name'] = id;
tileJSON['format'] = 'pbf'; tileJSON['format'] = 'pbf';

View file

@ -493,7 +493,7 @@ async function start(opts) {
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 = `${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;
@ -522,14 +522,6 @@ async function start(opts) {
)}/${center[0].toFixed(5)}`; )}/${center[0].toFixed(5)}`;
} }
data.is_vector = tileJSON.format === 'pbf';
if (!data.is_vector) {
if (center) {
const centerPx = mercator.px([center[0], center[1]], center[2]);
data.thumbnail = `${center[2]}/${Math.floor(centerPx[0] / 256)}/${Math.floor(centerPx[1] / 256)}.${tileJSON.format}`;
}
}
const tileSize = undefined; const tileSize = undefined;
data.xyz_link = getTileUrls( data.xyz_link = getTileUrls(
req, req,
@ -543,6 +535,24 @@ async function start(opts) {
}, },
)[0]; )[0];
data.is_vector = tileJSON.format === 'pbf';
if (!data.is_vector) {
if (
tileJSON.encoding === 'terrarium' ||
tileJSON.encoding === 'mapbox'
) {
data.elevation_link = getTileUrls(
req,
tileJSON.tiles,
`data/${id}/elevation`,
)[0];
}
if (center) {
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}`;
}
}
if (data.filesize) { if (data.filesize) {
let suffix = 'kB'; let suffix = 'kB';
let size = parseInt(tileJSON.filesize, 10) / 1024; let size = parseInt(tileJSON.filesize, 10) / 1024;
@ -611,17 +621,25 @@ async function start(opts) {
}; };
}); });
serveTemplate('/data/:id/', 'data', (req) => { serveTemplate('/data{/:view}/:id/', 'data', (req) => {
const { id } = req.params; const { id, view } = req.params;
const data = serving.data[id]; const data = serving.data[id];
if (!data) { if (!data) {
return null; return null;
} }
const is_terrain =
(data.tileJSON.encoding === 'terrarium' ||
data.tileJSON.encoding === 'mapbox') &&
view === 'preview';
return { return {
...data, ...data,
id, id,
is_vector: data.tileJSON.format === 'pbf', use_maplibre: data.tileJSON.format === 'pbf' || is_terrain,
is_terrain: is_terrain,
is_terrainrgb: data.tileJSON.encoding === 'mapbox',
terrain_encoding: data.tileJSON.encoding,
}; };
}); });

View file

@ -52,6 +52,12 @@ function getUrlObject(req) {
// 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;
// support overriding port by sending X-Forwarded-Port http header
const xForwardedPort = req.get('X-Forwarded-Port');
if (xForwardedPort) {
urlObject.port = xForwardedPort;
}
// support add url prefix by sending X-Forwarded-Path http header // support add url prefix by sending X-Forwarded-Path http header
const xForwardedPath = req.get('X-Forwarded-Path'); const xForwardedPath = req.get('X-Forwarded-Path');
if (xForwardedPath) { if (xForwardedPath) {
@ -138,16 +144,22 @@ export function getTileUrls(
tileParams = `${tileSize}/{z}/{x}/{y}`; tileParams = `${tileSize}/{z}/{x}/{y}`;
} }
if (format && format != '') {
format = `.${format}`;
} else {
format = '';
}
const uris = []; const uris = [];
if (!publicUrl) { if (!publicUrl) {
let xForwardedPath = `${req.get('X-Forwarded-Path') ? '/' + req.get('X-Forwarded-Path') : ''}`; let xForwardedPath = `${req.get('X-Forwarded-Path') ? '/' + req.get('X-Forwarded-Path') : ''}`;
for (const domain of domains) { for (const domain of domains) {
uris.push( uris.push(
`${req.protocol}://${domain}${xForwardedPath}/${path}/${tileParams}.${format}${query}`, `${req.protocol}://${domain}${xForwardedPath}/${path}/${tileParams}${format}${query}`,
); );
} }
} else { } else {
uris.push(`${publicUrl}${path}/${tileParams}.${format}${query}`); uris.push(`${publicUrl}${path}/${tileParams}${format}${query}`);
} }
return uris; return uris;