From 6fddbae15736551e4f11d329dc7e7e1b901f5da2 Mon Sep 17 00:00:00 2001 From: acalcutt Date: Thu, 2 Jan 2025 18:17:19 -0500 Subject: [PATCH] merge elevation changes --- src/serve_data.js | 146 ++++++++++++++++++++++++++++++++++++++++++++++ src/server.js | 2 +- 2 files changed, 147 insertions(+), 1 deletion(-) diff --git a/src/serve_data.js b/src/serve_data.js index 84b9234..ff896d0 100644 --- a/src/serve_data.js +++ b/src/serve_data.js @@ -123,6 +123,152 @@ export const serve_data = { return res.status(200).send(data); }); + app.get('/:id/elevation/:z/:x/:y', + 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]; + } + const fetchTile = await fetchTileData( + source, + sourceType, + z, + x, + y, + ); + if (fetchTile == null) return res.status(204).send(); + + let data = fetchTile.data; + 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) => { const item = repo[req.params.id]; if (!item) { diff --git a/src/server.js b/src/server.js index df1134f..beee4b3 100644 --- a/src/server.js +++ b/src/server.js @@ -621,7 +621,7 @@ async function start(opts) { }; }); - serveTemplate('^/data{/:view}/:id/', 'data', (req) => { + serveTemplate('/data{/:view}/:id/', 'data', (req) => { const { id, view } = req.params; const data = serving.data[id];