diff --git a/docs/endpoints.rst b/docs/endpoints.rst index 12f1acf5e..435144acb 100644 --- a/docs/endpoints.rst +++ b/docs/endpoints.rst @@ -108,6 +108,8 @@ Source data * 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}`` + * The elevation api is not available in the ``tileserver-gl-light`` version. + Static files =========== * Static files are served at ``/files/{filename}`` diff --git a/public/templates/data.tmpl b/public/templates/data.tmpl index 70d3a2272..12a8d9850 100644 --- a/public/templates/data.tmpl +++ b/public/templates/data.tmpl @@ -9,7 +9,9 @@ + {{^is_light}} + {{/is_light}} {{/use_maplibre}} {{^use_maplibre}} @@ -135,11 +139,13 @@ }) ); + {{^is_light}} map.addControl( new ElevationInfoControl({ url: "{{public_url}}data/{{id}}/elevation/{z}/{x}/{y}" }) ); + {{/is_light}} {{/is_terrain}} {{^is_terrain}} diff --git a/public/templates/index.tmpl b/public/templates/index.tmpl index acf094f77..1d3a514ec 100644 --- a/public/templates/index.tmpl +++ b/public/templates/index.tmpl @@ -124,9 +124,9 @@ {{/is_vector}} {{^is_vector}} View - {{#elevation_link}} + {{#is_terrain}} Preview Terrain - {{/elevation_link}} + {{/is_terrain}} {{/is_vector}} diff --git a/src/serve_data.js b/src/serve_data.js index cd2e6bbf7..6369aa21d 100644 --- a/src/serve_data.js +++ b/src/serve_data.js @@ -8,8 +8,6 @@ import express from 'express'; import Pbf from 'pbf'; import { VectorTile } from '@mapbox/vector-tile'; import SphericalMercator from '@mapbox/sphericalmercator'; -import { Image, createCanvas } from 'canvas'; -import sharp from 'sharp'; import { fixTileJSONCenter, @@ -21,6 +19,20 @@ import { getPMtilesInfo, openPMtiles } from './pmtiles_adapter.js'; import { gunzipP, gzipP } from './promises.js'; import { openMbTilesWrapper } from './mbtiles_wrapper.js'; +import fs from 'node:fs'; +import { fileURLToPath } from 'url'; +const packageJson = JSON.parse( + fs.readFileSync( + path.dirname(fileURLToPath(import.meta.url)) + '/../package.json', + 'utf8', + ), +); + +const isLight = packageJson.name.slice(-6) === '-light'; +const serve_rendered = ( + await import(`${!isLight ? `./serve_rendered.js` : `./serve_light.js`}`) +).serve_rendered; + export const serve_data = { /** * Initializes the serve_data module. @@ -246,79 +258,20 @@ export const serve_data = { 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 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 >= TILE_SIZE || - yPixel >= TILE_SIZE - ) { - return reject('Out of bounds Pixel'); - } - 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; - } else if (encoding === 'terrarium') { - elevation = red * 256 + green + blue / 256 - 32768; - } else { - elevation = 'invalid encoding'; - } - resolve( - res.status(200).send({ - z: zoom, - x: xy[0], - y: xy[1], - red, - green, - blue, - latitude: lat, - longitude: long, - 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; - } - }); + var param = { + long: bbox[0], + lat: bbox[1], + encoding, + format, + tile_size: TILE_SIZE, + z: zoom, + x: xy[0], + y: xy[1], + }; + + res + .status(200) + .send(await serve_rendered.getTerrainElevation(data, param)); } catch (err) { return res .status(500) diff --git a/src/serve_light.js b/src/serve_light.js index 7e49c4929..13aa84c73 100644 --- a/src/serve_light.js +++ b/src/serve_light.js @@ -6,4 +6,8 @@ export const serve_rendered = { init: (options, repo, programOpts) => {}, add: (options, repo, params, id, programOpts, dataResolver) => {}, remove: (repo, id) => {}, + getTerrainElevation: (data, param) => { + param['elevation'] = 'not supported in light'; + return param; + }, }; diff --git a/src/serve_rendered.js b/src/serve_rendered.js index af928d993..3dc3cd546 100644 --- a/src/serve_rendered.js +++ b/src/serve_rendered.js @@ -7,7 +7,7 @@ // This happens on ARM: // > terminate called after throwing an instance of 'std::runtime_error' // > what(): Cannot read GLX extensions. -import 'canvas'; +import { Image, createCanvas } from 'canvas'; import '@maplibre/maplibre-gl-native'; // // SECTION END @@ -1458,4 +1458,76 @@ export const serve_rendered = { } delete repo[id]; }, + + /** + * Get the elevation of terrain tile data by rendering it to a canvas image + * @param {object} data The background color (or empty string for transparent). + * @param {object} param Required parameters (coordinates e.g.) + * @returns {object} + */ + getTerrainElevation: async function (data, param) { + return await new Promise(async (resolve, reject) => { + const image = new Image(); + image.onload = async () => { + const canvas = createCanvas(param['tile_size'], param['tile_size']); + const context = canvas.getContext('2d'); + context.drawImage(image, 0, 0); + + // calculate pixel coordinate of tile, + // see https://developers.google.com/maps/documentation/javascript/examples/map-coordinates + let siny = Math.sin((param['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 = param['tile_size'] * (0.5 + param['long'] / 360); + const yWorld = + param['tile_size'] * + (0.5 - Math.log((1 + siny) / (1 - siny)) / (4 * Math.PI)); + + const scale = 1 << param['z']; + + const xTile = Math.floor((xWorld * scale) / param['tile_size']); + const yTile = Math.floor((yWorld * scale) / param['tile_size']); + + const xPixel = Math.floor(xWorld * scale) - xTile * param['tile_size']; + const yPixel = Math.floor(yWorld * scale) - yTile * param['tile_size']; + if ( + xPixel < 0 || + yPixel < 0 || + xPixel >= param['tile_size'] || + yPixel >= param['tile_size'] + ) { + return reject('Out of bounds Pixel'); + } + 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 (param['encoding'] === 'mapbox') { + elevation = -10000 + (red * 256 * 256 + green * 256 + blue) * 0.1; + } else if (param['encoding'] === 'terrarium') { + elevation = red * 256 + green + blue / 256 - 32768; + } else { + elevation = 'invalid encoding'; + } + param['elevation'] = elevation; + param['red'] = red; + param['green'] = green; + param['blue'] = blue; + resolve(param); + }; + image.onerror = (err) => reject(err); + if (param['format'] === 'webp') { + try { + const img = await sharp(data).toFormat('png').toBuffer(); + image.src = img; + } catch (err) { + reject(err); + } + } else { + image.src = data; + } + }); + }, }; diff --git a/src/server.js b/src/server.js index 682e07e9e..468b71337 100644 --- a/src/server.js +++ b/src/server.js @@ -24,13 +24,12 @@ import { } from './utils.js'; import { fileURLToPath } from 'url'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const packageJson = JSON.parse( fs.readFileSync(__dirname + '/../package.json', 'utf8'), ); - const isLight = packageJson.name.slice(-6) === '-light'; + const serve_rendered = ( await import(`${!isLight ? `./serve_rendered.js` : `./serve_light.js`}`) ).serve_rendered; @@ -575,11 +574,14 @@ async function start(opts) { tileJSON.encoding === 'terrarium' || tileJSON.encoding === 'mapbox' ) { - data.elevation_link = getTileUrls( - req, - tileJSON.tiles, - `data/${id}/elevation`, - )[0]; + if (!isLight) { + data.elevation_link = getTileUrls( + req, + tileJSON.tiles, + `data/${id}/elevation`, + )[0]; + } + data.is_terrain = true; } if (center) { const centerPx = mercator.px([center[0], center[1]], center[2]); @@ -698,6 +700,7 @@ async function start(opts) { is_terrain: is_terrain, is_terrainrgb: data.tileJSON.encoding === 'mapbox', terrain_encoding: data.tileJSON.encoding, + is_light: isLight, }; });