Add pmtile support

Co-authored-by: Andrew Calcutt <acalcutt@techidiots.net>
This commit is contained in:
Miko 2024-12-21 21:39:25 +01:00 committed by GitHub
parent 24154d05c2
commit 0e87d456c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -168,139 +168,178 @@ export const serve_data = {
app.get( app.get(
'^/:id/elevation/:z([0-9]+)/:x([-.0-9]+)/:y([-.0-9]+)', '^/:id/elevation/:z([0-9]+)/:x([-.0-9]+)/:y([-.0-9]+)',
async (req, res, next) => { async (req, res, next) => {
const item = repo[req.params.id]; try {
if (!item) { const item = repo?.[req.params.id];
return res.sendStatus(404); 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.');
} }
if ( const encoding = tileJSON?.encoding;
item.source._info.encoding != 'terrarium' && if (encoding == null) {
item.source._info.encoding != 'mapbox' return res.status(400).send('Missing tileJSON.encoding');
) { } else if (encoding !== 'terrarium' && encoding !== 'mapbox') {
return res.status(404).send('Missing encoding'); return res
.status(400)
.send('Invalid encoding. Must be terrarium or mapbox.');
} }
const tileJSONFormat = item.tileJSON.format; const format = tileJSON?.format;
const z = req.params.z | 0; if (format == null) {
var x = req.params.x; return res.status(400).send('Missing tileJSON.format');
var y = req.params.y; } else if (format !== 'webp' && format !== 'png') {
return res.status(400).send('Invalid format. Must be webp or png.');
}
if (item.sourceType === 'pmtiles') { const z = parseInt(req.params.z, 10);
return res.status(404).send('Invalid format'); const x = parseFloat(req.params.x);
} else if (item.sourceType === 'mbtiles') { const y = parseFloat(req.params.y);
const mercator = new SphericalMercator();
if (tileJSON.minzoom == null || tileJSON.maxzoom == null) {
return res.status(404).send(JSON.stringify(tileJSON));
}
const TILE_SIZE = 256;
let tileCenter; let tileCenter;
let xy; let xy;
const int = /^-?[0-9]+$/;
if (int.test(x) && int.test(y)) { if (Number.isInteger(x) && Number.isInteger(y)) {
//console.log("int") const intX = parseInt(req.params.x, 10);
const intY = parseInt(req.params.y, 10);
if ( if (
z < item.tileJSON.minzoom || z < tileJSON.minzoom ||
0 || z > tileJSON.maxzoom ||
x < 0 || intX < 0 ||
y < 0 || intY < 0 ||
z > item.tileJSON.maxzoom || intX >= Math.pow(2, z) ||
x >= Math.pow(2, z) || intY >= Math.pow(2, z)
y >= Math.pow(2, z)
) { ) {
return res.status(404).send('Out of bounds'); return res.status(404).send('Out of bounds');
} }
xy = [intX, intY];
xy = [x | 0, y | 0]; tileCenter = new SphericalMercator().bbox(intX, intY, z);
tileCenter = mercator.bbox(x, y, z);
} else { } else {
//console.log("float")
if ( if (
z < item.tileJSON.minzoom || z < tileJSON.minzoom ||
z > tileJSON.maxzoom ||
x < -180 || x < -180 ||
y < -90 || y < -90 ||
z > item.tileJSON.maxzoom ||
x > 180 || x > 180 ||
y > 90 y > 90
) { ) {
return res.status(404).send('Out of bounds'); return res.status(404).send('Out of bounds');
} }
x = parseFloat(x);
y = parseFloat(y);
tileCenter = [y, x, y + 0.1, x + 0.1]; tileCenter = [y, x, y + 0.1, x + 0.1];
xy = mercator.xyz(tileCenter, z); const { minX, minY } = new SphericalMercator().xyz(tileCenter, z);
xy = [xy.minX, xy.minY]; xy = [minX, minY];
} }
//console.log(tileCenter + " ## " + xy); let data;
item.source.getTile(z, xy[0], xy[1], async (err, data, headers) => { if (sourceType === 'pmtiles') {
const tileinfo = await getPMtilesTile(source, z, x, y);
if (!tileinfo?.data) return res.status(204).send();
data = tileinfo.data;
} else {
data = await new Promise((resolve, reject) => {
source.getTile(z, xy[0], xy[1], (err, tileData) => {
if (err) { if (err) {
if (/does not exist/.test(err.message)) { return /does not exist/.test(err.message)
return res.status(204).send(); ? resolve(null)
} else { : reject(err);
return res
.status(500)
.header('Content-Type', 'text/plain')
.send(err.message);
} }
} else { resolve(tileData);
if (data == null) { });
return res.status(404).send('Not found'); });
} else {
if (tileJSONFormat === 'pbf') {
return res.status(404).send('Invalid format');
} }
var image = new Image(); if (data == null) return res.status(204).send();
image.onload = function () { if (!data) return res.status(404).send('Not found');
const imgSize = 256; if (tileJSON.format === 'pbf')
var canvas = createCanvas(imgSize, imgSize); return res.status(400).send('Invalid format');
var context = canvas.getContext('2d'); 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); context.drawImage(image, 0, 0);
const imgdata = context.getImageData(0, 0, TILE_SIZE, TILE_SIZE);
var imgdata = context.getImageData(0, 0, imgSize, imgSize); 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];
var index = (xy[1] * imgdata.width + xy[0]) * 4;
var red = imgdata.data[index];
var green = imgdata.data[index + 1];
var blue = imgdata.data[index + 2];
let elevation; let elevation;
if (item.source._info.encoding == 'mapbox') { if (encoding === 'mapbox') {
elevation = elevation =
-10000 + (red * 256 * 256 + green * 256 + blue) * 0.1; -10000 + (red * 256 * 256 + green * 256 + blue) * 0.1;
} else if (item.source._info.encoding == 'terrarium') { } else if (encoding === 'terrarium') {
elevation = red * 256 + green + blue / 256 - 32768; elevation = red * 256 + green + blue / 256 - 32768;
} else { } else {
elevation = 'invalid encoding'; elevation = 'invalid encoding';
} }
//"index": index, "length": imgdata.data.length,
return res.status(200).send({ resolve(
z: z, res.status(200).send({
z,
x: xy[0], x: xy[0],
y: xy[1], y: xy[1],
red: red, red,
green: green, green,
blue: blue, blue,
latitude: tileCenter[0], latitude: tileCenter[0],
longitude: tileCenter[1], longitude: tileCenter[1],
elevation: elevation, elevation,
}); }),
);
}; };
image.onerror = (err) => {
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 return res
.status(500) .status(500)
.header('Content-Type', 'text/plain') .header('Content-Type', 'text/plain')
.send(err.message); .send(err.message);
};
//image.onerror = err => { return res.status(200).header('Content-Type', 'image/webp').send(data); }
if (item.source._info.format == 'webp') {
const img = await sharp(data).toFormat('png').toBuffer();
image.src = img;
} else {
image.src = data;
}
}
}
});
} }
}, },
); );
@ -388,7 +427,7 @@ export const serve_data = {
const mbw = await openMbTilesWrapper(inputFile); const mbw = await openMbTilesWrapper(inputFile);
const info = await mbw.getInfo(); const info = await mbw.getInfo();
source = mbw.getMbTiles(); source = mbw.getMbTiles();
info['encoding'] = params['encoding']; tileJSON['encoding'] = params['encoding'];
tileJSON['name'] = id; tileJSON['name'] = id;
tileJSON['format'] = 'pbf'; tileJSON['format'] = 'pbf';