test 1 - not working
This commit is contained in:
parent
56eb29518e
commit
c124b5ee1f
4 changed files with 286 additions and 0 deletions
6
package-lock.json
generated
6
package-lock.json
generated
|
@ -28,6 +28,7 @@
|
|||
"express": "4.19.2",
|
||||
"handlebars": "4.7.8",
|
||||
"http-shutdown": "1.2.2",
|
||||
"maplibre-contour": "^0.1.0",
|
||||
"morgan": "1.10.0",
|
||||
"pbf": "4.0.1",
|
||||
"pmtiles": "3.0.7",
|
||||
|
@ -5304,6 +5305,11 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/maplibre-contour": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/maplibre-contour/-/maplibre-contour-0.1.0.tgz",
|
||||
"integrity": "sha512-H8muT7JWYE4oLbFv7L2RSbIM1NOu5JxjA9P/TQqhODDnRChE8ENoDkQIWOKgfcKNU77ypLk2ggGoh4/pt4UPLA=="
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
"express": "4.19.2",
|
||||
"handlebars": "4.7.8",
|
||||
"http-shutdown": "1.2.2",
|
||||
"maplibre-contour": "^0.1.0",
|
||||
"morgan": "1.10.0",
|
||||
"pbf": "4.0.1",
|
||||
"pmtiles": "3.0.7",
|
||||
|
|
205
src/contour.js
Normal file
205
src/contour.js
Normal file
|
@ -0,0 +1,205 @@
|
|||
import sharp from 'sharp';
|
||||
import mlcontour from '../node_modules/maplibre-contour/dist/index.mjs';
|
||||
import { getPMtilesTile } from './pmtiles_adapter.js';
|
||||
|
||||
/**
|
||||
* Manages local DEM (Digital Elevation Model) data using maplibre-contour.
|
||||
*/
|
||||
export class LocalDemManager {
|
||||
/**
|
||||
* Creates a new LocalDemManager instance.
|
||||
* @param {string} encoding - The encoding type for the DEM data.
|
||||
* @param {number} maxzoom - The maximum zoom level for the DEM data.
|
||||
* @param {object} source - The source object that contains either pmtiles or mbtiles.
|
||||
* @param {'pmtiles' | 'mbtiles'} sourceType - The type of data source
|
||||
* @param {Function} [extractZXYFromUrlTrimFunction] - The function to extract the zxy from the url.
|
||||
* @param {Function} [GetTileFunction] - the function that returns a tile from the pmtiles object.
|
||||
*/
|
||||
constructor(
|
||||
encoding,
|
||||
maxzoom,
|
||||
source,
|
||||
sourceType,
|
||||
GetTileFunction,
|
||||
extractZXYFromUrlTrimFunction,
|
||||
) {
|
||||
this.encoding = encoding;
|
||||
this.maxzoom = maxzoom;
|
||||
this.source = source;
|
||||
this.sourceType = sourceType;
|
||||
this.getTile = GetTileFunction || this.GetTile.bind(this);
|
||||
this.extractZXYFromUrlTrim =
|
||||
extractZXYFromUrlTrimFunction || this.extractZXYFromUrlTrim.bind(this);
|
||||
|
||||
this.manager = new mlcontour.LocalDemManager({
|
||||
demUrlPattern: '/{z}/{x}/{y}',
|
||||
cacheSize: 100,
|
||||
encoding: this.encoding,
|
||||
maxzoom: this.maxzoom,
|
||||
timeoutMs: 10000,
|
||||
decodeImage: this.getImageData.bind(this),
|
||||
getTile: this.getTileFunction.bind(this),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes image data from a blob.
|
||||
* @param {Blob} blob - The image data as a Blob.
|
||||
* @param {AbortController} abortController - An AbortController to cancel the image processing.
|
||||
* @returns {Promise<any>} - A Promise that resolves with the processed image data, or null if aborted.
|
||||
* @throws {Error} If an error occurs during image processing.
|
||||
*/
|
||||
async getImageData(blob, abortController) {
|
||||
try {
|
||||
if (Boolean(abortController?.signal?.aborted)) return null; // Check for abort signal.
|
||||
|
||||
const buffer = await blob.arrayBuffer();
|
||||
const image = sharp(Buffer.from(buffer));
|
||||
const metadata = await image.metadata();
|
||||
if (Boolean(abortController?.signal?.aborted)) return null; // Check for abort signal.
|
||||
|
||||
const { data, info } = await image
|
||||
.raw()
|
||||
.toBuffer({ resolveWithObject: true });
|
||||
if (Boolean(abortController?.signal?.aborted)) return null; // Check for abort signal.
|
||||
|
||||
const parsed = mlcontour.decodeParsedImage(
|
||||
info.width,
|
||||
info.height,
|
||||
this.encoding,
|
||||
data,
|
||||
);
|
||||
if (Boolean(abortController?.signal?.aborted)) return null; // Check for abort signal.
|
||||
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
console.error('Error processing image:', error);
|
||||
throw error; // Rethrow to handle upstream
|
||||
// return null; // Or handle error gracefully
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a tile using the provided url and abortController
|
||||
* @param {string} url - The url that should be used to fetch the tile.
|
||||
* @param {AbortController} abortController - An AbortController to cancel the request.
|
||||
* @returns {Promise<{data: Blob, expires: undefined, cacheControl: undefined}>} A promise that resolves with the response data.
|
||||
* @throws {Error} If an error occurs fetching or processing the tile.
|
||||
*/
|
||||
async GetTile(url, abortController) {
|
||||
console.log(url);
|
||||
const $zxy = this.extractZXYFromUrlTrim(url);
|
||||
if (!$zxy) {
|
||||
throw new Error(`Could not extract zxy from $`);
|
||||
}
|
||||
if (abortController.signal.aborted) {
|
||||
return null; // Or throw an error
|
||||
}
|
||||
|
||||
try {
|
||||
let data;
|
||||
if (this.sourceType === 'pmtiles') {
|
||||
let zxyTile;
|
||||
if (this.getPMtilesTile) {
|
||||
zxyTile = await getPMtilesTile(
|
||||
this.source,
|
||||
$zxy.z,
|
||||
$zxy.x,
|
||||
$zxy.y,
|
||||
abortController,
|
||||
);
|
||||
} else {
|
||||
if (abortController.signal.aborted) {
|
||||
console.log('pmtiles aborted in default');
|
||||
return null;
|
||||
}
|
||||
zxyTile = {
|
||||
data: new Uint8Array([$zxy.z, $zxy.x, $zxy.y]),
|
||||
};
|
||||
}
|
||||
|
||||
if (!zxyTile || !zxyTile.data) {
|
||||
throw new Error(`No tile returned for $`);
|
||||
}
|
||||
data = zxyTile.data;
|
||||
} else {
|
||||
data = await new Promise((resolve, reject) => {
|
||||
this.source.getTile($zxy.z, $zxy.x, $zxy.y, (err, tileData) => {
|
||||
if (err) {
|
||||
return /does not exist/.test(err.message)
|
||||
? resolve(null)
|
||||
: reject(err);
|
||||
}
|
||||
resolve(tileData);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
throw new Error(`No tile returned for $`);
|
||||
}
|
||||
|
||||
const blob = new Blob([data]);
|
||||
return {
|
||||
data: blob,
|
||||
expires: undefined,
|
||||
cacheControl: undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') {
|
||||
console.log('fetch cancelled');
|
||||
return null;
|
||||
}
|
||||
throw error; // Rethrow for handling upstream
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation for extracting z,x,y from a url
|
||||
* @param {string} url - The url to extract from
|
||||
* @returns {{z: number, x: number, y:number} | null} Returns the z,x,y of the url, or null if can't extract
|
||||
*/
|
||||
extractZXYFromUrlTrim(url) {
|
||||
// 1. Find the index of the last `/`
|
||||
const lastSlashIndex = url.lastIndexOf('/');
|
||||
if (lastSlashIndex === -1) {
|
||||
return null; // URL does not have any slashes
|
||||
}
|
||||
|
||||
const segments = url.split('/');
|
||||
if (segments.length <= 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ySegment = segments[segments.length - 1];
|
||||
const xSegment = segments[segments.length - 2];
|
||||
const zSegment = segments[segments.length - 3];
|
||||
|
||||
const lastDotIndex = ySegment.lastIndexOf('.');
|
||||
const cleanedYSegment =
|
||||
lastDotIndex === -1 ? ySegment : ySegment.substring(0, lastDotIndex);
|
||||
|
||||
// 3. Attempt to parse segments as numbers
|
||||
const z = parseInt(zSegment, 10);
|
||||
const x = parseInt(xSegment, 10);
|
||||
const y = parseInt(cleanedYSegment, 10);
|
||||
|
||||
if (isNaN(z) || isNaN(x) || isNaN(y)) {
|
||||
return null; // Conversion failed, invalid URL format
|
||||
}
|
||||
|
||||
return { z, x, y };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying maplibre-contour LocalDemManager
|
||||
* @returns {any} the underlying maplibre-contour LocalDemManager
|
||||
*/
|
||||
getManager() {
|
||||
return this.manager;
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import SphericalMercator from '@mapbox/sphericalmercator';
|
|||
import { Image, createCanvas } from 'canvas';
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { LocalDemManager } from './contour.js';
|
||||
import { fixTileJSONCenter, getTileUrls, isValidHttpUrl } from './utils.js';
|
||||
import {
|
||||
getPMtilesInfo,
|
||||
|
@ -165,6 +166,79 @@ export const serve_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();
|
||||
|
||||
const $data = await demManager.fetchContourTile(
|
||||
z,
|
||||
x,
|
||||
y,
|
||||
{ levels: [10] },
|
||||
new AbortController(),
|
||||
);
|
||||
|
||||
// Set the Content-Type header here
|
||||
res.setHeader('Content-Type', 'application/x-protobuf');
|
||||
res.setHeader('Content-Encoding', 'gzip');
|
||||
res.send($data);
|
||||
} catch (err) {
|
||||
return res
|
||||
.status(500)
|
||||
.header('Content-Type', 'text/plain')
|
||||
.send(err.message);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
'^/:id/elevation/:z([0-9]+)/:x([-.0-9]+)/:y([-.0-9]+)',
|
||||
async (req, res, next) => {
|
||||
|
|
Loading…
Reference in a new issue