tileserver-gl 4.1.1 → 4.2.0

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/src/server.js CHANGED
@@ -2,8 +2,7 @@
2
2
  'use strict';
3
3
 
4
4
  import os from 'os';
5
- process.env.UV_THREADPOOL_SIZE =
6
- Math.ceil(Math.max(4, os.cpus().length * 1.5));
5
+ process.env.UV_THREADPOOL_SIZE = Math.ceil(Math.max(4, os.cpus().length * 1.5));
7
6
 
8
7
  import fs from 'node:fs';
9
8
  import path from 'path';
@@ -17,20 +16,28 @@ import handlebars from 'handlebars';
17
16
  import SphericalMercator from '@mapbox/sphericalmercator';
18
17
  const mercator = new SphericalMercator();
19
18
  import morgan from 'morgan';
20
- import {serve_data} from './serve_data.js';
21
- import {serve_style} from './serve_style.js';
22
- import {serve_font} from './serve_font.js';
23
- import {getTileUrls, getPublicUrl} from './utils.js';
19
+ import { serve_data } from './serve_data.js';
20
+ import { serve_style } from './serve_style.js';
21
+ import { serve_font } from './serve_font.js';
22
+ import { getTileUrls, getPublicUrl } from './utils.js';
24
23
 
25
- import {fileURLToPath} from 'url';
24
+ import { fileURLToPath } from 'url';
26
25
  const __filename = fileURLToPath(import.meta.url);
27
26
  const __dirname = path.dirname(__filename);
28
- const packageJson = JSON.parse(fs.readFileSync(__dirname + '/../package.json', 'utf8'));
27
+ const packageJson = JSON.parse(
28
+ fs.readFileSync(__dirname + '/../package.json', 'utf8'),
29
+ );
29
30
 
30
31
  const isLight = packageJson.name.slice(-6) === '-light';
