Feature: Terrain Preview and simple Elevation Query (#1425)

* add terrain preview and elevation link

* add elevation api for terrain tiles

* add documentation for elevation api

* applied lint:js:fix

* Add `test-docker` for test execution in docker build environment

* Fix too greedy router expression

* Add pmtile support

Co-authored-by: Andrew Calcutt <acalcutt@techidiots.net>

* add encoding param to pmtile section

* add map controls

Co-authored-by: Andrew Calcutt <acalcutt@techidiots.net>

* remove not needed check

* fix possible float usage in thumbnail url

* update readme for encoding option

* add better link name

---------

Co-authored-by: Miko <miko@none>
Co-authored-by: Andrew Calcutt <acalcutt@techidiots.net>
This commit is contained in:
Miko 2025-01-02 20:03:03 +01:00 committed by GitHub
parent 93f72c1fe7
commit a2bc9f0cce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 375 additions and 67 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 { fixTileJSONCenter, getTileUrls, isValidHttpUrl } from './utils.js'; import { fixTileJSONCenter, getTileUrls, isValidHttpUrl } from './utils.js';
import { import {
@ -162,6 +165,183 @@ export const serve_data = {
}, },
); );
app.get(
'^/:id/elevation/:z([0-9]+)/:x([-.0-9]+)/:y([-.0-9]+)',
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];
}
let data;
if (sourceType === 'pmtiles') {
const tileinfo = await getPMtilesTile(source, z, x, y);
if (!tileinfo?.data) return res.status(204).send();
data = tileinfo.data;
} else {
data = await new Promise((resolve, reject) => {
source.getTile(z, xy[0], xy[1], (err, tileData) => {
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');
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, next) => { app.get('/:id.json', (req, res, next) => {
const item = repo[req.params.id]; const item = repo[req.params.id];
if (!item) { if (!item) {
@ -225,6 +405,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);
@ -245,6 +426,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

@ -468,7 +468,7 @@ 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;
@ -498,14 +498,6 @@ 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,
@ -519,6 +511,24 @@ 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;
@ -595,18 +605,25 @@ function start(opts) {
}; };
}); });
serveTemplate('/data/:id/$', 'data', (req) => { serveTemplate('^/data/(:preview(preview)/)?:id/$', 'data', (req) => {
const { id } = req.params; const id = req.params.id;
const preview = req.params.preview || undefined;
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') &&
preview === '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

@ -119,16 +119,22 @@ export const 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;