feat: allow 'pmtiles:' in configs. open file less

Signed-off-by: Andrew Calcutt <acalcutt@techidiots.net>
This commit is contained in:
Andrew Calcutt 2023-10-10 12:30:46 -04:00
parent 98995ab1be
commit 0b99d8ddd1
6 changed files with 223 additions and 154 deletions

View file

@ -8,7 +8,11 @@ import { fileURLToPath } from 'url';
import request from 'request';
import { server } from './server.js';
import MBTiles from '@mapbox/mbtiles';
import { GetPMtilesInfo } from './pmtiles_adapter.js';
import {
PMtilesOpen,
PMtilesClose,
GetPMtilesInfo,
} from './pmtiles_adapter.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -80,18 +84,18 @@ const startServer = (configPath, config) => {
});
};
const startWithInputFile = async (inputfile) => {
console.log(`[INFO] Automatically creating config file for ${inputfile}`);
const startWithinputFile = async (inputFile) => {
console.log(`[INFO] Automatically creating config file for ${inputFile}`);
console.log(`[INFO] Only a basic preview style will be used.`);
console.log(
`[INFO] See documentation to learn how to create config.json file.`,
);
inputfile = path.resolve(process.cwd(), inputfile);
inputFile = path.resolve(process.cwd(), inputFile);
const inputfileStats = fs.statSync(inputfile);
if (!inputfileStats.isFile() || inputfileStats.size === 0) {
console.log(`ERROR: Not a valid input file: ${inputfile}`);
const inputFileStats = fs.statSync(inputFile);
if (!inputFileStats.isFile() || inputFileStats.size === 0) {
console.log(`ERROR: Not a valid input file: ${inputFile}`);
process.exit(1);
}
@ -106,24 +110,27 @@ const startWithInputFile = async (inputfile) => {
root: styleDir,
fonts: 'fonts',
styles: 'styles',
mbtiles: path.dirname(inputfile),
mbtiles: path.dirname(inputFile),
pmtiles: path.dirname(inputFile),
},
},
styles: {},
data: {},
};
const extension = inputfile.split('.').pop().toLowerCase();
const extension = inputFile.split('.').pop().toLowerCase();
if (extension === 'pmtiles') {
const info = await GetPMtilesInfo(inputfile);
const FileDescriptor = PMtilesOpen(inputFile);
const info = await GetPMtilesInfo(FileDescriptor);
const metadata = info.metadata;
PMtilesClose(FileDescriptor);
if (
metadata.format === 'pbf' &&
metadata.name.toLowerCase().indexOf('openmaptiles') > -1
) {
config['data'][`v3`] = {
mbtiles: path.basename(inputfile),
pmtiles: path.basename(inputFile),
};
const styles = fs.readdirSync(path.resolve(styleDir, 'styles'));
@ -143,8 +150,8 @@ const startWithInputFile = async (inputfile) => {
console.log(
`WARN: PMTiles not in "openmaptiles" format. Serving raw data only...`,
);
config['data'][(metadata.id || 'mbtiles').replace(/[?/:]/g, '_')] = {
mbtiles: path.basename(inputfile),
config['data'][(metadata.id || 'pmtiles').replace(/[?/:]/g, '_')] = {
pmtiles: path.basename(inputFile),
};
}
@ -156,10 +163,10 @@ const startWithInputFile = async (inputfile) => {
return startServer(null, config);
} else {
const instance = new MBTiles(inputfile + '?mode=ro', (err) => {
const instance = new MBTiles(inputFile + '?mode=ro', (err) => {
if (err) {
console.log('ERROR: Unable to open MBTiles.');
console.log(`Make sure ${path.basename(inputfile)} is valid MBTiles.`);
console.log(`Make sure ${path.basename(inputFile)} is valid MBTiles.`);
process.exit(1);
}
@ -167,7 +174,7 @@ const startWithInputFile = async (inputfile) => {
if (err || !info) {
console.log('ERROR: Metadata missing in the MBTiles.');
console.log(
`Make sure ${path.basename(inputfile)} is valid MBTiles.`,
`Make sure ${path.basename(inputFile)} is valid MBTiles.`,
);
process.exit(1);
}
@ -178,7 +185,7 @@ const startWithInputFile = async (inputfile) => {
info.name.toLowerCase().indexOf('openmaptiles') > -1
) {
config['data'][`v3`] = {
mbtiles: path.basename(inputfile),
mbtiles: path.basename(inputFile),
};
const styles = fs.readdirSync(path.resolve(styleDir, 'styles'));
@ -199,7 +206,7 @@ const startWithInputFile = async (inputfile) => {
`WARN: MBTiles not in "openmaptiles" format. Serving raw data only...`,
);
config['data'][(info.id || 'mbtiles').replace(/[?/:]/g, '_')] = {
mbtiles: path.basename(inputfile),
mbtiles: path.basename(inputFile),
};
}
@ -217,30 +224,30 @@ const startWithInputFile = async (inputfile) => {
fs.stat(path.resolve(opts.config), (err, stats) => {
if (err || !stats.isFile() || stats.size === 0) {
let inputfile;
let inputFile;
if (opts.file) {
inputfile = opts.file;
inputFile = opts.file;
} else if (opts.mbtiles) {
inputfile = opts.mbtiles;
inputFile = opts.mbtiles;
}
if (inputfile) {
return startWithInputFile(inputfile);
if (inputFile) {
return startWithinputFile(inputFile);
} else {
// try to find in the cwd
const files = fs.readdirSync(process.cwd());
for (const filename of files) {
if (filename.endsWith('.mbtiles') || filename.endsWith('.pmtiles')) {
const InputFilesStats = fs.statSync(filename);
if (InputFilesStats.isFile() && InputFilesStats.size > 0) {
inputfile = filename;
const inputFilesStats = fs.statSync(filename);
if (inputFilesStats.isFile() && inputFilesStats.size > 0) {
inputFile = filename;
break;
}
}
}
if (inputfile) {
console.log(`No input file specified, using ${inputfile}`);
return startWithInputFile(inputfile);
if (inputFile) {
console.log(`No input file specified, using ${inputFile}`);
return startWithinputFile(inputFile);
} else {
const url =
'https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/zurich_switzerland.mbtiles';
@ -248,7 +255,7 @@ fs.stat(path.resolve(opts.config), (err, stats) => {
const stream = fs.createWriteStream(filename);
console.log(`No input file found`);
console.log(`[DEMO] Downloading sample data (${filename}) from ${url}`);
stream.on('finish', () => startWithInputFile(filename));
stream.on('finish', () => startWithinputFile(filename));
return request.get(url).pipe(stream);
}
}

View file

@ -1,18 +1,25 @@
import fs from 'node:fs';
import PMTiles from 'pmtiles';
const PMTilesLocalSource = class {
constructor(file) {
this.file = file;
export const PMtilesOpen = (FilePath) => {
const fd = fs.openSync(FilePath, 'r');
return fd;
};
export const PMtilesClose = (fd) => {
fs.closeSync(fd);
};
const PMTilesFileDescriptorSource = class {
constructor(fd) {
this.fd = fd;
}
getKey() {
return this.file.name;
return this.fd;
}
async getBytes(offset, length) {
const buffer = Buffer.alloc(length);
const fd = fs.openSync(this.file, 'r'); //Open the file in read mode
await ReadBytes(fd, buffer, offset); //Read the specifed bytes from the file
fs.closeSync(fd); //close the file
await ReadBytes(this.fd, buffer, offset);
return { data: BufferToArrayBuffer(buffer) };
}
};
@ -28,8 +35,8 @@ const ReadBytes = async (fd, buffer, offset) => {
});
};
export const GetPMtilesInfo = async (pmtilesFile) => {
const source = new PMTilesLocalSource(pmtilesFile);
export const GetPMtilesInfo = async (fd) => {
const source = new PMTilesFileDescriptorSource(fd);
const pmtiles = new PMTiles.PMTiles(source);
const header = await pmtiles.getHeader();
const metadata = await pmtiles.getMetadata();
@ -47,8 +54,8 @@ export const GetPMtilesInfo = async (pmtilesFile) => {
return { header: header, metadata: metadata };
};
export const GetPMtilesTile = async (pmtilesFile, z, x, y) => {
const source = new PMTilesLocalSource(pmtilesFile);
export const GetPMtilesTile = async (fd, z, x, y) => {
const source = new PMTilesFileDescriptorSource(fd);
const pmtiles = new PMTiles.PMTiles(source);
const header = await pmtiles.getHeader();
const TileType = GetPmtilesTileType(header.tileType);

View file

@ -11,7 +11,12 @@ import Pbf from 'pbf';
import { VectorTile } from '@mapbox/vector-tile';
import { getTileUrls, fixTileJSONCenter } from './utils.js';
import { GetPMtilesInfo, GetPMtilesTile } from './pmtiles_adapter.js';
import {
PMtilesOpen,
PMtilesClose,
GetPMtilesInfo,
GetPMtilesTile,
} from './pmtiles_adapter.js';
export const serve_data = {
init: (options, repo) => {
@ -187,22 +192,33 @@ export const serve_data = {
return app;
},
add: async (options, repo, params, id, publicUrl) => {
const mbtilesFile = path.resolve(options.paths.mbtiles, params.mbtiles);
let inputFile;
let inputType;
if (params.pmtiles) {
inputFile = path.resolve(options.paths.pmtiles, params.pmtiles);
inputType = 'pmtiles';
} else if (params.mbtiles) {
inputFile = path.resolve(options.paths.mbtiles, params.mbtiles);
inputType = 'mbtiles';
}
let tileJSON = {
tiles: params.domains || options.domains,
};
const mbtilesFileStats = fs.statSync(mbtilesFile);
if (!mbtilesFileStats.isFile() || mbtilesFileStats.size === 0) {
throw Error(`Not valid MBTiles file: ${mbtilesFile}`);
const inputFileStats = fs.statSync(inputFile);
if (!inputFileStats.isFile() || inputFileStats.size === 0) {
throw Error(`Not valid input file: ${inputFile}`);
}
const extension = mbtilesFile.split('.').pop().toLowerCase();
let source;
if (extension === 'pmtiles') {
const info = await GetPMtilesInfo(mbtilesFile);
let source_type;
if (inputType === 'pmtiles') {
const FileDescriptor = PMtilesOpen(inputFile);
const info = await GetPMtilesInfo(FileDescriptor);
const metadata = info.metadata;
source = mbtilesFile;
source = FileDescriptor;
source_type = 'pmtiles';
tileJSON['name'] = id;
tileJSON['format'] = 'pbf';
@ -220,9 +236,10 @@ export const serve_data = {
if (options.dataDecoratorFunc) {
tileJSON = options.dataDecoratorFunc(id, 'tilejson', tileJSON);
}
} else {
} else if (inputType === 'mbtiles') {
source_type = 'mbtiles';
const sourceInfoPromise = new Promise((resolve, reject) => {
source = new MBTiles(mbtilesFile + '?mode=ro', (err) => {
source = new MBTiles(inputFile + '?mode=ro', (err) => {
if (err) {
reject(err);
return;
@ -261,6 +278,7 @@ export const serve_data = {
tileJSON,
publicUrl,
source,
source_type,
};
},
};

View file

@ -6,8 +6,9 @@ import path from 'path';
import url from 'url';
import util from 'util';
import zlib from 'zlib';
import sharp from 'sharp'; // sharp has to be required before node-canvas. see https://github.com/lovell/sharp/issues/371
import { createCanvas, Image } from 'canvas';
import sharp from 'sharp'; // sharp has to be required before node-canvas on linux but after it on windows. see https://github.com/lovell/sharp/issues/371
//import { createCanvas, Image } from 'canvas';
import clone from 'clone';
import Color from 'color';
import express from 'express';
@ -19,7 +20,12 @@ import polyline from '@mapbox/polyline';
import proj4 from 'proj4';
import request from 'request';
import { getFontsPbf, getTileUrls, fixTileJSONCenter } from './utils.js';
import { GetPMtilesInfo, GetPMtilesTile } from './pmtiles_adapter.js';
import {
PMtilesOpen,
PMtilesClose,
GetPMtilesInfo,
GetPMtilesTile,
} from './pmtiles_adapter.js';
const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+.?\\d+)';
const PATH_PATTERN =
@ -1211,6 +1217,7 @@ export const serve_rendered = {
renderers: [],
renderers_static: [],
sources: {},
source_types: {},
};
let styleJSON;
@ -1221,7 +1228,7 @@ export const serve_rendered = {
ratio: ratio,
request: async (req, callback) => {
const protocol = req.url.split(':')[0];
console.log('Handling request:', req);
// console.log('Handling request:', req);
if (protocol === 'sprites') {
const dir = options.paths[protocol];
const file = unescape(req.url).substring(protocol.length + 3);
@ -1246,10 +1253,11 @@ export const serve_rendered = {
callback(err, { data: null });
},
);
} else if (protocol === 'mbtiles') {
} else if (protocol === 'mbtiles' || protocol === 'pmtiles') {
const parts = req.url.split('/');
const sourceId = parts[2];
const source = map.sources[sourceId];
const source_type = map.source_types[sourceId];
const sourceInfo = styleJSON.sources[sourceId];
const z = parts[3] | 0;
@ -1257,17 +1265,8 @@ export const serve_rendered = {
const y = parts[5].split('.')[0] | 0;
const format = parts[5].split('.')[1];
if (
typeof map.sources[sourceId] === 'string' &&
map.sources[sourceId].split('.').pop().toLowerCase() ===
'pmtiles'
) {
let tileinfo = await GetPMtilesTile(
map.sources[sourceId],
z,
x,
y,
);
if (source_type === 'pmtiles') {
let tileinfo = await GetPMtilesTile(source, z, x, y);
let data = tileinfo.data;
let headers = tileinfo.header;
if (data == undefined) {
@ -1301,7 +1300,7 @@ export const serve_rendered = {
callback(null, response);
}
} else {
} else if (source_type === 'mbtiles') {
source.getTile(z, x, y, (err, data, headers) => {
if (err) {
if (options.verbose)
@ -1463,39 +1462,46 @@ export const serve_rendered = {
const queue = [];
for (const name of Object.keys(styleJSON.sources)) {
let source = styleJSON.sources[name];
const source = styleJSON.sources[name];
const url = source.url;
let source_type;
if (url && url.lastIndexOf('mbtiles:', 0) === 0) {
// found mbtiles source, replace with info from local file
delete source.url;
let mbtilesFile = url.substring('mbtiles://'.length);
let inputFile = url.replace('pmtiles://', '').replace('mbtiles://', '');
const fromData =
mbtilesFile[0] === '{' && mbtilesFile[mbtilesFile.length - 1] === '}';
inputFile[0] === '{' && inputFile[inputFile.length - 1] === '}';
if (fromData) {
mbtilesFile = mbtilesFile.substr(1, mbtilesFile.length - 2);
const mapsTo = (params.mapping || {})[mbtilesFile];
inputFile = inputFile.substr(1, inputFile.length - 2);
const mapsTo = (params.mapping || {})[inputFile];
if (mapsTo) {
mbtilesFile = mapsTo;
inputFile = mapsTo;
}
mbtilesFile = dataResolver(mbtilesFile);
if (!mbtilesFile) {
console.error(`ERROR: data "${mbtilesFile}" not found!`);
const DataInfo = dataResolver(inputFile);
if (DataInfo.inputfile) {
inputFile = DataInfo.inputfile;
source_type = DataInfo.filetype;
} else {
console.error(`ERROR: data "${inputFile}" not found!`);
process.exit(1);
}
}
const mbtilesFileStats = fs.statSync(mbtilesFile);
if (!mbtilesFileStats.isFile() || mbtilesFileStats.size === 0) {
throw Error(`Not valid MBTiles file: ${mbtilesFile}`);
const inputFileStats = fs.statSync(inputFile);
if (!inputFileStats.isFile() || inputFileStats.size === 0) {
throw Error(`Not valid MBTiles file: ${inputFile}`);
}
const extension = mbtilesFile.split('.').pop().toLowerCase();
if (extension === 'pmtiles') {
const info = await GetPMtilesInfo(mbtilesFile);
if (source_type === 'pmtiles') {
let FileDescriptor = PMtilesOpen(inputFile);
const info = await GetPMtilesInfo(FileDescriptor);
const metadata = info.metadata;
map.sources[metadata.name.toLowerCase()] = mbtilesFile;
map.sources[metadata.name.toLowerCase()] = FileDescriptor;
map.source_types[metadata.name.toLowerCase()] = 'pmtiles';
if (!repoobj.dataProjWGStoInternalWGS && metadata.proj4) {
// how to do this for multiple sources with different proj4 defs?
@ -1514,7 +1520,6 @@ export const serve_rendered = {
`mbtiles://${name}/{z}/{x}/{y}.${metadata.format || 'pbf'}`,
];
delete source.scheme;
//console.log(source);
if (
!attributionOverride &&
@ -1531,62 +1536,60 @@ export const serve_rendered = {
} else {
queue.push(
new Promise((resolve, reject) => {
mbtilesFile = path.resolve(options.paths.mbtiles, mbtilesFile);
const mbtilesFileStats = fs.statSync(mbtilesFile);
if (!mbtilesFileStats.isFile() || mbtilesFileStats.size === 0) {
throw Error(`Not valid MBTiles file: ${mbtilesFile}`);
inputFile = path.resolve(options.paths.mbtiles, inputFile);
const inputFileStats = fs.statSync(inputFile);
if (!inputFileStats.isFile() || inputFileStats.size === 0) {
throw Error(`Not valid MBTiles file: ${inputFile}`);
}
map.sources[name] = new MBTiles(
mbtilesFile + '?mode=ro',
(err) => {
map.sources[name].getInfo((err, info) => {
if (err) {
console.error(err);
return;
}
map.sources[name] = new MBTiles(inputFile + '?mode=ro', (err) => {
map.sources[name].getInfo((err, info) => {
if (err) {
console.error(err);
return;
}
map.source_types[name] = 'mbtiles';
if (!repoobj.dataProjWGStoInternalWGS && info.proj4) {
// how to do this for multiple sources with different proj4 defs?
const to3857 = proj4('EPSG:3857');
const toDataProj = proj4(info.proj4);
repoobj.dataProjWGStoInternalWGS = (xy) =>
to3857.inverse(toDataProj.forward(xy));
}
if (!repoobj.dataProjWGStoInternalWGS && info.proj4) {
// how to do this for multiple sources with different proj4 defs?
const to3857 = proj4('EPSG:3857');
const toDataProj = proj4(info.proj4);
repoobj.dataProjWGStoInternalWGS = (xy) =>
to3857.inverse(toDataProj.forward(xy));
}
const type = source.type;
info['extension'] = 'mbtiles';
Object.assign(source, info);
source.type = type;
source.tiles = [
// meta url which will be detected when requested
`mbtiles://${name}/{z}/{x}/{y}.${info.format || 'pbf'}`,
];
delete source.scheme;
const type = source.type;
info['extension'] = 'mbtiles';
Object.assign(source, info);
source.type = type;
source.tiles = [
// meta url which will be detected when requested
`mbtiles://${name}/{z}/{x}/{y}.${info.format || 'pbf'}`,
];
delete source.scheme;
if (options.dataDecoratorFunc) {
source = options.dataDecoratorFunc(
name,
'tilejson',
source,
);
}
if (options.dataDecoratorFunc) {
source = options.dataDecoratorFunc(
name,
'tilejson',
source,
);
}
if (
!attributionOverride &&
source.attribution &&
source.attribution.length > 0
) {
if (!tileJSON.attribution.includes(source.attribution)) {
if (tileJSON.attribution.length > 0) {
tileJSON.attribution += ' | ';
}
tileJSON.attribution += source.attribution;
if (
!attributionOverride &&
source.attribution &&
source.attribution.length > 0
) {
if (!tileJSON.attribution.includes(source.attribution)) {
if (tileJSON.attribution.length > 0) {
tileJSON.attribution += ' | ';
}
tileJSON.attribution += source.attribution;
}
resolve();
});
},
);
}
resolve();
});
});
}),
);
}

View file

@ -111,19 +111,26 @@ export const serve_style = {
for (const name of Object.keys(styleJSON.sources)) {
const source = styleJSON.sources[name];
const url = source.url;
if (url && url.lastIndexOf('mbtiles:', 0) === 0) {
let mbtilesFile = url.substring('mbtiles://'.length);
if (
url &&
(url.lastIndexOf('pmtiles:', 0) === 0 ||
url.lastIndexOf('mbtiles:', 0) === 0)
) {
let inputSource = url
.replace('pmtiles://', '')
.replace('mbtiles://', '');
const fromData =
mbtilesFile[0] === '{' && mbtilesFile[mbtilesFile.length - 1] === '}';
inputSource[0] === '{' && inputSource[inputSource.length - 1] === '}';
if (fromData) {
mbtilesFile = mbtilesFile.substr(1, mbtilesFile.length - 2);
const mapsTo = (params.mapping || {})[mbtilesFile];
inputSource = inputSource.substr(1, inputSource.length - 2);
const mapsTo = (params.mapping || {})[inputSource];
if (mapsTo) {
mbtilesFile = mapsTo;
inputSource = mapsTo;
}
}
const identifier = reportTiles(mbtilesFile, fromData);
const identifier = reportTiles(inputSource, fromData);
if (!identifier) {
return false;
}

View file

@ -181,15 +181,28 @@ function start(opts) {
item,
id,
opts.publicUrl,
(mbtiles, fromData) => {
(fileid, fromData) => {
let dataItemId;
for (const id of Object.keys(data)) {
if (fromData) {
if (id === mbtiles) {
if (id === fileid) {
dataItemId = id;
}
} else {
if (data[id].mbtiles === mbtiles) {
if (
data[id].mbtiles !== undefined &&
data[id].mbtiles === fileid
) {
dataItemId = id;
} else if (
data[id].pmtiles !== undefined &&
data[id].pmtiles === fileid
) {
dataItemId = id;
} else if (
data[id].filename !== undefined &&
data[id].filename === fileid
) {
dataItemId = id;
}
}
@ -200,14 +213,14 @@ function start(opts) {
} else {
if (fromData || !allowMoreData) {
console.log(
`ERROR: style "${item.style}" using unknown mbtiles "${mbtiles}"! Skipping...`,
`ERROR: style "${item.style}" using unknown mbtiles "${fileid}"! Skipping...`,
);
return undefined;
} else {
let id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles;
let id = fileid.substr(0, fileid.lastIndexOf('.')) || fileid;
while (data[id]) id += '_';
data[id] = {
mbtiles: mbtiles,
filename: fileid,
};
return id;
}
@ -229,14 +242,24 @@ function start(opts) {
item,
id,
opts.publicUrl,
(mbtiles) => {
let mbtilesFile;
(fileid) => {
let inputFile;
let fileType;
for (const id of Object.keys(data)) {
if (id === mbtiles) {
mbtilesFile = data[id].mbtiles;
if (id === fileid) {
if (data[id].pmtiles !== undefined) {
inputFile = data[id].pmtiles;
fileType = 'pmtiles';
} else if (data[id].mbtiles !== undefined) {
inputFile = data[id].mbtiles;
fileType = 'mbtiles';
} else if (data[id].filename !== undefined) {
inputFile = data[id].fileid;
fileType = inputFile.split('.').pop().toLowerCase();
}
}
}
return mbtilesFile;
return { inputfile: inputFile, filetype: fileType };
},
),
);
@ -264,8 +287,12 @@ function start(opts) {
for (const id of Object.keys(data)) {
const item = data[id];
if (!item.mbtiles || item.mbtiles.length === 0) {
console.log(`Missing "mbtiles" property for ${id}`);
if (item.pmtiles && item.pmtiles.length !== 0) {
// valid pmtiles
} else if (item.mbtiles && item.mbtiles.length !== 0) {
// valid mbtiles
} else {
console.log(`Missing "pmtiles" or "mbtiles" property for ${id}`);
continue;
}