tileserver-gl-light 4.13.0 → 4.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.dockerignore CHANGED
@@ -5,3 +5,4 @@
5
5
  !package.json
6
6
  !package-lock.json
7
7
  !docker-entrypoint.sh
8
+ **.gitignore
package/docs/config.rst CHANGED
@@ -17,7 +17,8 @@ Example:
17
17
  "icons": "icons",
18
18
  "styles": "styles",
19
19
  "mbtiles": "data",
20
- "pmtiles": "data"
20
+ "pmtiles": "data",
21
+ "files": "files"
21
22
  },
22
23
  "domains": [
23
24
  "localhost:8080",
@@ -77,7 +77,7 @@ Static images
77
77
  * scales with ``scale`` parameter since image placement is relative to it's size
78
78
  * e.g. ``2,-4`` - Image will be moved 2 pixel to the right and 4 pixel in the upwards direction from the provided location
79
79
 
80
- * e.g. ``5.9,45.8|marker-start.svg|scale:0.5|offset:2,-4``
80
+ * e.g. ``5.9,45.8|marker-icon.png|scale:0.5|offset:2,-4``
81
81
  * can be provided multiple times
82
82
 
83
83
  * ``padding`` - "percentage" padding for fitted endpoints (area-based and path autofit)
@@ -100,6 +100,18 @@ Source data
100
100
 
101
101
  * TileJSON at ``/data/{id}.json``
102
102
 
103
+ Static files
104
+ ===========
105
+ * Static files are served at ``/files/{filename}``
106
+
107
+ * The source folder can be configured (``options.paths.files``), default is ``public/files``
108
+
109
+ * This feature can be used to serve ``geojson`` files for styles and rendered tiles.
110
+
111
+ * 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.
112
+
113
+ * Use ``file://{filename}`` to have matching paths for both endoints
114
+
103
115
  TileJSON arrays
104
116
  ===============
105
117
  Array of all TileJSONs is at ``[/{tileSize}]/index.json`` (``[/{tileSize}]/rendered.json``; ``/data.json``)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tileserver-gl-light",
3
- "version": "4.13.0",
3
+ "version": "4.13.2",
4
4
  "description": "Map tile server for JSON GL styles - serving vector tiles",
5
5
  "main": "src/main.js",
6
6
  "bin": "src/main.js",
@@ -25,7 +25,7 @@
25
25
  "@maplibre/maplibre-gl-style-spec": "20.3.1",
26
26
  "@sindresorhus/fnv1a": "3.1.0",
27
27
  "advanced-pool": "0.3.3",
28
- "axios": "^1.7.5",
28
+ "axios": "^1.7.6",
29
29
  "chokidar": "3.6.0",
30
30
  "clone": "2.1.2",
31
31
  "color": "4.2.3",
package/src/main.js CHANGED
@@ -8,9 +8,11 @@ import path from 'path';
8
8
  import { fileURLToPath } from 'url';
9
9
  import axios from 'axios';
10
10
  import { server } from './server.js';
11
- import MBTiles from '@mapbox/mbtiles';
12
11
  import { isValidHttpUrl } from './utils.js';
13
12
  import { openPMtiles, getPMtilesInfo } from './pmtiles_adapter.js';
13
+ import { program } from 'commander';
14
+ import { existsP } from './promises.js';
15
+ import { openMbTilesWrapper } from './mbtiles_wrapper.js';
14
16
 
15
17
  const __filename = fileURLToPath(import.meta.url);
16
18
  const __dirname = path.dirname(__filename);
@@ -23,8 +25,6 @@ if (args.length >= 3 && args[2][0] !== '-') {
23
25
  args.splice(2, 0, '--mbtiles');
24
26
  }
25
27
 
26
- import { program } from 'commander';
27
- import { existsP } from './promises.js';
28
28
  program
29
29
  .description('tileserver-gl startup options')
30
30
  .usage('tileserver-gl [mbtiles] [options]')
@@ -184,62 +184,55 @@ const startWithInputFile = async (inputFile) => {
184
184
  );
185
185
  process.exit(1);
186
186
  }
187
- const instance = new MBTiles(inputFile + '?mode=ro', (err) => {
188
- if (err) {
189
- console.log('ERROR: Unable to open MBTiles.');
190
- console.log(`Make sure ${path.basename(inputFile)} is valid MBTiles.`);
191
- process.exit(1);
192
- }
193
-
194
- instance.getInfo(async (err, info) => {
195
- if (err || !info) {
196
- console.log('ERROR: Metadata missing in the MBTiles.');
197
- console.log(
198
- `Make sure ${path.basename(inputFile)} is valid MBTiles.`,
199
- );
200
- process.exit(1);
201
- }
202
- const bounds = info.bounds;
187
+ let info;
188
+ try {
189
+ const mbw = await openMbTilesWrapper(inputFile);
190
+ info = await mbw.getInfo();
191
+ if (!info) throw new Error('Metadata missing in the MBTiles.');
192
+ } catch (err) {
193
+ console.log('ERROR: Unable to open MBTiles or read metadata:', err);
194
+ console.log(`Make sure ${path.basename(inputFile)} is valid MBTiles.`);
195
+ process.exit(1);
196
+ }
197
+ const bounds = info.bounds;
203
198
 
204
- if (
205
- info.format === 'pbf' &&
206
- info.name.toLowerCase().indexOf('openmaptiles') > -1
207
- ) {
208
- config['data'][`v3`] = {
209
- mbtiles: path.basename(inputFile),
210
- };
199
+ if (
200
+ info.format === 'pbf' &&
201
+ info.name.toLowerCase().indexOf('openmaptiles') > -1
202
+ ) {
203
+ config['data'][`v3`] = {
204
+ mbtiles: path.basename(inputFile),
205
+ };
211
206
 
212
- const styles = await fsp.readdir(path.resolve(styleDir, 'styles'));
213
- for (const styleName of styles) {
214
- const styleFileRel = styleName + '/style.json';
215
- const styleFile = path.resolve(styleDir, 'styles', styleFileRel);
216
- if (await existsP(styleFile)) {
217
- config['styles'][styleName] = {
218
- style: styleFileRel,
219
- tilejson: {
220
- bounds,
221
- },
222
- };
223
- }
224
- }
225
- } else {
226
- console.log(
227
- `WARN: MBTiles not in "openmaptiles" format. Serving raw data only...`,
228
- );
229
- config['data'][(info.id || 'mbtiles').replace(/[?/:]/g, '_')] = {
230
- mbtiles: path.basename(inputFile),
207
+ const styles = await fsp.readdir(path.resolve(styleDir, 'styles'));
208
+ for (const styleName of styles) {
209
+ const styleFileRel = styleName + '/style.json';
210
+ const styleFile = path.resolve(styleDir, 'styles', styleFileRel);
211
+ if (await existsP(styleFile)) {
212
+ config['styles'][styleName] = {
213
+ style: styleFileRel,
214
+ tilejson: {
215
+ bounds,
216
+ },
231
217
  };
232
218
  }
219
+ }
220
+ } else {
221
+ console.log(
222
+ `WARN: MBTiles not in "openmaptiles" format. Serving raw data only...`,
223
+ );
224
+ config['data'][(info.id || 'mbtiles').replace(/[?/:]/g, '_')] = {
225
+ mbtiles: path.basename(inputFile),
226
+ };
227
+ }
233
228
 
234
- if (opts.verbose) {
235
- console.log(JSON.stringify(config, undefined, 2));
236
- } else {
237
- console.log('Run with --verbose to see the config file here.');
238
- }
229
+ if (opts.verbose) {
230
+ console.log(JSON.stringify(config, undefined, 2));
231
+ } else {
232
+ console.log('Run with --verbose to see the config file here.');
233
+ }
239
234
 
240
- return startServer(null, config);
241
- });
242
- });
235
+ return startServer(null, config);
243
236
  }
244
237
  };
245
238
 
@@ -0,0 +1,46 @@
1
+ import MBTiles from '@mapbox/mbtiles';
2
+ import util from 'node:util';
3
+
4
+ /**
5
+ * Promise-ful wrapper around the MBTiles class.
6
+ */
7
+ class MBTilesWrapper {
8
+ constructor(mbtiles) {
9
+ this._mbtiles = mbtiles;
10
+ this._getInfoP = util.promisify(mbtiles.getInfo.bind(mbtiles));
11
+ }
12
+
13
+ /**
14
+ * Get the underlying MBTiles object.
15
+ * @returns {MBTiles}
16
+ */
17
+ getMbTiles() {
18
+ return this._mbtiles;
19
+ }
20
+
21
+ /**
22
+ * Get the MBTiles metadata object.
23
+ * @returns {Promise<object>}
24
+ */
25
+ getInfo() {
26
+ return this._getInfoP();
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Open the given MBTiles file and return a promise that resolves with a
32
+ * MBTilesWrapper instance.
33
+ * @param inputFile Input file
34
+ * @returns {Promise<MBTilesWrapper>}
35
+ */
36
+ export function openMbTilesWrapper(inputFile) {
37
+ return new Promise((resolve, reject) => {
38
+ const mbtiles = new MBTiles(inputFile + '?mode=ro', (err) => {
39
+ if (err) {
40
+ reject(err);
41
+ return;
42
+ }
43
+ resolve(new MBTilesWrapper(mbtiles));
44
+ });
45
+ });
46
+ }
package/src/serve_data.js CHANGED
@@ -5,7 +5,6 @@ import path from 'path';
5
5
 
6
6
  import clone from 'clone';
7
7
  import express from 'express';
8
- import MBTiles from '@mapbox/mbtiles';
9
8
  import Pbf from 'pbf';
10
9
  import { VectorTile } from '@mapbox/vector-tile';
11
10
 
@@ -16,6 +15,7 @@ import {
16
15
  openPMtiles,
17
16
  } from './pmtiles_adapter.js';
18
17
  import { gunzipP, gzipP } from './promises.js';
18
+ import { openMbTilesWrapper } from './mbtiles_wrapper.js';
19
19
 
20
20
  export const serve_data = {
21
21
  init: (options, repo) => {
@@ -242,39 +242,25 @@ export const serve_data = {
242
242
  }
243
243
  } else if (inputType === 'mbtiles') {
244
244
  sourceType = 'mbtiles';
245
- const sourceInfoPromise = new Promise((resolve, reject) => {
246
- source = new MBTiles(inputFile + '?mode=ro', (err) => {
247
- if (err) {
248
- reject(err);
249
- return;
250
- }
251
- source.getInfo((err, info) => {
252
- if (err) {
253
- reject(err);
254
- return;
255
- }
256
- tileJSON['name'] = id;
257
- tileJSON['format'] = 'pbf';
258
-
259
- Object.assign(tileJSON, info);
245
+ const mbw = await openMbTilesWrapper(inputFile);
246
+ const info = await mbw.getInfo();
247
+ source = mbw.getMbTiles();
248
+ tileJSON['name'] = id;
249
+ tileJSON['format'] = 'pbf';
260
250
 
261
- tileJSON['tilejson'] = '2.0.0';
262
- delete tileJSON['filesize'];
263
- delete tileJSON['mtime'];
264
- delete tileJSON['scheme'];
251
+ Object.assign(tileJSON, info);
265
252
 
266
- Object.assign(tileJSON, params.tilejson || {});
267
- fixTileJSONCenter(tileJSON);
253
+ tileJSON['tilejson'] = '2.0.0';
254
+ delete tileJSON['filesize'];
255
+ delete tileJSON['mtime'];
256
+ delete tileJSON['scheme'];
268
257
 
269
- if (options.dataDecoratorFunc) {
270
- tileJSON = options.dataDecoratorFunc(id, 'tilejson', tileJSON);
271
- }
272
- resolve();
273
- });
274
- });
275
- });
258
+ Object.assign(tileJSON, params.tilejson || {});
259
+ fixTileJSONCenter(tileJSON);
276
260
 
277
- await sourceInfoPromise;
261
+ if (options.dataDecoratorFunc) {
262
+ tileJSON = options.dataDecoratorFunc(id, 'tilejson', tileJSON);
263
+ }
278
264
  }
279
265
 
280
266
  repo[id] = {
@@ -24,7 +24,6 @@ import express from 'express';
24
24
  import sanitize from 'sanitize-filename';
25
25
  import SphericalMercator from '@mapbox/sphericalmercator';
26
26
  import mlgl from '@maplibre/maplibre-gl-native';
27
- import MBTiles from '@mapbox/mbtiles';
28
27
  import polyline from '@mapbox/polyline';
29
28
  import proj4 from 'proj4';
30
29
  import axios from 'axios';
@@ -42,7 +41,8 @@ import {
42
41
  } from './pmtiles_adapter.js';
43
42
  import { renderOverlay, renderWatermark, renderAttribution } from './render.js';
44
43
  import fsp from 'node:fs/promises';
45
- import { gunzipP } from './promises.js';
44
+ import { existsP, gunzipP } from './promises.js';
45
+ import { openMbTilesWrapper } from './mbtiles_wrapper.js';
46
46
 
47
47
  const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+.?\\d+)';
48
48
  const PATH_PATTERN =
@@ -893,13 +893,15 @@ export const serve_rendered = {
893
893
  // console.log('Handling request:', req);
894
894
  if (protocol === 'sprites') {
895
895
  const dir = options.paths[protocol];
896
- const file = unescape(req.url).substring(protocol.length + 3);
896
+ const file = decodeURIComponent(req.url).substring(
897
+ protocol.length + 3,
898
+ );
897
899
  fs.readFile(path.join(dir, file), (err, data) => {
898
900
  callback(err, { data: data });
899
901
  });
900
902
  } else if (protocol === 'fonts') {
901
903
  const parts = req.url.split('/');
902
- const fontstack = unescape(parts[2]);
904
+ const fontstack = decodeURIComponent(parts[2]);
903
905
  const range = parts[3].split('.')[0];
904
906
 
905
907
  try {
@@ -1039,6 +1041,25 @@ export const serve_rendered = {
1039
1041
  const format = extensionToFormat[extension] || '';
1040
1042
  createEmptyResponse(format, '', callback);
1041
1043
  }
1044
+ } else if (protocol === 'file') {
1045
+ const name = decodeURI(req.url).substring(protocol.length + 3);
1046
+ const file = path.join(options.paths['files'], name);
1047
+ if (await existsP(file)) {
1048
+ const inputFileStats = await fsp.stat(file);
1049
+ if (!inputFileStats.isFile() || inputFileStats.size === 0) {
1050
+ throw Error(
1051
+ `File is not valid: "${req.url}" - resolved to "${file}"`,
1052
+ );
1053
+ }
1054
+
1055
+ fs.readFile(file, (err, data) => {
1056
+ callback(err, { data: data });
1057
+ });
1058
+ } else {
1059
+ throw Error(
1060
+ `File does not exist: "${req.url}" - resolved to "${file}"`,
1061
+ );
1062
+ }
1042
1063
  }
1043
1064
  },
1044
1065
  });
@@ -1131,7 +1152,6 @@ export const serve_rendered = {
1131
1152
  };
1132
1153
  repo[id] = repoobj;
1133
1154
 
1134
- const queue = [];
1135
1155
  for (const name of Object.keys(styleJSON.sources)) {
1136
1156
  let sourceType;
1137
1157
  let source = styleJSON.sources[name];
@@ -1205,69 +1225,52 @@ export const serve_rendered = {
1205
1225
  }
1206
1226
  }
1207
1227
  } else {
1208
- queue.push(
1209
- new Promise(async (resolve, reject) => {
1210
- inputFile = path.resolve(options.paths.mbtiles, inputFile);
1211
- const inputFileStats = await fsp.stat(inputFile);
1212
- if (!inputFileStats.isFile() || inputFileStats.size === 0) {
1213
- throw Error(`Not valid MBTiles file: "${inputFile}"`);
1214
- }
1215
- map.sources[name] = new MBTiles(inputFile + '?mode=ro', (err) => {
1216
- map.sources[name].getInfo((err, info) => {
1217
- if (err) {
1218
- console.error(err);
1219
- return;
1220
- }
1221
- map.sourceTypes[name] = 'mbtiles';
1222
-
1223
- if (!repoobj.dataProjWGStoInternalWGS && info.proj4) {
1224
- // how to do this for multiple sources with different proj4 defs?
1225
- const to3857 = proj4('EPSG:3857');
1226
- const toDataProj = proj4(info.proj4);
1227
- repoobj.dataProjWGStoInternalWGS = (xy) =>
1228
- to3857.inverse(toDataProj.forward(xy));
1229
- }
1228
+ const inputFileStats = await fsp.stat(inputFile);
1229
+ if (!inputFileStats.isFile() || inputFileStats.size === 0) {
1230
+ throw Error(`Not valid MBTiles file: "${inputFile}"`);
1231
+ }
1232
+ const mbw = await openMbTilesWrapper(inputFile);
1233
+ const info = await mbw.getInfo();
1234
+ map.sources[name] = mbw.getMbTiles();
1235
+ map.sourceTypes[name] = 'mbtiles';
1230
1236
 
1231
- const type = source.type;
1232
- Object.assign(source, info);
1233
- source.type = type;
1234
- source.tiles = [
1235
- // meta url which will be detected when requested
1236
- `mbtiles://${name}/{z}/{x}/{y}.${info.format || 'pbf'}`,
1237
- ];
1238
- delete source.scheme;
1239
-
1240
- if (options.dataDecoratorFunc) {
1241
- source = options.dataDecoratorFunc(
1242
- name,
1243
- 'tilejson',
1244
- source,
1245
- );
1246
- }
1237
+ if (!repoobj.dataProjWGStoInternalWGS && info.proj4) {
1238
+ // how to do this for multiple sources with different proj4 defs?
1239
+ const to3857 = proj4('EPSG:3857');
1240
+ const toDataProj = proj4(info.proj4);
1241
+ repoobj.dataProjWGStoInternalWGS = (xy) =>
1242
+ to3857.inverse(toDataProj.forward(xy));
1243
+ }
1247
1244
 
1248
- if (
1249
- !attributionOverride &&
1250
- source.attribution &&
1251
- source.attribution.length > 0
1252
- ) {
1253
- if (!tileJSON.attribution.includes(source.attribution)) {
1254
- if (tileJSON.attribution.length > 0) {
1255
- tileJSON.attribution += ' | ';
1256
- }
1257
- tileJSON.attribution += source.attribution;
1258
- }
1259
- }
1260
- resolve();
1261
- });
1262
- });
1263
- }),
1264
- );
1245
+ const type = source.type;
1246
+ Object.assign(source, info);
1247
+ source.type = type;
1248
+ source.tiles = [
1249
+ // meta url which will be detected when requested
1250
+ `mbtiles://${name}/{z}/{x}/{y}.${info.format || 'pbf'}`,
1251
+ ];
1252
+ delete source.scheme;
1253
+
1254
+ if (options.dataDecoratorFunc) {
1255
+ source = options.dataDecoratorFunc(name, 'tilejson', source);
1256
+ }
1257
+
1258
+ if (
1259
+ !attributionOverride &&
1260
+ source.attribution &&
1261
+ source.attribution.length > 0
1262
+ ) {
1263
+ if (!tileJSON.attribution.includes(source.attribution)) {
1264
+ if (tileJSON.attribution.length > 0) {
1265
+ tileJSON.attribution += ' | ';
1266
+ }
1267
+ tileJSON.attribution += source.attribution;
1268
+ }
1269
+ }
1265
1270
  }
1266
1271
  }
