diff --git a/src/main.js b/src/main.js index 129b21d6f..7523aa937 100644 --- a/src/main.js +++ b/src/main.js @@ -8,9 +8,11 @@ import path from 'path'; import { fileURLToPath } from 'url'; import axios from 'axios'; import { server } from './server.js'; -import MBTiles from '@mapbox/mbtiles'; import { isValidHttpUrl } from './utils.js'; import { openPMtiles, getPMtilesInfo } from './pmtiles_adapter.js'; +import { program } from 'commander'; +import { existsP } from './promises.js'; +import { openMbTilesWrapper } from './mbtiles_wrapper.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -23,8 +25,6 @@ if (args.length >= 3 && args[2][0] !== '-') { args.splice(2, 0, '--mbtiles'); } -import { program } from 'commander'; -import { existsP } from './promises.js'; program .description('tileserver-gl startup options') .usage('tileserver-gl [mbtiles] [options]') @@ -184,62 +184,55 @@ const startWithInputFile = async (inputFile) => { ); process.exit(1); } - 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.`); - process.exit(1); - } - - instance.getInfo(async (err, info) => { - if (err || !info) { - console.log('ERROR: Metadata missing in the MBTiles.'); - console.log( - `Make sure ${path.basename(inputFile)} is valid MBTiles.`, - ); - process.exit(1); - } - const bounds = info.bounds; + let info; + try { + const mbw = await openMbTilesWrapper(inputFile); + info = await mbw.getInfo(); + if (!info) throw new Error('Metadata missing in the MBTiles.'); + } catch (err) { + console.log('ERROR: Unable to open MBTiles or read metadata:', err); + console.log(`Make sure ${path.basename(inputFile)} is valid MBTiles.`); + process.exit(1); + } + const bounds = info.bounds; - if ( - info.format === 'pbf' && - info.name.toLowerCase().indexOf('openmaptiles') > -1 - ) { - config['data'][`v3`] = { - mbtiles: path.basename(inputFile), - }; + if ( + info.format === 'pbf' && + info.name.toLowerCase().indexOf('openmaptiles') > -1 + ) { + config['data'][`v3`] = { + mbtiles: path.basename(inputFile), + }; - const styles = await fsp.readdir(path.resolve(styleDir, 'styles')); - for (const styleName of styles) { - const styleFileRel = styleName + '/style.json'; - const styleFile = path.resolve(styleDir, 'styles', styleFileRel); - if (await existsP(styleFile)) { - config['styles'][styleName] = { - style: styleFileRel, - tilejson: { - bounds, - }, - }; - } - } - } else { - console.log( - `WARN: MBTiles not in "openmaptiles" format. Serving raw data only...`, - ); - config['data'][(info.id || 'mbtiles').replace(/[?/:]/g, '_')] = { - mbtiles: path.basename(inputFile), + const styles = await fsp.readdir(path.resolve(styleDir, 'styles')); + for (const styleName of styles) { + const styleFileRel = styleName + '/style.json'; + const styleFile = path.resolve(styleDir, 'styles', styleFileRel); + if (await existsP(styleFile)) { + config['styles'][styleName] = { + style: styleFileRel, + tilejson: { + bounds, + }, }; } + } + } else { + console.log( + `WARN: MBTiles not in "openmaptiles" format. Serving raw data only...`, + ); + config['data'][(info.id || 'mbtiles').replace(/[?/:]/g, '_')] = { + mbtiles: path.basename(inputFile), + }; + } - if (opts.verbose) { - console.log(JSON.stringify(config, undefined, 2)); - } else { - console.log('Run with --verbose to see the config file here.'); - } + if (opts.verbose) { + console.log(JSON.stringify(config, undefined, 2)); + } else { + console.log('Run with --verbose to see the config file here.'); + } - return startServer(null, config); - }); - }); + return startServer(null, config); } }; diff --git a/src/mbtiles_wrapper.js b/src/mbtiles_wrapper.js new file mode 100644 index 000000000..95d121c6d --- /dev/null +++ b/src/mbtiles_wrapper.js @@ -0,0 +1,46 @@ +import MBTiles from '@mapbox/mbtiles'; +import util from 'node:util'; + +/** + * Promise-ful wrapper around the MBTiles class. + */ +class MBTilesWrapper { + constructor(mbtiles) { + this._mbtiles = mbtiles; + this._getInfoP = util.promisify(mbtiles.getInfo.bind(mbtiles)); + } + + /** + * Get the underlying MBTiles object. + * @returns {MBTiles} + */ + getMbTiles() { + return this._mbtiles; + } + + /** + * Get the MBTiles metadata object. + * @returns {Promise} + */ + getInfo() { + return this._getInfoP(); + } +} + +/** + * Open the given MBTiles file and return a promise that resolves with a + * MBTilesWrapper instance. + * @param inputFile Input file + * @returns {Promise} + */ +export function openMbTilesWrapper(inputFile) { + return new Promise((resolve, reject) => { + const mbtiles = new MBTiles(inputFile + '?mode=ro', (err) => { + if (err) { + reject(err); + return; + } + resolve(new MBTilesWrapper(mbtiles)); + }); + }); +} diff --git a/src/serve_data.js b/src/serve_data.js index 0e37cc6cd..995b6564a 100644 --- a/src/serve_data.js +++ b/src/serve_data.js @@ -5,7 +5,6 @@ import path from 'path'; import clone from 'clone'; import express from 'express'; -import MBTiles from '@mapbox/mbtiles'; import Pbf from 'pbf'; import { VectorTile } from '@mapbox/vector-tile'; @@ -16,6 +15,7 @@ import { openPMtiles, } from './pmtiles_adapter.js'; import { gunzipP, gzipP } from './promises.js'; +import { openMbTilesWrapper } from './mbtiles_wrapper.js'; export const serve_data = { init: (options, repo) => { @@ -242,39 +242,25 @@ export const serve_data = { } } else if (inputType === 'mbtiles') { sourceType = 'mbtiles'; - const sourceInfoPromise = new Promise((resolve, reject) => { - source = new MBTiles(inputFile + '?mode=ro', (err) => { - if (err) { - reject(err); - return; - } - source.getInfo((err, info) => { - if (err) { - reject(err); - return; - } - tileJSON['name'] = id; - tileJSON['format'] = 'pbf'; - - Object.assign(tileJSON, info); + const mbw = await openMbTilesWrapper(inputFile); + const info = await mbw.getInfo(); + source = mbw.getMbTiles(); + tileJSON['name'] = id; + tileJSON['format'] = 'pbf'; - tileJSON['tilejson'] = '2.0.0'; - delete tileJSON['filesize']; - delete tileJSON['mtime']; - delete tileJSON['scheme']; + Object.assign(tileJSON, info); - Object.assign(tileJSON, params.tilejson || {}); - fixTileJSONCenter(tileJSON); + tileJSON['tilejson'] = '2.0.0'; + delete tileJSON['filesize']; + delete tileJSON['mtime']; + delete tileJSON['scheme']; - if (options.dataDecoratorFunc) { - tileJSON = options.dataDecoratorFunc(id, 'tilejson', tileJSON); - } - resolve(); - }); - }); - }); + Object.assign(tileJSON, params.tilejson || {}); + fixTileJSONCenter(tileJSON); - await sourceInfoPromise; + if (options.dataDecoratorFunc) { + tileJSON = options.dataDecoratorFunc(id, 'tilejson', tileJSON); + } } repo[id] = { diff --git a/src/serve_rendered.js b/src/serve_rendered.js index 2043ce391..8972ce8bd 100644 --- a/src/serve_rendered.js +++ b/src/serve_rendered.js @@ -24,7 +24,6 @@ import express from 'express'; import sanitize from 'sanitize-filename'; import SphericalMercator from '@mapbox/sphericalmercator'; import mlgl from '@maplibre/maplibre-gl-native'; -import MBTiles from '@mapbox/mbtiles'; import polyline from '@mapbox/polyline'; import proj4 from 'proj4'; import axios from 'axios'; @@ -43,6 +42,7 @@ import { import { renderOverlay, renderWatermark, renderAttribution } from './render.js'; import fsp from 'node:fs/promises'; import { gunzipP } from './promises.js'; +import { openMbTilesWrapper } from './mbtiles_wrapper.js'; const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+.?\\d+)'; const PATH_PATTERN = @@ -1131,7 +1131,6 @@ export const serve_rendered = { }; repo[id] = repoobj; - const queue = []; for (const name of Object.keys(styleJSON.sources)) { let sourceType; let source = styleJSON.sources[name]; @@ -1205,69 +1204,52 @@ export const serve_rendered = { } } } else { - queue.push( - new Promise(async (resolve, reject) => { - inputFile = path.resolve(options.paths.mbtiles, inputFile); - const inputFileStats = await fsp.stat(inputFile); - if (!inputFileStats.isFile() || inputFileStats.size === 0) { - throw Error(`Not valid MBTiles file: "${inputFile}"`); - } - map.sources[name] = new MBTiles(inputFile + '?mode=ro', (err) => { - map.sources[name].getInfo((err, info) => { - if (err) { - console.error(err); - return; - } - map.sourceTypes[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)); - } + const inputFileStats = await fsp.stat(inputFile); + if (!inputFileStats.isFile() || inputFileStats.size === 0) { + throw Error(`Not valid MBTiles file: "${inputFile}"`); + } + const mbw = await openMbTilesWrapper(inputFile); + const info = await mbw.getInfo(); + map.sources[name] = mbw.getMbTiles(); + map.sourceTypes[name] = 'mbtiles'; - const type = source.type; - 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 (!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 ( - !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(); - }); - }); - }), - ); + const type = source.type; + 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 ( + !attributionOverride && + source.attribution && + source.attribution.length > 0 + ) { + if (!tileJSON.attribution.includes(source.attribution)) { + if (tileJSON.attribution.length > 0) { + tileJSON.attribution += ' | '; + } + tileJSON.attribution += source.attribution; + } + } } } } - await Promise.all(queue); - // standard and @2x tiles are much more usual -> default to larger pools const minPoolSizes = options.minRendererPoolSizes || [8, 4, 2]; const maxPoolSizes = options.maxRendererPoolSizes || [16, 8, 4];