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:
parent
93f72c1fe7
commit
a2bc9f0cce
9 changed files with 375 additions and 67 deletions
|
@ -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.
|
||||||
|
|
|
@ -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}``
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,9 +66,71 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
layers: []
|
layers: []
|
||||||
|
};
|
||||||
|
{{/is_terrain}}
|
||||||
|
{{#is_terrain}}
|
||||||
|
var style = {
|
||||||
|
version: 8,
|
||||||
|
sources: {
|
||||||
|
"terrain": {
|
||||||
|
"type": "raster-dem",
|
||||||
|
"url": "{{public_url}}data/{{id}}.json",
|
||||||
|
"encoding": "{{terrain_encoding}}"
|
||||||
|
},
|
||||||
|
"hillshade": {
|
||||||
|
"type": "raster-dem",
|
||||||
|
"url": "{{public_url}}data/{{id}}.json",
|
||||||
|
"encoding": "{{terrain_encoding}}"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"terrain": {
|
||||||
|
"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());
|
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({
|
var inspect = new MaplibreInspect({
|
||||||
showInspectMap: true,
|
showInspectMap: true,
|
||||||
showInspectButton: false
|
showInspectButton: false
|
||||||
|
@ -84,15 +150,15 @@
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
{{/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);
|
||||||
|
|
||||||
|
@ -141,6 +207,6 @@
|
||||||
new L.Hash(map);
|
new L.Hash(map);
|
||||||
}, 0);
|
}, 0);
|
||||||
</script>
|
</script>
|
||||||
{{/is_vector}}
|
{{/use_maplibre}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
10
src/utils.js
10
src/utils.js
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue