diff --git a/public/resources/elevation-control.js b/public/resources/elevation-control.js new file mode 100644 index 0000000..a1fda61 --- /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 e4ac4e0..70d3a22 100644 --- a/public/templates/data.tmpl +++ b/public/templates/data.tmpl @@ -9,17 +9,19 @@ + {{/use_maplibre}} {{^use_maplibre}} @@ -69,6 +71,7 @@ }; {{/is_terrain}} {{#is_terrain}} + var style = { version: 8, sources: { @@ -86,11 +89,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 +121,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: "{{public_url}}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 = ''; diff --git a/src/serve_data.js b/src/serve_data.js index d9948e9..cd2e6bb 100644 --- a/src/serve_data.js +++ b/src/serve_data.js @@ -203,44 +203,43 @@ export const serve_data = { if (tileJSON.minzoom == null || tileJSON.maxzoom == null) { 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 || - y > 90 - ) { - return res.status(404).send('Out of bounds'); + //no zoom limit with coordinates + if (zoom < tileJSON.minzoom) { + zoom = tileJSON.minzoom; } - tileCenter = [y, x, y + 0.1, x + 0.1]; - const { minX, minY } = new SphericalMercator().xyz(tileCenter, z); + 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]; } const fetchTile = await fetchTileData( source, sourceType, - z, + zoom, xy[0], xy[1], ); @@ -253,24 +252,39 @@ 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 xPixel = Math.floor(xy[0]); - const yPixel = Math.floor(xy[1]); + 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 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'); } - 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; @@ -281,14 +295,14 @@ export const serve_data = { } resolve( res.status(200).send({ - z, + z: zoom, x: xy[0], y: xy[1], red, green, blue, - latitude: tileCenter[0], - longitude: tileCenter[1], + latitude: lat, + longitude: long, elevation, }), ); @@ -403,6 +417,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); @@ -424,6 +439,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';