From f60de3830464478fff11c23e08dbdb4a177857a7 Mon Sep 17 00:00:00 2001 From: Miko Date: Sun, 13 Oct 2024 21:27:40 +0200 Subject: [PATCH 001/160] add terrain preview and elevation link --- public/resources/index.css | 2 +- public/templates/data.tmpl | 139 +++++++++++++++++++++++++----------- public/templates/index.tmpl | 25 +++++-- src/server.js | 12 ++-- src/utils.js | 10 ++- 5 files changed, 132 insertions(+), 56 deletions(-) diff --git a/public/resources/index.css b/public/resources/index.css index d18b63816..fd5f21ee3 100644 --- a/public/resources/index.css +++ b/public/resources/index.css @@ -114,7 +114,7 @@ section { } .details h3 { font-size: 18px; - margin-top: 25px; + margin-top: 5px; } .details p { padding: 0; diff --git a/public/templates/data.tmpl b/public/templates/data.tmpl index 59b0d3b35..2c4d21733 100644 --- a/public/templates/data.tmpl +++ b/public/templates/data.tmpl @@ -4,20 +4,25 @@ {{name}} - TileServer GL - {{#is_vector}} + {{#use_maplibre}} - {{/is_vector}} - {{^is_vector}} + {{/use_maplibre}} + {{^use_maplibre}} @@ -37,23 +42,22 @@ background-image: url({{public_url}}images/marker-icon.png{{&key_query}}); } - {{/is_vector}} + {{/use_maplibre}} - {{#is_vector}} + {{#use_maplibre}}

{{name}}

+ {{^is_terrain}}

+  {{/is_terrain}}
   
-  {{/is_vector}}
-  {{^is_vector}}
+  {{/use_maplibre}}
+  {{^use_maplibre}}
   

{{name}}

- {{/is_vector}} + {{/use_maplibre}} diff --git a/public/templates/index.tmpl b/public/templates/index.tmpl index d4d5be766..0edd19a52 100644 --- a/public/templates/index.tmpl +++ b/public/templates/index.tmpl @@ -6,10 +6,15 @@ TileServer GL - Server for vector and raster maps with GL styles + {{/use_maplibre}} {{^use_maplibre}} @@ -71,10 +73,9 @@ }; {{/is_terrain}} {{#is_terrain}} - let baseUrl = window.location.origin; - console.log(baseUrl); - baseUrl = baseUrl + "/data/{{id}}/contour/{z}/{x}/{y}" - console.log(baseUrl); + + let baseUrl = window.location.origin; + var style = { version: 8, sources: { @@ -90,42 +91,64 @@ }, "contour": { "type": "vector", - "tiles": [ baseUrl ], + "tiles": [ baseUrl + "/data/{{id}}/contour/{z}/{x}/{y}" ], } }, + "glyphs": "local://fonts/{fontstack}/{range}.pbf", "terrain": { "source": "terrain" }, - "layers": [ - { - "id": "background", - "paint": { - "background-color": "hsl(190, 99%, 63%)" - }, - "type": "background" - }, - { - "id": "hillshade", - "source": "hillshade", - "type": "hillshade", - "paint": { - "hillshade-shadow-color": "hsl(39, 21%, 33%)", - "hillshade-illumination-direction": 315, - "hillshade-exaggeration": 0.8 - } - }, - { - "id": "contours", - "type": "line", - "source": "contour", - "source-layer": "contours", - "paint": { - "line-opacity": 0.5, - "line-width": ["match", ["get", "level"], 1, 1, 0.5] - } - } - ] - }; + "layers": [ + { + "id": "background", + "paint": { + "background-color": "hsl(190, 99%, 63%)" + }, + "type": "background" + }, + { + "id": "hillshade", + "source": "hillshade", + "type": "hillshade", + "paint": { + "hillshade-shadow-color": "hsl(39, 21%, 33%)", + "hillshade-illumination-direction": 315, + "hillshade-exaggeration": 0.8 + } + }, + { + "id": "contours", + "type": "line", + "source": "contour", + "source-layer": "contours", + "paint": { + "line-opacity": 0.5, + "line-width": ["match", ["get", "level"], 1, 1, 0.5] + } + }, + { + "id": 'contour-label', + "type": 'symbol', + "source": 'contour', + "source-layer": 'contours', + "filter": ['>', ['get', 'level'], 0], + "paint": { + 'text-halo-color': 'white', + 'text-halo-width': 1 + }, + "layout": { + 'symbol-placement': 'line', + 'text-size': 10, + 'text-field': [ + 'concat', + ['number-format', ['get', 'ele'], {}], + '\'' + ], + 'text-font': ['Noto Sans Bold'] + } + } + ] + }; {{/is_terrain}} var map = new maplibregl.Map({ @@ -134,17 +157,33 @@ 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 MaplibreContourControl({ + source: "contour", + visibility: false, + layers: [ "contours", "contour-label" ] + }) + ); + + //map.addControl( + // new ElevationInfo({ + // url: baseUrl + "/data/{{id}}/elvation/{z}/{x}/{y}" + // }) + //); {{/is_terrain}} {{^is_terrain}} @@ -153,6 +192,7 @@ showInspectButton: false }); map.addControl(inspect); + map.on('styledata', function() { var layerList = document.getElementById('layerList'); layerList.innerHTML = ''; From 1bfeac2e018a32d85b811aab5f14fb31a8aa3f80 Mon Sep 17 00:00:00 2001 From: Miko Date: Sat, 4 Jan 2025 01:00:04 +0100 Subject: [PATCH 148/160] add contour-label --- public/resources/contour-control.js | 2 +- public/templates/data.tmpl | 43 ++++++++++++++--------------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/public/resources/contour-control.js b/public/resources/contour-control.js index 0bf2f0260..2549d4986 100644 --- a/public/resources/contour-control.js +++ b/public/resources/contour-control.js @@ -26,7 +26,7 @@ class MaplibreContourControl { this.controlContainer.classList.add("maplibre-ctrl-contour-active"); this.contourButton.title = "Disable Contours"; } else { - this.contourButton.title = "Ensable Contours"; + this.contourButton.title = "Enable Contours"; } }); }); diff --git a/public/templates/data.tmpl b/public/templates/data.tmpl index 0d823f009..5bea45227 100644 --- a/public/templates/data.tmpl +++ b/public/templates/data.tmpl @@ -11,6 +11,7 @@ + {{/use_maplibre}} {{^use_maplibre}} @@ -178,7 +179,7 @@ map.addControl( new ElevationInfoControl({ - url: baseUrl + "/data/{{id}}/elvation/{z}/{x}/{y}" + url: baseUrl + "/data/{{id}}/elevation/{z}/{x}/{y}" }) ); {{/is_terrain}} From c9e7f100346027b16fc6732bf04f8674f73cb90d Mon Sep 17 00:00:00 2001 From: Miko Date: Tue, 7 Jan 2025 23:58:40 +0100 Subject: [PATCH 150/160] disable debug log --- src/contour.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/contour.js b/src/contour.js index 2dcbb6fd7..fa770dd11 100644 --- a/src/contour.js +++ b/src/contour.js @@ -107,7 +107,7 @@ export class LocalDemManager { * @throws If an error occurs fetching or processing the tile. */ async GetTile(url, abortController) { - console.log(url); + //console.log(url); const $zxy = this.extractZXYFromUrlTrim(url); if (!$zxy) { throw new Error(`Could not extract zxy from $url`); @@ -171,7 +171,7 @@ export class LocalDemManager { }; } catch (error) { if (error.name === 'AbortError') { - console.log('fetch cancelled'); + console.log('fetch canceled'); return null; } throw error; From 56dbeb2e8e5b5e90a4efb63121d8a1a30f4d758f Mon Sep 17 00:00:00 2001 From: Miko Date: Sat, 11 Jan 2025 00:01:39 +0100 Subject: [PATCH 151/160] fix rebase issues and update contour router to express5 --- src/serve_data.js | 283 +++++++++++++++++++++------------------------- 1 file changed, 130 insertions(+), 153 deletions(-) diff --git a/src/serve_data.js b/src/serve_data.js index fc21910d1..7835a821c 100644 --- a/src/serve_data.js +++ b/src/serve_data.js @@ -12,13 +12,7 @@ import { Image, createCanvas } from 'canvas'; import sharp from 'sharp'; import { LocalDemManager } from './contour.js'; -import { fixTileJSONCenter, getTileUrls, isValidHttpUrl } from './utils.js'; -import { - fixTileJSONCenter, - getTileUrls, - isValidHttpUrl, - fetchTileData, -} from './utils.js'; +import { fixTileJSONCenter, getTileUrls, isValidHttpUrl, fetchTileData } from './utils.js'; import { getPMtilesInfo, openPMtiles } from './pmtiles_adapter.js'; import { gunzipP, gzipP } from './promises.js'; import { openMbTilesWrapper } from './mbtiles_wrapper.js'; @@ -109,154 +103,13 @@ export const serve_data = { data = await gunzipP(data); isGzipped = false; } - } else if (item.sourceType === 'mbtiles') { - item.source.getTile(z, x, y, async (err, data, headers) => { - let isGzipped; - if (err) { - if (/does not exist/.test(err.message)) { - return res.status(204).send(); - } else { - return res - .status(500) - .header('Content-Type', 'text/plain') - .send(err.message); - } - } else { - if (data == null) { - return res.status(404).send('Not found'); - } else { - if (tileJSONFormat === 'pbf') { - isGzipped = - data.slice(0, 2).indexOf(Buffer.from([0x1f, 0x8b])) === 0; - if (options.dataDecoratorFunc) { - if (isGzipped) { - data = await gunzipP(data); - isGzipped = false; - } - data = options.dataDecoratorFunc(id, 'data', data, z, x, y); - } - } - if (format === 'pbf') { - headers['Content-Type'] = 'application/x-protobuf'; - } else if (format === 'geojson') { - headers['Content-Type'] = 'application/json'; - - if (isGzipped) { - data = await gunzipP(data); - isGzipped = false; - } - - const tile = new VectorTile(new Pbf(data)); - const geojson = { - type: 'FeatureCollection', - features: [], - }; - for (const layerName in tile.layers) { - const layer = tile.layers[layerName]; - for (let i = 0; i < layer.length; i++) { - const feature = layer.feature(i); - const featureGeoJSON = feature.toGeoJSON(x, y, z); - featureGeoJSON.properties.layer = layerName; - geojson.features.push(featureGeoJSON); - } - } - data = JSON.stringify(geojson); - } - delete headers['ETag']; // do not trust the tile ETag -- regenerate - headers['Content-Encoding'] = 'gzip'; - res.set(headers); - - if (!isGzipped) { - data = await gzipP(data); - } - - return res.status(200).send(data); - } - } - }); - } - }, - ); - - app.get( - '^/:id/contour/:z([0-9]+)/:x([-.0-9]+)/:y([-.0-9]+)', - 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 maxzoom = tileJSON?.maxzoom; - if (maxzoom == null) { - return res.status(400).send('Missing tileJSON.maxzoom'); - } - - const z = parseInt(req.params.z, 10); - const x = parseFloat(req.params.x); - const y = parseFloat(req.params.y); - - const demManagerInit = new LocalDemManager( - encoding, - maxzoom, - source, - sourceType, - ); - const demManager = await demManagerInit.getManager(); - - let levels; - if (z <= 8) { - levels = 1000; - } else if (z <= 10) { - levels = 500; - } else if (z <= 11) { - levels = 250; - } else if (z <= 12) { - levels = 100; - } else if (z <= 13) { - levels = 50; - } else if (z <= 14) { - levels = 25; - } else if (z <= 15) { - levels = 20; - } else if (z <= 17) { - levels = 10; - } else if (z >= 18) { - levels = 5; - } - - const { arrayBuffer } = await demManager.fetchContourTile( + data = options.dataDecoratorFunc( + req.params.id, + 'data', + data, z, x, y, - { levels: [levels] }, - new AbortController(), ); } } @@ -294,6 +147,117 @@ export const serve_data = { return res.status(200).send(data); }); + /** + * Handles requests for contour data. + * @param {object} req - Express request object. + * @param {object} res - Express response object. + * @param {string} req.params.id - ID of the contour data. + * @param {string} req.params.z - Z coordinate of the tile. + * @param {string} req.params.x - X coordinate of the tile (either integer or float). + * @param {string} req.params.y - Y coordinate of the tile (either integer or float). + * @returns {Promise} + */ + app.get('/:id/contour/:z/:x/:y', async (req, res, next) => { + try { + if (verbose) { + console.log( + `Handling contour request for: /data/%s/contour/%s/%s/%s`, + String(req.params.id).replace(/\n|\r/g, ''), + String(req.params.z).replace(/\n|\r/g, ''), + String(req.params.x).replace(/\n|\r/g, ''), + String(req.params.y).replace(/\n|\r/g, ''), + ); + } + 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 maxzoom = tileJSON?.maxzoom; + if (maxzoom == null) { + return res.status(400).send('Missing tileJSON.maxzoom'); + } + + const z = parseInt(req.params.z, 10); + const x = parseFloat(req.params.x); + const y = parseFloat(req.params.y); + + const demManagerInit = new LocalDemManager( + encoding, + maxzoom, + source, + sourceType, + ); + const demManager = await demManagerInit.getManager(); + + let levels; + if (z <= 8) { + levels = 1000; + } else if (z <= 10) { + levels = 500; + } else if (z <= 11) { + levels = 250; + } else if (z <= 12) { + levels = 100; + } else if (z <= 13) { + levels = 50; + } else if (z <= 14) { + levels = 25; + } else if (z <= 15) { + levels = 20; + } else if (z <= 17) { + levels = 10; + } else if (z >= 18) { + levels = 5; + } + + const { arrayBuffer } = await demManager.fetchContourTile( + z, + x, + y, + { levels: [levels] }, + new AbortController(), + ); + // Set the Content-Type header here + res.setHeader('Content-Type', 'application/x-protobuf'); + res.setHeader('Content-Encoding', 'gzip'); + let data = Buffer.from(arrayBuffer); + data = await gzipP(data); + res.send(data); + } catch (err) { + return res + .status(500) + .header('Content-Type', 'text/plain') + .send(err.message); + } + }); + /** * Handles requests for elevation data. * @param {object} req - Express request object. @@ -340,12 +304,15 @@ export const serve_data = { } 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 = tileJSON.tileSize || 512; let bbox; let xy; @@ -354,6 +321,7 @@ export const serve_data = { if (Number.isInteger(x) && Number.isInteger(y)) { const intX = parseInt(req.params.x, 10); const intY = parseInt(req.params.y, 10); + if ( zoom < tileJSON.minzoom || zoom > tileJSON.maxzoom || @@ -374,6 +342,7 @@ export const serve_data = { 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]; @@ -384,7 +353,7 @@ export const serve_data = { sourceType, zoom, xy[0], - xy[1], + xy[1] ); if (fetchTile == null) return res.status(204).send(); @@ -395,6 +364,7 @@ export const serve_data = { 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]; @@ -404,6 +374,7 @@ export const serve_data = { // 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 * @@ -416,6 +387,7 @@ export const serve_data = { const xPixel = Math.floor(xWorld * scale) - xTile * TILE_SIZE; const yPixel = Math.floor(yWorld * scale) - yTile * TILE_SIZE; + if ( xPixel < 0 || yPixel < 0 || @@ -424,10 +396,12 @@ export const serve_data = { ) { 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; @@ -436,6 +410,7 @@ export const serve_data = { } else { elevation = 'invalid encoding'; } + resolve( res.status(200).send({ z: zoom, @@ -450,7 +425,9 @@ export const serve_data = { }), ); }; + image.onerror = (err) => reject(err); + if (format === 'webp') { try { const img = await sharp(data).toFormat('png').toBuffer(); From 6ad99a30b64593f61beb546bfe27e65b5ba71f80 Mon Sep 17 00:00:00 2001 From: Miko Date: Sat, 11 Jan 2025 00:20:22 +0100 Subject: [PATCH 152/160] remove base url, re-add encoding based background-color --- public/templates/data.tmpl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/public/templates/data.tmpl b/public/templates/data.tmpl index 77029d1f9..61cabc750 100644 --- a/public/templates/data.tmpl +++ b/public/templates/data.tmpl @@ -76,8 +76,6 @@ {{/is_terrain}} {{#is_terrain}} - let baseUrl = window.location.origin; - var style = { version: 8, sources: { @@ -93,7 +91,7 @@ }, "contour": { "type": "vector", - "tiles": [ baseUrl + "/data/{{id}}/contour/{z}/{x}/{y}" ], + "tiles": [ "{{public_url}}/data/{{id}}/contour/{z}/{x}/{y}" ], } }, "glyphs": "/fonts/{fontstack}/{range}.pbf", @@ -104,7 +102,11 @@ { "id": "background", "paint": { + {{#if is_terrainrgb}} "background-color": "hsl(190, 99%, 63%)" + {{else}} + "background-color": "hsl(0, 100%, 25%)" + {{/if}} }, "type": "background" }, @@ -179,7 +181,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 450de428541741e1e1acfe10422a89d2733563ba Mon Sep 17 00:00:00 2001 From: Miko Date: Sat, 11 Jan 2025 00:49:41 +0100 Subject: [PATCH 153/160] fix further rebase issues --- package-lock.json | 8 -------- public/templates/data.tmpl | 3 +-- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 409460e8a..687ff75c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6887,14 +6887,6 @@ "fflate": "^0.8.0" } }, - "node_modules/pngjs": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", - "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", - "engines": { - "node": ">=14.19.0" - } - }, "node_modules/prebuild-install": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", diff --git a/public/templates/data.tmpl b/public/templates/data.tmpl index 61cabc750..dcc40093e 100644 --- a/public/templates/data.tmpl +++ b/public/templates/data.tmpl @@ -11,7 +11,6 @@ -