Skip to content

Commit

Permalink
Feature: Terrain Preview and simple Elevation Query (#1425)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* add encoding param to pmtile section

* add map controls

Co-authored-by: Andrew Calcutt <[email protected]>

* 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 <[email protected]>
  • Loading branch information
3 people authored Jan 2, 2025
1 parent 93f72c1 commit a2bc9f0
Show file tree
Hide file tree
Showing 9 changed files with 375 additions and 67 deletions.
21 changes: 19 additions & 2 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

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
=======================================

Expand Down Expand Up @@ -283,7 +300,7 @@ For example::
"source3": {
"url": "pmtiles://https://foo.lan/source3.pmtiles",
"type": "vector"
},
}
}

Alternatively, you can use ``pmtiles://{source2}`` to reference existing data object from the config.
Expand Down
8 changes: 8 additions & 0 deletions docs/endpoints.rst
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ Source data

* 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 are served at ``/files/{filename}``
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"type": "module",
"scripts": {
"test": "mocha test/**.js --timeout 10000 --exit",
"test-docker": "xvfb-run npm test",
"lint:yml": "yamllint --schema=CORE_SCHEMA *.{yml,yaml}",
"lint:js": "npm run lint:eslint && npm run lint:prettier",
"lint:js:fix": "npm run lint:eslint:fix && npm run lint:prettier:fix",
Expand Down
2 changes: 1 addition & 1 deletion public/resources/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ section {
}
.details h3 {
font-size: 18px;
margin-top: 25px;
margin-top: 5px;
}
.details p {
padding: 0;
Expand Down
150 changes: 108 additions & 42 deletions public/templates/data.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<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-inspect.css{{&key_query}}" />
<script src="{{public_url}}maplibre-gl.js{{&key_query}}"></script>
<script src="{{public_url}}maplibre-gl-inspect.js{{&key_query}}"></script>
<style>
body {background:#fff;color:#333;font-family:Arial, sans-serif;}
{{^is_terrain}}
#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;}
#layerList {position:absolute;top:35px;right:0;bottom:0;width:240px;overflow:auto;}
#layerList div div {width:15px;height:15px;display:inline-block;}
</style>
{{/is_vector}}
{{^is_vector}}
{{/use_maplibre}}
{{^use_maplibre}}
<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-hash.js{{&key_query}}"></script>
Expand All @@ -37,23 +42,22 @@
background-image: url({{public_url}}images/marker-icon.png{{&key_query}});
}
</style>
{{/is_vector}}
{{/use_maplibre}}
</head>
<body>
{{#is_vector}}
{{#use_maplibre}}
<h1>{{name}}</h1>
<div id="map"></div>
{{^is_terrain}}
<div id="layerList"></div>
<pre id="propertyList"></pre>
{{/is_terrain}}
<script>
var keyMatch = location.search.match(/[\?\&]key=([^&]+)/i);
var keyParam = keyMatch ? '?key=' + keyMatch[1] : '';

var map = new maplibregl.Map({
container: 'map',
hash: true,
maxPitch: 85,
style: {
var keyMatch = location.search.match(/[\?\&]key=([^&]+)/i);
var keyParam = keyMatch ? '?key=' + keyMatch[1] : '';

{{^is_terrain}}
var style = {
version: 8,
sources: {
'vector_layer_': {
Expand All @@ -62,37 +66,99 @@
}
},
layers: []
}
});
map.addControl(new maplibregl.NavigationControl());
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}}
{{#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({
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>
{{/is_vector}}
{{^is_vector}}
{{/use_maplibre}}
{{^use_maplibre}}
<h1 style="display:none;">{{name}}</h1>
<div id='map'></div>
<script>
var keyMatch = location.search.match(/[\?\&]key=([^&]+)/i);
var keyParam = keyMatch ? '?key=' + keyMatch[1] : '';

var keyMatch = location.search.match(/[\?\&]key=([^&]+)/i);
var keyParam = keyMatch ? '?key=' + keyMatch[1] : '';
var map = L.map('map', { zoomControl: false });
new L.Control.Zoom({ position: 'topright' }).addTo(map);

Expand Down Expand Up @@ -129,7 +195,7 @@
attribution: tile_attribution
}).addTo(map);
}

map.eachLayer(function(layer) {
// do not add scale prefix even if retina display is detected
layer.scalePrefix = '.';
Expand All @@ -141,6 +207,6 @@
new L.Hash(map);
}, 0);
</script>
{{/is_vector}}
{{/use_maplibre}}
</body>
</html>
25 changes: 18 additions & 7 deletions public/templates/index.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@
<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}}" />
<script>
function toggle_xyz(id) {
function toggle_link(id, link) {
var el = document.getElementById(id);
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);
return false;
}
Expand Down Expand Up @@ -37,7 +42,7 @@
<div class="filter-details">
<h3>Filter styles and data by name or identifier</h3>
<!-- 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>
Expand Down Expand Up @@ -66,8 +71,8 @@
| <a href="{{public_url}}styles/{{@key}}/wmts.xml{{&../key_query}}">WMTS</a>
{{/if}}
{{#if xyz_link}}
| <a href="#" onclick="return toggle_xyz('xyz_style_{{@key}}');">XYZ</a>
<input id="xyz_style_{{@key}}" type="text" value="{{&xyz_link}}" style="display:none;" />
| <a href="#" onclick="return toggle_link('xyz_style_{{@key}}', '{{&xyz_link}}');">XYZ</a>
<input id="xyz_style_{{@key}}" type="text" value="" style="display:none;" />
{{/if}}
</p>
</div>
Expand Down Expand Up @@ -105,9 +110,12 @@
<p class="services">
services: <a href="{{public_url}}data/{{@key}}.json{{&../key_query}}">TileJSON</a>
{{#if xyz_link}}
| <a href="#" onclick="return toggle_xyz('xyz_data_{{@key}}');">XYZ</a>
<input id="xyz_data_{{@key}}" type="text" value="{{&xyz_link}}" style="display:none;" />
| <a href="#" onclick="return toggle_link('link_data_{{@key}}', '{{&xyz_link}}');">XYZ</a>
{{/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>
</div>
<div class="viewers">
Expand All @@ -116,6 +124,9 @@
{{/is_vector}}
{{^is_vector}}
<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}}
</div>
</div>
Expand Down
Loading

0 comments on commit a2bc9f0

Please sign in to comment.