31
- const serve_rendered = (await import(`${!isLight ? `./serve_rendered.js` : `./serve_light.js`}`)).serve_rendered;
32
-
33
- export function server(opts) {
32
+ const serve_rendered = (
33
+ await import(`${!isLight ? `./serve_rendered.js` : `./serve_light.js`}`)
34
+ ).serve_rendered;
35
+
36
+ /**
37
+ *
38
+ * @param opts
39
+ */
40
+ function start(opts) {
34
41
  console.log('Starting server');
35
42
 
36
43
  const app = express().disable('x-powered-by');
@@ -38,18 +45,24 @@ export function server(opts) {
38
45
  styles: {},
39
46
  rendered: {},
40
47
  data: {},
41
- fonts: {}
48
+ fonts: {},
42
49
  };
43
50
 
44
51
  app.enable('trust proxy');
45
52
 
46
53
  if (process.env.NODE_ENV !== 'test') {
47
- const defaultLogFormat = process.env.NODE_ENV === 'production' ? 'tiny' : 'dev';
54
+ const defaultLogFormat =
55
+ process.env.NODE_ENV === 'production' ? 'tiny' : 'dev';
48
56
  const logFormat = opts.logFormat || defaultLogFormat;
49
- app.use(morgan(logFormat, {
50
- stream: opts.logFile ? fs.createWriteStream(opts.logFile, {flags: 'a'}) : process.stdout,
51
- skip: (req, res) => opts.silent && (res.statusCode === 200 || res.statusCode === 304)
52
- }));
57
+ app.use(
58
+ morgan(logFormat, {
59
+ stream: opts.logFile
60
+ ? fs.createWriteStream(opts.logFile, { flags: 'a' })
61
+ : process.stdout,
62
+ skip: (req, res) =>
63
+ opts.silent && (res.statusCode === 200 || res.statusCode === 304),
64
+ }),
65
+ );
53
66
  }
54
67
 
55
68
  let config = opts.config || null;
@@ -74,17 +87,21 @@ export function server(opts) {
74
87
  options.paths = paths;
75
88
  paths.root = path.resolve(
76
89
  configPath ? path.dirname(configPath) : process.cwd(),
77
- paths.root || '');
90
+ paths.root || '',
91
+ );
78
92
  paths.styles = path.resolve(paths.root, paths.styles || '');
79
93
  paths.fonts = path.resolve(paths.root, paths.fonts || '');
80
94
  paths.sprites = path.resolve(paths.root, paths.sprites || '');
81
95
  paths.mbtiles = path.resolve(paths.root, paths.mbtiles || '');
96
+ paths.icons = path.resolve(paths.root, paths.icons || '');
82
97
 
83
98
  const startupPromises = [];
84
99
 
85
100
  const checkPath = (type) => {
86
101
  if (!fs.existsSync(paths[type])) {
87
- console.error(`The specified path for "${type}" does not exist (${paths[type]}).`);
102
+ console.error(
103
+ `The specified path for "${type}" does not exist (${paths[type]}).`,
104
+ );
88
105
  process.exit(1);
89
106
  }
90
107
  };
@@ -92,10 +109,51 @@ export function server(opts) {
92
109
  checkPath('fonts');
93
110
  checkPath('sprites');
94
111
  checkPath('mbtiles');
112
+ checkPath('icons');
113
+
114
+ /**
115
+ * Recursively get all files within a directory.
116
+ * Inspired by https://stackoverflow.com/a/45130990/10133863
117
+ *
118
+ * @param {string} directory Absolute path to a directory to get files from.
119
+ */
120
+ const getFiles = async (directory) => {
121
+ // Fetch all entries of the directory and attach type information
122
+ const dirEntries = await fs.promises.readdir(directory, {
123
+ withFileTypes: true,
124
+ });
125
+
126
+ // Iterate through entries and return the relative file-path to the icon directory if it is not a directory
127
+ // otherwise initiate a recursive call
128
+ const files = await Promise.all(
129
+ dirEntries.map((dirEntry) => {
130
+ const entryPath = path.resolve(directory, dirEntry.name);
131
+ return dirEntry.isDirectory()
132
+ ? getFiles(entryPath)
133
+ : entryPath.replace(paths.icons + path.sep, '');
134
+ }),
135
+ );
136
+
137
+ // Flatten the list of files to a single array
138
+ return files.flat();
139
+ };
140
+
141
+ // Load all available icons into a settings object
142
+ startupPromises.push(
143
+ new Promise((resolve) => {
144
+ getFiles(paths.icons).then((files) => {
145
+ paths.availableIcons = files;
146
+ resolve();
147
+ });
148
+ }),
149
+ );
95
150
 
96
151
  if (options.dataDecorator) {
97
152
  try {
98
- options.dataDecoratorFunc = require(path.resolve(paths.root, options.dataDecorator));
153
+ options.dataDecoratorFunc = require(path.resolve(
154
+ paths.root,
155
+ options.dataDecorator,
156
+ ));
99
157
  } catch (e) {}
100
158
  }
101
159
 
@@ -109,54 +167,69 @@ export function server(opts) {
109
167
  app.use('/styles/', serve_style.init(options, serving.styles));
110
168
  if (!isLight) {
111
169
  startupPromises.push(
112
- serve_rendered.init(options, serving.rendered)
113
- .then((sub) => {
114
- app.use('/styles/', sub);
115
- })
170
+ serve_rendered.init(options, serving.rendered).then((sub) => {
171
+ app.use('/styles/', sub);
172
+ }),
116
173
  );
117
174
  }
118
175
 
119
176
  const addStyle = (id, item, allowMoreData, reportFonts) => {
120
177
  let success = true;
121
178
  if (item.serve_data !== false) {
122
- success = serve_style.add(options, serving.styles, item, id, opts.publicUrl,
123
- (mbtiles, fromData) => {
124
- let dataItemId;
125
- for (const id of Object.keys(data)) {
126
- if (fromData) {
127
- if (id === mbtiles) {
128
- dataItemId = id;
129
- }
130
- } else {
131
- if (data[id].mbtiles === mbtiles) {
132
- dataItemId = id;
133
- }
179
+ success = serve_style.add(
180
+ options,
181
+ serving.styles,
182
+ item,
183
+ id,
184
+ opts.publicUrl,
185
+ (mbtiles, fromData) => {
186
+ let dataItemId;
187
+ for (const id of Object.keys(data)) {
188
+ if (fromData) {
189
+ if (id === mbtiles) {
190
+ dataItemId = id;
134
191
  }
135
- }
136
- if (dataItemId) { // mbtiles exist in the data config
137
- return dataItemId;
138
192
  } else {
139
- if (fromData || !allowMoreData) {
140
- console.log(`ERROR: style "${item.style}" using unknown mbtiles "${mbtiles}"! Skipping...`);
141
- return undefined;
142
- } else {
143
- let id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles;
144
- while (data[id]) id += '_';
145
- data[id] = {
146
- 'mbtiles': mbtiles
147
- };
148
- return id;
193
+ if (data[id].mbtiles === mbtiles) {
194
+ dataItemId = id;
149
195
  }
150
196
  }
151
- }, (font) => {
152
- if (reportFonts) {
153
- serving.fonts[font] = true;
197
+ }
198
+ if (dataItemId) {
199
+ // mbtiles exist in the data config
200
+ return dataItemId;
201
+ } else {
202
+ if (fromData || !allowMoreData) {
203
+ console.log(
204
+ `ERROR: style "${item.style}" using unknown mbtiles "${mbtiles}"! Skipping...`,
205
+ );
206
+ return undefined;
207
+ } else {
208
+ let id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles;
209
+ while (data[id]) id += '_';
210
+ data[id] = {
211
+ mbtiles: mbtiles,
212
+ };
213
+ return id;
154
214
  }
155
- });
215
+ }
216
+ },
217
+ (font) => {
218
+ if (reportFonts) {
219
+ serving.fonts[font] = true;
220
+ }
221
+ },
222
+ );
156
223
  }
157
224
  if (success && item.serve_rendered !== false) {
158
225
  if (!isLight) {
159
- startupPromises.push(serve_rendered.add(options, serving.rendered, item, id, opts.publicUrl,
226
+ startupPromises.push(
227
+ serve_rendered.add(
228
+ options,
229
+ serving.rendered,
230
+ item,
231
+ id,
232
+ opts.publicUrl,
160
233
  (mbtiles) => {
161
234
  let mbtilesFile;
162
235
  for (const id of Object.keys(data)) {
@@ -165,8 +238,9 @@ export function server(opts) {
165
238
  }
166
239
  }
167
240
  return mbtilesFile;
168
- }
169
- ));
241
+ },
242
+ ),
243
+ );
170
244
  } else {
171
245
  item.serve_rendered = false;
172
246
  }
@@ -184,9 +258,9 @@ export function server(opts) {
184
258
  }
185
259
 
186
260
  startupPromises.push(
187
- serve_font(options, serving.fonts).then((sub) => {
188
- app.use('/', sub);
189
- })
261
+ serve_font(options, serving.fonts).then((sub) => {
262
+ app.use('/', sub);
263
+ }),
190
264
  );
191
265
 
192
266
  for (const id of Object.keys(data)) {
@@ -197,61 +271,65 @@ export function server(opts) {
197
271
  }
198
272
 
199
273
  startupPromises.push(
200
- serve_data.add(options, serving.data, item, id, opts.publicUrl)
274
+ serve_data.add(options, serving.data, item, id, opts.publicUrl),
201
275
  );
202
276
  }
203
277
 
204
278
  if (options.serveAllStyles) {
205
- fs.readdir(options.paths.styles, {withFileTypes: true}, (err, files) => {
279
+ fs.readdir(options.paths.styles, { withFileTypes: true }, (err, files) => {
206
280
  if (err) {
207
281
  return;
208
282
  }
209
283
  for (const file of files) {
210
- if (file.isFile() &&
211
- path.extname(file.name).toLowerCase() == '.json') {
284
+ if (file.isFile() && path.extname(file.name).toLowerCase() == '.json') {
212
285
  const id = path.basename(file.name, '.json');
213
286
  const item = {
214
- style: file.name
287
+ style: file.name,
215
288
  };
216
289
  addStyle(id, item, false, false);
217
290
  }
218
291
  }
219
292
  });
220
293
 
221
- const watcher = chokidar.watch(path.join(options.paths.styles, '*.json'),
222
- {
223
- });
224
- watcher.on('all',
225
- (eventType, filename) => {
226
- if (filename) {
227
- const id = path.basename(filename, '.json');
228
- console.log(`Style "${id}" changed, updating...`);
229
-
230
- serve_style.remove(serving.styles, id);
231
- if (!isLight) {
232
- serve_rendered.remove(serving.rendered, id);
233
- }
294
+ const watcher = chokidar.watch(
295
+ path.join(options.paths.styles, '*.json'),
296
+ {},
297
+ );
298
+ watcher.on('all', (eventType, filename) => {
299
+ if (filename) {
300
+ const id = path.basename(filename, '.json');
301
+ console.log(`Style "${id}" changed, updating...`);
302
+
303
+ serve_style.remove(serving.styles, id);
304
+ if (!isLight) {
305
+ serve_rendered.remove(serving.rendered, id);
306
+ }
234
307
 
235
- if (eventType == 'add' || eventType == 'change') {
236
- const item = {
237
- style: filename
238
- };
239
- addStyle(id, item, false, false);
240
- }
241
- }
242
- });
308
+ if (eventType == 'add' || eventType == 'change') {
309
+ const item = {
310
+ style: filename,
311
+ };
312
+ addStyle(id, item, false, false);
313
+ }
314
+ }
315
+ });
243
316
  }
244
317
 
245
318
  app.get('/styles.json', (req, res, next) => {
246
319
  const result = [];
247
- const query = req.query.key ? (`?key=${encodeURIComponent(req.query.key)}`) : '';
320
+ const query = req.query.key
321
+ ? `?key=${encodeURIComponent(req.query.key)}`
322
+ : '';
248
323
  for (const id of Object.keys(serving.styles)) {
249
324
  const styleJSON = serving.styles[id].styleJSON;
250
325
  result.push({
251
326
  version: styleJSON.version,
252
327
  name: styleJSON.name,
253
328
  id: id,
254
- url: `${getPublicUrl(opts.publicUrl, req)}styles/${id}/style.json${query}`
329
+ url: `${getPublicUrl(
330
+ opts.publicUrl,
331
+ req,
332
+ )}styles/${id}/style.json${query}`,
255
333
  });
256
334
  }
257
335
  res.send(result);
@@ -266,9 +344,16 @@ export function server(opts) {
266
344
  } else {
267
345
  path = `${type}/${id}`;
268
346
  }
269
- info.tiles = getTileUrls(req, info.tiles, path, info.format, opts.publicUrl, {
270
- 'pbf': options.pbfAlias
271
- });
347
+ info.tiles = getTileUrls(
348
+ req,
349
+ info.tiles,
350
+ path,
351
+ info.format,
352
+ opts.publicUrl,
353
+ {
354
+ pbf: options.pbfAlias,
355
+ },
356
+ );
272
357
  arr.push(info);
273
358
  }
274
359
  return arr;
@@ -294,40 +379,49 @@ export function server(opts) {
294
379
  if (template === 'index') {
295
380
  if (options.frontPage === false) {
296
381
  return;
297
- } else if (options.frontPage &&
298
- options.frontPage.constructor === String) {
382
+ } else if (
383
+ options.frontPage &&
384
+ options.frontPage.constructor === String
385
+ ) {
299
386
  templateFile = path.resolve(paths.root, options.frontPage);
300
387
  }
301
388
  }
302
- startupPromises.push(new Promise((resolve, reject) => {
303
- fs.readFile(templateFile, (err, content) => {
304
- if (err) {
305
- err = new Error(`Template not found: ${err.message}`);
306
- reject(err);
307
- return;
308
- }
309
- const compiled = handlebars.compile(content.toString());
310
-
311
- app.use(urlPath, (req, res, next) => {
312
- let data = {};
313
- if (dataGetter) {
314
- data = dataGetter(req);
315
- if (!data) {
316
- return res.status(404).send('Not found');
317
- }
389
+ startupPromises.push(
390
+ new Promise((resolve, reject) => {
391
+ fs.readFile(templateFile, (err, content) => {
392
+ if (err) {
393
+ err = new Error(`Template not found: ${err.message}`);
394
+ reject(err);
395
+ return;
318
396
  }
319
- data['server_version'] = `${packageJson.name} v${packageJson.version}`;
320
- data['public_url'] = opts.publicUrl || '/';
321
- data['is_light'] = isLight;
322
- data['key_query_part'] =
323
- req.query.key ? `key=${encodeURIComponent(req.query.key)}&` : '';
324
- data['key_query'] = req.query.key ? `?key=${encodeURIComponent(req.query.key)}` : '';
325
- if (template === 'wmts') res.set('Content-Type', 'text/xml');
326
- return res.status(200).send(compiled(data));
397
+ const compiled = handlebars.compile(content.toString());
398
+
399
+ app.use(urlPath, (req, res, next) => {
400
+ let data = {};
401
+ if (dataGetter) {
402
+ data = dataGetter(req);
403
+ if (!data) {
404
+ return res.status(404).send('Not found');
405
+ }
406
+ }
407
+ data[
408
+ 'server_version'
409
+ ] = `${packageJson.name} v${packageJson.version}`;
410
+ data['public_url'] = opts.publicUrl || '/';
411
+ data['is_light'] = isLight;
412
+ data['key_query_part'] = req.query.key
413
+ ? `key=${encodeURIComponent(req.query.key)}&`
414
+ : '';
415
+ data['key_query'] = req.query.key
416
+ ? `?key=${encodeURIComponent(req.query.key)}`
417
+ : '';
418
+ if (template === 'wmts') res.set('Content-Type', 'text/xml');
419
+ return res.status(200).send(compiled(data));
420
+ });
421
+ resolve();
327
422
  });
328
- resolve();
329
- });
330
- }));
423
+ }),
424
+ );
331
425
  };
332
426
 
333
427
  serveTemplate('/$', 'index', (req) => {
@@ -340,15 +434,23 @@ export function server(opts) {
340
434
  if (style.serving_rendered) {
341
435
  const center = style.serving_rendered.tileJSON.center;
342
436
  if (center) {
343
- style.viewer_hash = `#${center[2]}/${center[1].toFixed(5)}/${center[0].toFixed(5)}`;
437
+ style.viewer_hash = `#${center[2]}/${center[1].toFixed(
438
+ 5,
439
+ )}/${center[0].toFixed(5)}`;
344
440
 
345
441
  const centerPx = mercator.px([center[0], center[1]], center[2]);
346
- style.thumbnail = `${center[2]}/${Math.floor(centerPx[0] / 256)}/${Math.floor(centerPx[1] / 256)}.png`;
442
+ style.thumbnail = `${center[2]}/${Math.floor(
443
+ centerPx[0] / 256,
444
+ )}/${Math.floor(centerPx[1] / 256)}.png`;
347
445
  }
348
446
 
349
447
  style.xyz_link = getTileUrls(
350
- req, style.serving_rendered.tileJSON.tiles,
351
- `styles/${id}`, style.serving_rendered.tileJSON.format, opts.publicUrl)[0];
448
+ req,
449
+ style.serving_rendered.tileJSON.tiles,
450
+ `styles/${id}`,
451
+ style.serving_rendered.tileJSON.format,
452
+ opts.publicUrl,
453
+ )[0];
352
454
  }
353
455
  }
354
456
  const data = clone(serving.data || {});
@@ -357,19 +459,29 @@ export function server(opts) {
357
459
  const tilejson = data[id].tileJSON;
358
460
  const center = tilejson.center;
359
461
  if (center) {
360
- data_.viewer_hash = `#${center[2]}/${center[1].toFixed(5)}/${center[0].toFixed(5)}`;
462
+ data_.viewer_hash = `#${center[2]}/${center[1].toFixed(
463
+ 5,
464
+ )}/${center[0].toFixed(5)}`;
361
465
  }
362
466
  data_.is_vector = tilejson.format === 'pbf';
363
467
  if (!data_.is_vector) {
364
468
  if (center) {
365
469
  const centerPx = mercator.px([center[0], center[1]], center[2]);
366
- data_.thumbnail = `${center[2]}/${Math.floor(centerPx[0] / 256)}/${Math.floor(centerPx[1] / 256)}.${data_.tileJSON.format}`;
470
+ data_.thumbnail = `${center[2]}/${Math.floor(
471
+ centerPx[0] / 256,
472
+ )}/${Math.floor(centerPx[1] / 256)}.${data_.tileJSON.format}`;
367
473
  }
368
474
 
369
475
  data_.xyz_link = getTileUrls(
370
- req, tilejson.tiles, `data/${id}`, tilejson.format, opts.publicUrl, {
371
- 'pbf': options.pbfAlias
372
- })[0];
476
+ req,
477
+ tilejson.tiles,
478
+ `data/${id}`,
479
+ tilejson.format,
480
+ opts.publicUrl,
481
+ {
482
+ pbf: options.pbfAlias,
483
+ },
484
+ )[0];
373
485
  }
374
486
  if (data_.filesize) {
375
487
  let suffix = 'kB';
@@ -387,7 +499,7 @@ export function server(opts) {
387
499
  }
388
500
  return {
389
501
  styles: Object.keys(styles).length ? styles : null,
390
- data: Object.keys(data).length ? data : null
502
+ data: Object.keys(data).length ? data : null,
391
503
  };
392
504
  });
393
505
 
@@ -420,7 +532,15 @@ export function server(opts) {
420
532
  }
421
533
  wmts.id = id;
422
534
  wmts.name = (serving.styles[id] || serving.rendered[id]).name;
423
- wmts.baseUrl = `${req.get('X-Forwarded-Protocol') ? req.get('X-Forwarded-Protocol') : req.protocol}://${req.get('host')}`;
535
+ if (opts.publicUrl) {
536
+ wmts.baseUrl = opts.publicUrl;
537
+ } else {
538
+ wmts.baseUrl = `${
539
+ req.get('X-Forwarded-Protocol')
540
+ ? req.get('X-Forwarded-Protocol')
541
+ : req.protocol
542
+ }://${req.get('host')}/`;
543
+ }
424
544
  return wmts;
425
545
  });
426
546
 
@@ -448,13 +568,17 @@ export function server(opts) {
448
568
  }
449
569
  });
450
570
 
451
- const server = app.listen(process.env.PORT || opts.port, process.env.BIND || opts.bind, function() {
452
- let address = this.address().address;
453
- if (address.indexOf('::') === 0) {
454
- address = `[${address}]`; // literal IPv6 address
455
- }
456
- console.log(`Listening at http://${address}:${this.address().port}/`);
457
- });
571
+ const server = app.listen(
572
+ process.env.PORT || opts.port,
573
+ process.env.BIND || opts.bind,
574
+ function () {
575
+ let address = this.address().address;
576
+ if (address.indexOf('::') === 0) {
577
+ address = `[${address}]`; // literal IPv6 address
578
+ }
579
+ console.log(`Listening at http://${address}:${this.address().port}/`);
580
+ },
581
+ );
458
582
 
459
583
  // add server.shutdown() to gracefully stop serving
460
584
  enableShutdown(server);
@@ -462,11 +586,15 @@ export function server(opts) {
462
586
  return {
463
587
  app: app,
464
588
  server: server,
465
- startupPromise: startupPromise
589
+ startupPromise: startupPromise,
466
590
  };
467
591
  }
468
592
 
469
- export const exports = (opts) => {
593
+ /**
594
+ *
595
+ * @param opts
596
+ */
597
+ export function server(opts) {
470
598
  const running = start(opts);
471
599
 
472
600
  running.startupPromise.catch((err) => {
@@ -482,10 +610,6 @@ export const exports = (opts) => {
482
610
  console.log('Stopping server and reloading config');
483
611
 
484
612
  running.server.shutdown(() => {
485
- for (const key in require.cache) {
486
- delete require.cache[key];
487
- }
488
-
489
613
  const restarted = start(opts);
490
614
  running.server = restarted.server;
491
615
  running.app = restarted.app;
@@ -493,4 +617,4 @@ export const exports = (opts) => {
493
617
  });
494
618
 
495
619
  return running;
496
- };
620
+ }