diff --git a/.dockerignore b/.dockerignore index 1661d17..8b6bb72 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,3 +5,4 @@ !package.json !package-lock.json !docker-entrypoint.sh +**.gitignore \ No newline at end of file diff --git a/.github/workflows/ct.yml b/.github/workflows/ct.yml index 2d138df..69276f0 100644 --- a/.github/workflows/ct.yml +++ b/.github/workflows/ct.yml @@ -46,7 +46,7 @@ jobs: https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip - name: Prepare test data 📦 - run: unzip -q test_data.zip -d test_data + run: unzip -q test_data.zip - name: Run tests 🧪 run: xvfb-run --server-args="-screen 0 1024x768x24" npm test diff --git a/.gitignore b/.gitignore index 2009454..0af34c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,11 @@ docs/_build node_modules test_data +test_data.zip data light plugins config.json *.mbtiles +styles +fonts diff --git a/docs/config.rst b/docs/config.rst index 1e8ecb9..3ff2d45 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -17,7 +17,8 @@ Example: "icons": "icons", "styles": "styles", "mbtiles": "data", - "pmtiles": "data" + "pmtiles": "data", + "files": "public/files" }, "domains": [ "localhost:8080", diff --git a/docs/endpoints.rst b/docs/endpoints.rst index af7c7b4..6b32e8e 100644 --- a/docs/endpoints.rst +++ b/docs/endpoints.rst @@ -100,6 +100,18 @@ Source data * TileJSON at ``/data/{id}.json`` +Static files +=========== +* Static files are served at ``/files/{filename}`` + + * The source folder can be configured (``options.paths.files``), default is ``public/files`` + + * This feature can be used to serve ``geojson`` files for styles and rendered tiles. + + * Keep in mind, that each rendered tile loads the whole geojson file, if performance matters a conversion to a tiled format (e.g. with https://github.com/felt/tippecanoe)may be a better approch. + + * Use ``file://{filename}`` to have matching paths for both endoints + TileJSON arrays =============== Array of all TileJSONs is at ``[/{tileSize}]/index.json`` (``[/{tileSize}]/rendered.json``; ``/data.json``) diff --git a/public/files/.gitignore b/public/files/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/src/main.js b/src/main.js index 7523aa9..ac45828 100644 --- a/src/main.js +++ b/src/main.js @@ -109,6 +109,8 @@ const startWithInputFile = async (inputFile) => { '../node_modules/tileserver-gl-styles/', ); + const filesDir = path.resolve(__dirname, '../public/files'); + const config = { options: { paths: { @@ -117,6 +119,7 @@ const startWithInputFile = async (inputFile) => { styles: 'styles', mbtiles: inputFilePath, pmtiles: inputFilePath, + files: filesDir, }, }, styles: {}, diff --git a/src/serve_rendered.js b/src/serve_rendered.js index 8972ce8..6017a9b 100644 --- a/src/serve_rendered.js +++ b/src/serve_rendered.js @@ -41,7 +41,7 @@ import { } from './pmtiles_adapter.js'; import { renderOverlay, renderWatermark, renderAttribution } from './render.js'; import fsp from 'node:fs/promises'; -import { gunzipP } from './promises.js'; +import { existsP, gunzipP } from './promises.js'; import { openMbTilesWrapper } from './mbtiles_wrapper.js'; const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+.?\\d+)'; @@ -893,13 +893,15 @@ export const serve_rendered = { // console.log('Handling request:', req); if (protocol === 'sprites') { const dir = options.paths[protocol]; - const file = unescape(req.url).substring(protocol.length + 3); + const file = decodeURIComponent(req.url).substring( + protocol.length + 3, + ); fs.readFile(path.join(dir, file), (err, data) => { callback(err, { data: data }); }); } else if (protocol === 'fonts') { const parts = req.url.split('/'); - const fontstack = unescape(parts[2]); + const fontstack = decodeURIComponent(parts[2]); const range = parts[3].split('.')[0]; try { @@ -1039,6 +1041,25 @@ export const serve_rendered = { const format = extensionToFormat[extension] || ''; createEmptyResponse(format, '', callback); } + } else if (protocol === 'file') { + const name = decodeURI(req.url).substring(protocol.length + 3); + const file = path.join(options.paths['files'], name); + if (await existsP(file)) { + const inputFileStats = await fsp.stat(file); + if (!inputFileStats.isFile() || inputFileStats.size === 0) { + throw Error( + `File is not valid: "${req.url}" - resolved to "${file}"`, + ); + } + + fs.readFile(file, (err, data) => { + callback(err, { data: data }); + }); + } else { + throw Error( + `File does not exist: "${req.url}" - resolved to "${file}"`, + ); + } } }, }); diff --git a/src/serve_style.js b/src/serve_style.js index 44d5e04..5d3b469 100644 --- a/src/serve_style.js +++ b/src/serve_style.js @@ -26,6 +26,9 @@ export const serve_style = { for (const name of Object.keys(styleJSON_.sources)) { const source = styleJSON_.sources[name]; source.url = fixUrl(req, source.url, item.publicUrl); + if (typeof source.data == 'string') { + source.data = fixUrl(req, source.data, item.publicUrl); + } } // mapbox-gl-js viewer cannot handle sprite urls with query if (styleJSON_.sprite) { @@ -89,7 +92,7 @@ export const serve_style = { try { styleFileData = fs.readFileSync(styleFile); // TODO: could be made async if this function was } catch (e) { - console.log('Error reading style file'); + console.log(`Error reading style file "${params.style}"`); return false; } @@ -128,6 +131,16 @@ export const serve_style = { } source.url = `local://data/${identifier}.json`; } + + let data = source.data; + if (data && typeof data == 'string' && data.startsWith('file://')) { + source.data = + 'local://files' + + path.resolve( + '/', + data.replace('file://', '').replace(options.paths.files, ''), + ); + } } for (const obj of styleJSON.layers) { diff --git a/src/server.js b/src/server.js index 55e3f2e..db3504d 100644 --- a/src/server.js +++ b/src/server.js @@ -94,24 +94,22 @@ function start(opts) { paths.sprites = path.resolve(paths.root, paths.sprites || ''); paths.mbtiles = path.resolve(paths.root, paths.mbtiles || ''); paths.pmtiles = path.resolve(paths.root, paths.pmtiles || ''); - paths.icons = path.resolve(paths.root, paths.icons || ''); + paths.icons = path.resolve( + paths.root, + paths.icons || 'public/resources/images', + ); + paths.files = path.resolve(paths.root, paths.files || 'public/files'); const startupPromises = []; - const checkPath = (type) => { + for (const type of Object.keys(paths)) { if (!fs.existsSync(paths[type])) { console.error( `The specified path for "${type}" does not exist (${paths[type]}).`, ); process.exit(1); } - }; - checkPath('styles'); - checkPath('fonts'); - checkPath('sprites'); - checkPath('mbtiles'); - checkPath('pmtiles'); - checkPath('icons'); + } /** * Recursively get all files within a directory. @@ -161,6 +159,7 @@ function start(opts) { } app.use('/data/', serve_data.init(options, serving.data)); + app.use('/files/', express.static(paths.files)); app.use('/styles/', serve_style.init(options, serving.styles)); if (!isLight) { startupPromises.push( diff --git a/test/setup.js b/test/setup.js index 34fba67..d88c9ef 100644 --- a/test/setup.js +++ b/test/setup.js @@ -9,7 +9,6 @@ global.supertest = supertest; before(function () { console.log('global setup'); - process.chdir('test_data'); const running = server({ configPath: 'config.json', port: 8888,