diff --git a/docs/config.rst b/docs/config.rst index b7034af..3bc1a5a 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -27,6 +27,7 @@ Example:: "maxSize": 2048, "pbfAlias": "pbf", "serveAllFonts": false, + "serveAllStyles": false, "serveStaticMaps": true, "tileMargin": 0 }, @@ -99,9 +100,9 @@ Default is ``2048``. ``tileMargin`` -------------- -Additional image side length added during tile rendering that is cropped from the delivered tile. This is useful for resolving the issue with cropped labels, +Additional image side length added during tile rendering that is cropped from the delivered tile. This is useful for resolving the issue with cropped labels, but it does come with a performance degradation, because additional, adjacent vector tiles need to be loaded to genenrate a single tile. -Default is ``0`` to disable this processing. +Default is ``0`` to disable this processing. ``minRendererPoolSizes`` ------------------------ @@ -124,6 +125,13 @@ If you have plenty of memory, try setting these equal to or slightly above your If you need to conserve memory, try lower values for scale factors that are less common. Default is ``[16, 8, 4]``. +``serveAllStyles`` +------------------------ + +If this option is enabled, all the styles from the ``paths.styles`` will be served. (No recursion, only ``.json`` files are used.) +The process will also watch for changes in this directory and remove/add more styles dynamically. +It is recommended to also use the ``serveAllFonts`` option when using this option. + ``watermark`` ----------- diff --git a/package.json b/package.json index dcc7dba..b1a4274 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "handlebars": "4.5.3", "http-shutdown": "1.2.1", "morgan": "1.9.1", + "node-watch": "0.6.3", "pbf": "3.2.1", "proj4": "2.6.0", "request": "2.88.0", diff --git a/src/serve_rendered.js b/src/serve_rendered.js index ccea0ec..e0b02ca 100644 --- a/src/serve_rendered.js +++ b/src/serve_rendered.js @@ -663,7 +663,12 @@ module.exports = { const styleFile = params.style; const styleJSONPath = path.resolve(options.paths.styles, styleFile); - styleJSON = clone(require(styleJSONPath)); + try { + styleJSON = JSON.parse(fs.readFileSync(styleJSONPath)); + } catch (e) { + console.log('Error parsing style file'); + return false; + } if (styleJSON.sprite && !httpTester.test(styleJSON.sprite)) { styleJSON.sprite = 'sprites://' + @@ -798,5 +803,14 @@ module.exports = { }); return Promise.all([renderersReadyPromise]); - } + }, + remove: (repo, id) => { + let item = repo[id]; + if (item) { + item.map.renderers.forEach(pool => { + pool.close(); + }); + } + delete repo[id]; + }, }; diff --git a/src/serve_style.js b/src/serve_style.js index d1ea6ff..cbe1138 100644 --- a/src/serve_style.js +++ b/src/serve_style.js @@ -72,10 +72,19 @@ module.exports = { return app; }, + remove: (repo, id) => { + delete repo[id]; + }, add: (options, repo, params, id, publicUrl, reportTiles, reportFont) => { const styleFile = path.resolve(options.paths.styles, params.style); + let styleJSON; + try { + styleJSON = JSON.parse(fs.readFileSync(styleFile)); + } catch (e) { + console.log('Error parsing style file'); + return false; + } - const styleJSON = clone(require(styleFile)); for (const name of Object.keys(styleJSON.sources)) { const source = styleJSON.sources[name]; const url = source.url; @@ -92,6 +101,9 @@ module.exports = { } } const identifier = reportTiles(mbtilesFile, fromData); + if (!identifier) { + return false; + } source.url = `local://data/${identifier}.json`; } } @@ -128,5 +140,7 @@ module.exports = { publicUrl, name: styleJSON.name }; + + return true; } }; diff --git a/src/server.js b/src/server.js index 6d16803..8e0ff81 100644 --- a/src/server.js +++ b/src/server.js @@ -14,6 +14,7 @@ const express = require('express'); const handlebars = require('handlebars'); const mercator = new (require('@mapbox/sphericalmercator'))(); const morgan = require('morgan'); +const watch = require('node-watch'); const packageJson = require('../package'); const serve_font = require('./serve_font'); @@ -114,15 +115,10 @@ function start(opts) { ); } - for (const id of Object.keys(config.styles || {})) { - const item = config.styles[id]; - if (!item.style || item.style.length === 0) { - console.log(`Missing "style" property for ${id}`); - continue; - } - + let addStyle = (id, item, allowMoreData, reportFonts) => { + let success = true; if (item.serve_data !== false) { - serve_style.add(options, serving.styles, item, id, opts.publicUrl, + success = serve_style.add(options, serving.styles, item, id, opts.publicUrl, (mbtiles, fromData) => { let dataItemId; for (const id of Object.keys(data)) { @@ -138,22 +134,26 @@ function start(opts) { } if (dataItemId) { // mbtiles exist in the data config return dataItemId; - } else if (fromData) { - console.log(`ERROR: data "${mbtiles}" not found!`); - process.exit(1); } else { - let id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles; - while (data[id]) id += '_'; - data[id] = { - 'mbtiles': mbtiles - }; - return id; + if (fromData || !allowMoreData) { + console.log(`ERROR: style "${file.name}" using unknown mbtiles "${mbtiles}"! Skipping...`); + return undefined; + } else { + let id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles; + while (data[id]) id += '_'; + data[id] = { + 'mbtiles': mbtiles + }; + return id; + } } }, font => { - serving.fonts[font] = true; + if (reportFonts) { + serving.fonts[font] = true; + } }); } - if (item.serve_rendered !== false) { + if (success && item.serve_rendered !== false) { if (serve_rendered) { startupPromises.push(serve_rendered.add(options, serving.rendered, item, id, opts.publicUrl, mbtiles => { @@ -170,6 +170,16 @@ function start(opts) { item.serve_rendered = false; } } + }; + + for (const id of Object.keys(config.styles || {})) { + const item = config.styles[id]; + if (!item.style || item.style.length === 0) { + console.log(`Missing "style" property for ${id}`); + continue; + } + + addStyle(id, item, true, true); } startupPromises.push( @@ -190,6 +200,41 @@ function start(opts) { ); } + if (options.serveAllStyles) { + fs.readdir(options.paths.styles, {withFileTypes: true}, (err, files) => { + if (err) { + return; + } + for (const file of files) { + if (file.isFile() && + path.extname(file.name).toLowerCase() == '.json') { + let id = path.basename(file.name, '.json'); + let item = { + style: file.name + }; + addStyle(id, item, false, false); + } + } + }); + + watch(options.paths.styles, + { persistent: false, filter: /\.json$/ }, + (eventType, filename) => { + let id = path.basename(filename, '.json'); + console.log(`Style "${id}" changed, updating...`); + + serve_style.remove(serving.styles, id); + serve_rendered.remove(serving.rendered, id); + + if (eventType == "update") { + let item = { + style: filename + }; + addStyle(id, item, false, false); + } + }); + } + app.get('/styles.json', (req, res, next) => { const result = []; const query = req.query.key ? (`?key=${req.query.key}`) : ''; @@ -207,10 +252,9 @@ function start(opts) { const addTileJSONs = (arr, req, type) => { for (const id of Object.keys(serving[type])) { - let info = clone(serving[type][id]); + const info = clone(serving[type][id].tileJSON); let path = ''; if (type === 'rendered') { - info = info.tileJSON; path = `styles/${id}`; } else { path = `${type}/${id}`; @@ -280,7 +324,7 @@ function start(opts) { }; serveTemplate('/$', 'index', req => { - const styles = clone(config.styles || {}); + const styles = clone(serving.styles || {}); for (const id of Object.keys(styles)) { const style = styles[id]; style.name = (serving.styles[id] || serving.rendered[id] || {}).name; @@ -341,7 +385,7 @@ function start(opts) { serveTemplate('/styles/:id/$', 'viewer', req => { const id = req.params.id; - const style = clone((config.styles || {})[id]); + const style = clone(((serving.styles || {})[id] || {}).styleJSON); if (!style) { return null; } @@ -359,7 +403,7 @@ function start(opts) { */ serveTemplate('/styles/:id/wmts.xml', 'wmts', req => { const id = req.params.id; - const wmts = clone((config.styles || {})[id]); + const wmts = clone((serving.styles || {})[id]); if (!wmts) { return null; }