From 678d8833d1db53f61a609b7d3761547f097dc72f Mon Sep 17 00:00:00 2001 From: Miko Date: Wed, 8 Jan 2025 00:17:51 +0100 Subject: [PATCH 1/7] add elevation info in preview --- public/resources/elevation-control.js | 51 +++++++++++++++++++++++++++ public/templates/data.tmpl | 19 ++++++++-- 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 public/resources/elevation-control.js diff --git a/public/resources/elevation-control.js b/public/resources/elevation-control.js new file mode 100644 index 000000000..a1fda61b1 --- /dev/null +++ b/public/resources/elevation-control.js @@ -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; + } + }; diff --git a/public/templates/data.tmpl b/public/templates/data.tmpl index e4ac4e015..a19206192 100644 --- a/public/templates/data.tmpl +++ b/public/templates/data.tmpl @@ -9,6 +9,7 @@ + {{/use_maplibre}} {{^use_maplibre}} @@ -69,6 +71,9 @@ }; {{/is_terrain}} {{#is_terrain}} + + let baseUrl = window.location.origin; + var style = { version: 8, sources: { @@ -86,11 +91,11 @@ "terrain": { "source": "terrain" }, - layers: [ + "layers": [ { "id": "background", "paint": { - {{^if is_terrainrgb}} + {{#if is_terrainrgb}} "background-color": "hsl(190, 99%, 63%)" {{else}} "background-color": "hsl(0, 100%, 25%)" @@ -118,24 +123,34 @@ maxPitch: 85, style: style }); + map.addControl(new maplibregl.NavigationControl({ visualizePitch: true, showZoom: true, showCompass: true })); {{#is_terrain}} + map.addControl( new maplibregl.TerrainControl({ source: "terrain", }) ); + + map.addControl( + new ElevationInfoControl({ + url: baseUrl + "/data/{{id}}/elevation/{z}/{x}/{y}" + }) + ); {{/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 = ''; From 13c024f6f8906f53370df63707310314898940e6 Mon Sep 17 00:00:00 2001 From: Miko Date: Wed, 8 Jan 2025 00:19:59 +0100 Subject: [PATCH 2/7] fix various issues - pmtile coordinates - pixel calculation - swapped lat / long - allow tileSize to be configured - allow any zoom for coordinates --- src/serve_data.js | 89 +++++++++++++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 34 deletions(-) diff --git a/src/serve_data.js b/src/serve_data.js index 1936da6b2..f64cd0569 100644 --- a/src/serve_data.js +++ b/src/serve_data.js @@ -208,30 +208,29 @@ export const serve_data = { return res.status(404).send(JSON.stringify(tileJSON)); } - const TILE_SIZE = 256; - let tileCenter; + const TILE_SIZE = tileJSON.tileSize || 512; + let bbox; let xy; + var zoom = z; 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 || + zoom < tileJSON.minzoom || + zoom > tileJSON.maxzoom || intX < 0 || intY < 0 || - intX >= Math.pow(2, z) || - intY >= Math.pow(2, z) + intX >= Math.pow(2, zoom) || + intY >= Math.pow(2, zoom) ) { return res.status(404).send('Out of bounds'); } xy = [intX, intY]; - tileCenter = new SphericalMercator().bbox(intX, intY, z); + bbox = new SphericalMercator().bbox(intX, intY, zoom); } else { if ( - z < tileJSON.minzoom || - z > tileJSON.maxzoom || x < -180 || y < -90 || x > 180 || @@ -240,19 +239,27 @@ export const serve_data = { 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]; + //no zoom limit with coordinates + if (zoom < tileJSON.minzoom) { + zoom = tileJSON.minzoom; + } + if (zoom > tileJSON.maxzoom) { + zoom = tileJSON.maxzoom; + } + + bbox = [x, y, x + 0.1, y + 0.1]; + const { minX, minY } = new SphericalMercator().xyz(bbox, zoom); + xy = [minX, minY, zoom]; } let data; 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(); data = tileinfo.data; } else { 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) { return /does not exist/.test(err.message) ? resolve(null) @@ -271,51 +278,63 @@ export const serve_data = { 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 long = bbox[0]; + const lat = bbox[1]; + + // calculate pixel coordinate of tile, + // 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 xPixel = Math.floor(xy[0]); - const yPixel = Math.floor(xy[1]); + 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 ( xPixel < 0 || yPixel < 0 || - xPixel >= arrayWidth || - yPixel >= arrayHeight + xPixel >= TILE_SIZE || + yPixel >= TILE_SIZE ) { - return reject('Out of bounds Pixel'); + return reject('Pixel is out of bounds'); } - const index = (yPixel * arrayWidth + xPixel) * bytesPerPixel; - - const red = imgdata.data[index]; - const green = imgdata.data[index + 1]; - const blue = imgdata.data[index + 2]; + const imgdata = context.getImageData(xPixel, yPixel, 1, 1); + const red = imgdata.data[0]; + const green = imgdata.data[1]; + const blue = imgdata.data[2]; let elevation; if (encoding === 'mapbox') { elevation = - -10000 + (red * 256 * 256 + green * 256 + blue) * 0.1; + -10000 + ((red * 256 * 256 + green * 256 + blue) * 0.1); } else if (encoding === 'terrarium') { - elevation = red * 256 + green + blue / 256 - 32768; + elevation = (red * 256 + green + blue / 256) - 32768 } else { elevation = 'invalid encoding'; } resolve( res.status(200).send({ - z, + z: zoom, x: xy[0], y: xy[1], red, green, blue, - latitude: tileCenter[0], - longitude: tileCenter[1], - elevation, + latitude: lat, + longitude: long, + elevation }), ); }; @@ -406,6 +425,7 @@ export const serve_data = { const metadata = await getPMtilesInfo(source); tileJSON['encoding'] = params['encoding']; + tileJSON['tileSize'] = params['tileSize']; tileJSON['name'] = id; tileJSON['format'] = 'pbf'; Object.assign(tileJSON, metadata); @@ -427,6 +447,7 @@ export const serve_data = { const info = await mbw.getInfo(); source = mbw.getMbTiles(); tileJSON['encoding'] = params['encoding']; + tileJSON['tileSize'] = params['tileSize']; tileJSON['name'] = id; tileJSON['format'] = 'pbf'; From 3cc3da619568eeb9c8aab291847ef679fd23345f Mon Sep 17 00:00:00 2001 From: Miko Date: Wed, 8 Jan 2025 00:38:08 +0100 Subject: [PATCH 3/7] apply linter changes --- src/serve_data.js | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/serve_data.js b/src/serve_data.js index f64cd0569..78d781704 100644 --- a/src/serve_data.js +++ b/src/serve_data.js @@ -230,12 +230,7 @@ export const serve_data = { xy = [intX, intY]; bbox = new SphericalMercator().bbox(intX, intY, zoom); } else { - if ( - x < -180 || - y < -90 || - x > 180 || - y > 90 - ) { + if (x < -180 || y < -90 || x > 180 || y > 90) { return res.status(404).send('Out of bounds'); } @@ -290,12 +285,14 @@ export const serve_data = { 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 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 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; @@ -317,9 +314,9 @@ export const serve_data = { let elevation; if (encoding === 'mapbox') { elevation = - -10000 + ((red * 256 * 256 + green * 256 + blue) * 0.1); + -10000 + (red * 256 * 256 + green * 256 + blue) * 0.1; } else if (encoding === 'terrarium') { - elevation = (red * 256 + green + blue / 256) - 32768 + elevation = red * 256 + green + blue / 256 - 32768; } else { elevation = 'invalid encoding'; } @@ -334,7 +331,7 @@ export const serve_data = { blue, latitude: lat, longitude: long, - elevation + elevation, }), ); }; From 3ac09bac7f92e66e5225778c8e0bb72444524be4 Mon Sep 17 00:00:00 2001 From: Miko <39791814+okimiko@users.noreply.github.com> Date: Wed, 8 Jan 2025 00:51:45 +0100 Subject: [PATCH 4/7] remove not used entry --- src/serve_data.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serve_data.js b/src/serve_data.js index 78d781704..7e0508d44 100644 --- a/src/serve_data.js +++ b/src/serve_data.js @@ -244,7 +244,7 @@ export const serve_data = { bbox = [x, y, x + 0.1, y + 0.1]; const { minX, minY } = new SphericalMercator().xyz(bbox, zoom); - xy = [minX, minY, zoom]; + xy = [minX, minY]; } let data; From 77632a8798e42854bb27157178f307364c9a7b67 Mon Sep 17 00:00:00 2001 From: Miko Date: Wed, 8 Jan 2025 21:14:43 +0100 Subject: [PATCH 5/7] drop baseUrl in favor of public_url --- public/templates/data.tmpl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/public/templates/data.tmpl b/public/templates/data.tmpl index a19206192..04bc2243b 100644 --- a/public/templates/data.tmpl +++ b/public/templates/data.tmpl @@ -72,8 +72,6 @@ {{/is_terrain}} {{#is_terrain}} - let baseUrl = window.location.origin; - var style = { version: 8, sources: { @@ -139,7 +137,7 @@ map.addControl( new ElevationInfoControl({ - url: baseUrl + "/data/{{id}}/elevation/{z}/{x}/{y}" + url: "{{public_url}}data/{{id}}/elevation/{z}/{x}/{y}" }) ); {{/is_terrain}} From 2c3c927b4353dedd16d57a3439bd52293e842242 Mon Sep 17 00:00:00 2001 From: Miko <39791814+okimiko@users.noreply.github.com> Date: Thu, 9 Jan 2025 09:51:20 +0100 Subject: [PATCH 6/7] fix map not using full body --- public/templates/data.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/templates/data.tmpl b/public/templates/data.tmpl index 04bc2243b..70d3a2272 100644 --- a/public/templates/data.tmpl +++ b/public/templates/data.tmpl @@ -16,7 +16,7 @@ #map {position:absolute;top:0;left:0;right:250px;bottom:0;} {{/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}} 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;} From d9dc66afeeccf909861dd962429064fbf600b8da Mon Sep 17 00:00:00 2001 From: Miko Date: Thu, 9 Jan 2025 23:42:10 +0100 Subject: [PATCH 7/7] remove bounds check for coordinates --- src/serve_data.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/serve_data.js b/src/serve_data.js index 7e0508d44..b2a0f5a7c 100644 --- a/src/serve_data.js +++ b/src/serve_data.js @@ -230,10 +230,6 @@ export const serve_data = { xy = [intX, intY]; bbox = new SphericalMercator().bbox(intX, intY, zoom); } else { - if (x < -180 || y < -90 || x > 180 || y > 90) { - return res.status(404).send('Out of bounds'); - } - //no zoom limit with coordinates if (zoom < tileJSON.minzoom) { zoom = tileJSON.minzoom;