1267
1272
  }
1268
1273
 
1269
- await Promise.all(queue);
1270
-
1271
1274
  // standard and @2x tiles are much more usual -> default to larger pools
1272
1275
  const minPoolSizes = options.minRendererPoolSizes || [8, 4, 2];
1273
1276
  const maxPoolSizes = options.maxRendererPoolSizes || [16, 8, 4];
@@ -26,6 +26,9 @@ export const serve_style = {
26
26
  for (const name of Object.keys(styleJSON_.sources)) {
27
27
  const source = styleJSON_.sources[name];
28
28
  source.url = fixUrl(req, source.url, item.publicUrl);
29
+ if (typeof source.data == 'string') {
30
+ source.data = fixUrl(req, source.data, item.publicUrl);
31
+ }
29
32
  }
30
33
  // mapbox-gl-js viewer cannot handle sprite urls with query
31
34
  if (styleJSON_.sprite) {
@@ -89,7 +92,7 @@ export const serve_style = {
89
92
  try {
90
93
  styleFileData = fs.readFileSync(styleFile); // TODO: could be made async if this function was
91
94
  } catch (e) {
92
- console.log('Error reading style file');
95
+ console.log(`Error reading style file "${params.style}"`);
93
96
  return false;
94
97
  }
95
98
 
@@ -128,6 +131,16 @@ export const serve_style = {
128
131
  }
129
132
  source.url = `local://data/${identifier}.json`;
130
133
  }
134
+
135
+ let data = source.data;
136
+ if (data && typeof data == 'string' && data.startsWith('file://')) {
137
+ source.data =
138
+ 'local://files' +
139
+ path.resolve(
140
+ '/',
141
+ data.replace('file://', '').replace(options.paths.files, ''),
142
+ );
143
+ }
131
144
  }
132
145
 
133
146
  for (const obj of styleJSON.layers) {
package/src/server.js CHANGED
@@ -94,24 +94,23 @@ function start(opts) {
94
94
  paths.sprites = path.resolve(paths.root, paths.sprites || '');
95
95
  paths.mbtiles = path.resolve(paths.root, paths.mbtiles || '');
96
96
  paths.pmtiles = path.resolve(paths.root, paths.pmtiles || '');
97
- paths.icons = path.resolve(paths.root, paths.icons || '');
97
+ paths.icons = paths.icons
98
+ ? path.resolve(paths.root, paths.icons)
99
+ : path.resolve(__dirname, '../public/resources/images');
100
+ paths.files = paths.files
101
+ ? path.resolve(paths.root, paths.files)
102
+ : path.resolve(__dirname, '../public/files');
98
103
 
99
104
  const startupPromises = [];
100
105
 
101
- const checkPath = (type) => {
106
+ for (const type of Object.keys(paths)) {
102
107
  if (!fs.existsSync(paths[type])) {
103
108
  console.error(
104
109
  `The specified path for "${type}" does not exist (${paths[type]}).`,
105
110
  );
106
111
  process.exit(1);
107
112
  }
108
- };
109
- checkPath('styles');
110
- checkPath('fonts');
111
- checkPath('sprites');
112
- checkPath('mbtiles');
113
- checkPath('pmtiles');
114
- checkPath('icons');
113
+ }
115
114
 
116
115
  /**
117
116
  * Recursively get all files within a directory.
@@ -161,6 +160,7 @@ function start(opts) {
161
160
  }
162
161
 
163
162
  app.use('/data/', serve_data.init(options, serving.data));
163
+ app.use('/files/', express.static(paths.files));
164
164
  app.use('/styles/', serve_style.init(options, serving.styles));
165
165
  if (!isLight) {
166
166
  startupPromises.push(