Fix Elevation API and extend Elevation Preview (#1432)

* add elevation info in preview

* fix various issues
- pmtile coordinates
- pixel calculation
- swapped lat / long
- allow tileSize to be configured
- allow any zoom for coordinates

* apply linter changes

* remove not used entry

* drop baseUrl in favor of public_url

* fix map not using full body

* remove bounds check for coordinates

---------

Co-authored-by: Miko <miko@none>
This commit is contained in:
Miko 2025-01-10 16:51:17 +01:00 committed by GitHub
parent 9d222c1dec
commit 4a783446cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 118 additions and 40 deletions

View file

@ -0,0 +1,51 @@
class ElevationInfoControl {
constructor(options) {
this.url = options["url"];
}
getDefaultPosition() {
const defaultPosition = "bottom-left";
return defaultPosition;
}
onAdd(map) {
this.map = map;
this.controlContainer = document.createElement("div");
this.controlContainer.classList.add("maplibregl-ctrl");
this.controlContainer.classList.add("maplibregl-ctrl-group");
this.controlContainer.classList.add("maplibre-ctrl-elevation");
this.controlContainer.textContent = "Elevation: Click on Map";
map.on('click', (e) => {
var url = this.url;
var coord = {"z": Math.floor(map.getZoom()), "x": e.lngLat["lng"], "y": e.lngLat["lat"]};
for(var key in coord) {
url = url.replace(new RegExp('{'+ key +'}','g'), coord[key]);
}
let request = new XMLHttpRequest();
request.open("GET", url, true);
request.onload = () => {
if (request.status !== 200) {
this.controlContainer.textContent = "Elevation: No value";
} else {
this.controlContainer.textContent = `Elevation: ${JSON.parse(request.responseText).elevation} (${JSON.stringify(coord)})`;
}
}
request.send();
});
return this.controlContainer;
}
onRemove() {
if (
!this.controlContainer ||
!this.controlContainer.parentNode ||
!this.map
) {
return;
}
this.controlContainer.parentNode.removeChild(this.controlContainer);
this.map = undefined;
}
};

View file

@ -9,17 +9,19 @@
<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>
<script src="{{public_url}}elevation-control.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}} {{^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}}
{{#is_terrain}} {{#is_terrain}}
#map { position:absolute; top:0; bottom:0; width:100%; } #map { position:absolute; top:0; bottom:0; left:0; right:0; }
{{/is_terrain}} {{/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;}
.maplibre-ctrl-elevation { padding-left: 5px; padding-right: 5px; }
</style> </style>
{{/use_maplibre}} {{/use_maplibre}}
{{^use_maplibre}} {{^use_maplibre}}
@ -69,6 +71,7 @@
}; };
{{/is_terrain}} {{/is_terrain}}
{{#is_terrain}} {{#is_terrain}}
var style = { var style = {
version: 8, version: 8,
sources: { sources: {
@ -86,11 +89,11 @@
"terrain": { "terrain": {
"source": "terrain" "source": "terrain"
}, },
layers: [ "layers": [
{ {
"id": "background", "id": "background",
"paint": { "paint": {
{{^if is_terrainrgb}} {{#if is_terrainrgb}}
"background-color": "hsl(190, 99%, 63%)" "background-color": "hsl(190, 99%, 63%)"
{{else}} {{else}}
"background-color": "hsl(0, 100%, 25%)" "background-color": "hsl(0, 100%, 25%)"
@ -118,24 +121,34 @@
maxPitch: 85, maxPitch: 85,
style: style style: style
}); });
map.addControl(new maplibregl.NavigationControl({ map.addControl(new maplibregl.NavigationControl({
visualizePitch: true, visualizePitch: true,
showZoom: true, showZoom: true,
showCompass: true showCompass: true
})); }));
{{#is_terrain}} {{#is_terrain}}
map.addControl( map.addControl(
new maplibregl.TerrainControl({ new maplibregl.TerrainControl({
source: "terrain", source: "terrain",
}) })
); );
map.addControl(
new ElevationInfoControl({
url: "{{public_url}}data/{{id}}/elevation/{z}/{x}/{y}"
})
);
{{/is_terrain}} {{/is_terrain}}
{{^is_terrain}} {{^is_terrain}}
var inspect = new MaplibreInspect({ var inspect = new MaplibreInspect({
showInspectMap: true, showInspectMap: true,
showInspectButton: false showInspectButton: false
}); });
map.addControl(inspect); map.addControl(inspect);
map.on('styledata', function() { map.on('styledata', function() {
var layerList = document.getElementById('layerList'); var layerList = document.getElementById('layerList');
layerList.innerHTML = ''; layerList.innerHTML = '';

View file

@ -208,51 +208,49 @@ export const serve_data = {
return res.status(404).send(JSON.stringify(tileJSON)); return res.status(404).send(JSON.stringify(tileJSON));
} }
const TILE_SIZE = 256; const TILE_SIZE = tileJSON.tileSize || 512;
let tileCenter; let bbox;
let xy; let xy;
var zoom = z;
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 (
z < tileJSON.minzoom || zoom < tileJSON.minzoom ||
z > tileJSON.maxzoom || zoom > tileJSON.maxzoom ||
intX < 0 || intX < 0 ||
intY < 0 || intY < 0 ||
intX >= Math.pow(2, z) || intX >= Math.pow(2, zoom) ||
intY >= Math.pow(2, z) intY >= Math.pow(2, zoom)
) { ) {
return res.status(404).send('Out of bounds'); return res.status(404).send('Out of bounds');
} }
xy = [intX, intY]; xy = [intX, intY];
tileCenter = new SphericalMercator().bbox(intX, intY, z); bbox = new SphericalMercator().bbox(intX, intY, zoom);
} else { } else {
if ( //no zoom limit with coordinates
z < tileJSON.minzoom || if (zoom < tileJSON.minzoom) {
z > tileJSON.maxzoom || zoom = tileJSON.minzoom;
x < -180 || }
y < -90 || if (zoom > tileJSON.maxzoom) {
x > 180 || zoom = tileJSON.maxzoom;
y > 90
) {
return res.status(404).send('Out of bounds');
} }
tileCenter = [y, x, y + 0.1, x + 0.1]; bbox = [x, y, x + 0.1, y + 0.1];
const { minX, minY } = new SphericalMercator().xyz(tileCenter, z); const { minX, minY } = new SphericalMercator().xyz(bbox, zoom);
xy = [minX, minY]; xy = [minX, minY];
} }
let data; let data;
if (sourceType === 'pmtiles') { if (sourceType === 'pmtiles') {
const tileinfo = await getPMtilesTile(source, z, x, y); const tileinfo = await getPMtilesTile(source, zoom, xy[0], xy[1]);
if (!tileinfo?.data) return res.status(204).send(); if (!tileinfo?.data) return res.status(204).send();
data = tileinfo.data; data = tileinfo.data;
} else { } else {
data = await new Promise((resolve, reject) => { data = await new Promise((resolve, reject) => {
source.getTile(z, xy[0], xy[1], (err, tileData) => { source.getTile(zoom, xy[0], xy[1], (err, tileData) => {
if (err) { if (err) {
return /does not exist/.test(err.message) return /does not exist/.test(err.message)
? resolve(null) ? resolve(null)
@ -271,29 +269,43 @@ export const serve_data = {
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 imgdata = context.getImageData(0, 0, TILE_SIZE, TILE_SIZE);
const arrayWidth = imgdata.width; const long = bbox[0];
const arrayHeight = imgdata.height; const lat = bbox[1];
const bytesPerPixel = 4;
const xPixel = Math.floor(xy[0]); // calculate pixel coordinate of tile,
const yPixel = Math.floor(xy[1]); // see https://developers.google.com/maps/documentation/javascript/examples/map-coordinates
let siny = Math.sin((lat * Math.PI) / 180);
// 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.
siny = Math.min(Math.max(siny, -0.9999), 0.9999);
const xWorld = TILE_SIZE * (0.5 + long / 360);
const yWorld =
TILE_SIZE *
(0.5 - Math.log((1 + siny) / (1 - siny)) / (4 * Math.PI));
const scale = 1 << zoom;
const xTile = Math.floor((xWorld * scale) / TILE_SIZE);
const yTile = Math.floor((yWorld * scale) / TILE_SIZE);
const xPixel = Math.floor(xWorld * scale) - xTile * TILE_SIZE;
const yPixel = Math.floor(yWorld * scale) - yTile * TILE_SIZE;
if ( if (
xPixel < 0 || xPixel < 0 ||
yPixel < 0 || yPixel < 0 ||
xPixel >= arrayWidth || xPixel >= TILE_SIZE ||
yPixel >= arrayHeight yPixel >= TILE_SIZE
) { ) {
return reject('Out of bounds Pixel'); return reject('Pixel is out of bounds');
} }
const index = (yPixel * arrayWidth + xPixel) * bytesPerPixel; const imgdata = context.getImageData(xPixel, yPixel, 1, 1);
const red = imgdata.data[0];
const red = imgdata.data[index]; const green = imgdata.data[1];
const green = imgdata.data[index + 1]; const blue = imgdata.data[2];
const blue = imgdata.data[index + 2];
let elevation; let elevation;
if (encoding === 'mapbox') { if (encoding === 'mapbox') {
@ -307,14 +319,14 @@ export const serve_data = {
resolve( resolve(
res.status(200).send({ res.status(200).send({
z, z: zoom,
x: xy[0], x: xy[0],
y: xy[1], y: xy[1],
red, red,
green, green,
blue, blue,
latitude: tileCenter[0], latitude: lat,
longitude: tileCenter[1], longitude: long,
elevation, elevation,
}), }),
); );
@ -406,6 +418,7 @@ export const serve_data = {
const metadata = await getPMtilesInfo(source); const metadata = await getPMtilesInfo(source);
tileJSON['encoding'] = params['encoding']; tileJSON['encoding'] = params['encoding'];
tileJSON['tileSize'] = params['tileSize'];
tileJSON['name'] = id; tileJSON['name'] = id;
tileJSON['format'] = 'pbf'; tileJSON['format'] = 'pbf';
Object.assign(tileJSON, metadata); Object.assign(tileJSON, metadata);
@ -427,6 +440,7 @@ export const serve_data = {
const info = await mbw.getInfo(); const info = await mbw.getInfo();
source = mbw.getMbTiles(); source = mbw.getMbTiles();
tileJSON['encoding'] = params['encoding']; tileJSON['encoding'] = params['encoding'];
tileJSON['tileSize'] = params['tileSize'];
tileJSON['name'] = id; tileJSON['name'] = id;
tileJSON['format'] = 'pbf'; tileJSON['format'] = 'pbf';