From 0b99d8ddd18868460a56fdf6733b089554d9403b Mon Sep 17 00:00:00 2001 From: Andrew Calcutt Date: Tue, 10 Oct 2023 12:30:46 -0400 Subject: [PATCH] feat: allow 'pmtiles:' in configs. open file less Signed-off-by: Andrew Calcutt --- src/main.js | 67 +++++++++-------- src/pmtiles_adapter.js | 29 ++++--- src/serve_data.js | 40 +++++++--- src/serve_rendered.js | 167 +++++++++++++++++++++-------------------- src/serve_style.js | 21 ++++-- src/server.js | 53 +++++++++---- 6 files changed, 223 insertions(+), 154 deletions(-) diff --git a/src/main.js b/src/main.js index d6d3a47..4deadb9 100644 --- a/src/main.js +++ b/src/main.js @@ -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); } } diff --git a/src/pmtiles_adapter.js b/src/pmtiles_adapter.js index 92b115c..5c336aa 100644 --- a/src/pmtiles_adapter.js +++ b/src/pmtiles_adapter.js @@ -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); diff --git a/src/serve_data.js b/src/serve_data.js index bb80d06..1f9b662 100644 --- a/src/serve_data.js +++ b/src/serve_data.js @@ -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, }; }, }; diff --git a/src/serve_rendered.js b/src/serve_rendered.js index d0eaa24..80ffe54 100644 --- a/src/serve_rendered.js +++ b/src/serve_rendered.js @@ -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(); + }); + }); }), ); } diff --git a/src/serve_style.js b/src/serve_style.js index 3620018..ca668a8 100644 --- a/src/serve_style.js +++ b/src/serve_style.js @@ -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; } diff --git a/src/server.js b/src/server.js index 1030274..8939ad8 100644 --- a/src/server.js +++ b/src/server.js @@ -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; }