tileserver-gl-light 5.0.0 → 5.1.0-pre.1

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.
@@ -13,7 +13,6 @@ import '@maplibre/maplibre-gl-native';
13
13
  // SECTION END
14
14
 
15
15
  import advancedPool from 'advanced-pool';
16
- import fs from 'node:fs';
17
16
  import path from 'path';
18
17
  import url from 'url';
19
18
  import util from 'util';
@@ -28,29 +27,45 @@ import polyline from '@mapbox/polyline';
28
27
  import proj4 from 'proj4';
29
28
  import axios from 'axios';
30
29
  import {
30
+ allowedScales,
31
+ allowedTileSizes,
31
32
  getFontsPbf,
32
33
  listFonts,
33
34
  getTileUrls,
34
35
  isValidHttpUrl,
35
36
  fixTileJSONCenter,
37
+ fetchTileData,
38
+ readFile,
36
39
  } from './utils.js';
37
- import {
38
- openPMtiles,
39
- getPMtilesInfo,
40
- getPMtilesTile,
41
- } from './pmtiles_adapter.js';
40
+ import { openPMtiles, getPMtilesInfo } from './pmtiles_adapter.js';
42
41
  import { renderOverlay, renderWatermark, renderAttribution } from './render.js';
43
42
  import fsp from 'node:fs/promises';
44
43
  import { existsP, gunzipP } from './promises.js';
45
44
  import { openMbTilesWrapper } from './mbtiles_wrapper.js';
46
45
 
47
- const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+.?\\d+)';
46
+ const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d*\\.\\d+)';
47
+
48
+ const staticTypeRegex = new RegExp(
49
+ `^` +
50
+ `(?:` +
51
+ // Format 1: {lon},{lat},{zoom}[@{bearing}[,{pitch}]]
52
+ `(?<lon>${FLOAT_PATTERN}),(?<lat>${FLOAT_PATTERN}),(?<zoom>${FLOAT_PATTERN})` +
53
+ `(?:@(?<bearing>${FLOAT_PATTERN})(?:,(?<pitch>${FLOAT_PATTERN}))?)?` +
54
+ `|` +
55
+ // Format 2: {minx},{miny},{maxx},{maxy}
56
+ `(?<minx>${FLOAT_PATTERN}),(?<miny>${FLOAT_PATTERN}),(?<maxx>${FLOAT_PATTERN}),(?<maxy>${FLOAT_PATTERN})` +
57
+ `|` +
58
+ // Format 3: auto
59
+ `(?<auto>auto)` +
60
+ `)` +
61
+ `$`,
62
+ );
63
+
48
64
  const PATH_PATTERN =
49
65
  /^((fill|stroke|width)\:[^\|]+\|)*(enc:.+|-?\d+(\.\d*)?,-?\d+(\.\d*)?(\|-?\d+(\.\d*)?,-?\d+(\.\d*)?)+)/;
50
66
  const httpTester = /^https?:\/\//i;
51
67
 
52
68
  const mercator = new SphericalMercator();
53
- const getScale = (scale) => (scale || '@1x').slice(1, 2) | 0;
54
69
 
55
70
  mlgl.on('message', (e) => {
56
71
  if (e.severity === 'WARNING' || e.severity === 'ERROR') {
@@ -81,6 +96,7 @@ const cachedEmptyResponses = {
81
96
  * @param {string} format The format (a sharp format or 'pbf').
82
97
  * @param {string} color The background color (or empty string for transparent).
83
98
  * @param {Function} callback The mlgl callback.
99
+ * @returns {void}
84
100
  */
85
101
  function createEmptyResponse(format, color, callback) {
86
102
  if (!format || format === 'pbf') {
@@ -103,33 +119,42 @@ function createEmptyResponse(format, color, callback) {
103
119
  }
104
120
 
105
121
  // create an "empty" response image
106
- color = new Color(color);
107
- const array = color.array();
108
- const channels = array.length === 4 && format !== 'jpeg' ? 4 : 3;
109
- sharp(Buffer.from(array), {
110
- raw: {
111
- width: 1,
112
- height: 1,
113
- channels,
114
- },
115
- })
116
- .toFormat(format)
117
- .toBuffer((err, buffer, info) => {
118
- if (!err) {
122
+ try {
123
+ color = new Color(color);
124
+ const array = color.array();
125
+ const channels = array.length === 4 && format !== 'jpeg' ? 4 : 3;
126
+ sharp(Buffer.from(array), {
127
+ raw: {
128
+ width: 1,
129
+ height: 1,
130
+ channels,
131
+ },
132
+ })
133
+ .toFormat(format)
134
+ .toBuffer((err, buffer, info) => {
135
+ if (err) {
136
+ console.error('Error creating image with Sharp:', err);
137
+ callback(err, null);
138
+ return;
139
+ }
119
140
  cachedEmptyResponses[cacheKey] = buffer;
120
- }
121
- callback(null, { data: buffer });
122
- });
141
+ callback(null, { data: buffer });
142
+ });
143
+ } catch (error) {
144
+ console.error('Error during image processing setup:', error);
145
+ callback(error, null);
146
+ }
123
147
  }
124
148
 
125
149
  /**
126
150
  * Parses coordinate pair provided to pair of floats and ensures the resulting
127
151
  * pair is a longitude/latitude combination depending on lnglat query parameter.
128
- * @param {List} coordinatePair Coordinate pair.
152
+ * @param {Array<string>} coordinatePair Coordinate pair.
129
153
  * @param coordinates
130
154
  * @param {object} query Request query parameters.
155
+ * @returns {Array<number>|null} Parsed coordinate pair as [longitude, latitude] or null if invalid
131
156
  */
132
- const parseCoordinatePair = (coordinates, query) => {
157
+ function parseCoordinatePair(coordinates, query) {
133
158
  const firstCoordinate = parseFloat(coordinates[0]);
134
159
  const secondCoordinate = parseFloat(coordinates[1]);
135
160
 
@@ -145,15 +170,16 @@ const parseCoordinatePair = (coordinates, query) => {
145
170
  }
146
171
 
147
172
  return [firstCoordinate, secondCoordinate];
148
- };
173
+ }
149
174
 
150
175
  /**
151
176
  * Parses a coordinate pair from query arguments and optionally transforms it.
152
- * @param {List} coordinatePair Coordinate pair.
177
+ * @param {Array<string>} coordinatePair Coordinate pair.
153
178
  * @param {object} query Request query parameters.
154
179
  * @param {Function} transformer Optional transform function.
180
+ * @returns {Array<number>|null} Transformed coordinate pair or null if invalid.
155
181
  */
156
- const parseCoordinates = (coordinatePair, query, transformer) => {
182
+ function parseCoordinates(coordinatePair, query, transformer) {
157
183
  const parsedCoordinates = parseCoordinatePair(coordinatePair, query);
158
184
 
159
185
  // Transform coordinates
@@ -162,14 +188,15 @@ const parseCoordinates = (coordinatePair, query, transformer) => {
162
188
  }
163
189
 
164
190
  return parsedCoordinates;
165
- };
191
+ }
166
192
 
167
193
  /**
168
194
  * Parses paths provided via query into a list of path objects.
169
195
  * @param {object} query Request query parameters.
170
196
  * @param {Function} transformer Optional transform function.
197
+ * @returns {Array<Array<Array<number>>>} Array of paths.
171
198
  */
172
- const extractPathsFromQuery = (query, transformer) => {
199
+ function extractPathsFromQuery(query, transformer) {
173
200
  // Initiate paths array
174
201
  const paths = [];
175
202
  // Return an empty list if no paths have been provided
@@ -221,17 +248,17 @@ const extractPathsFromQuery = (query, transformer) => {
221
248
  }
222
249
  }
223
250
  return paths;
224
- };
225
-
251
+ }
226
252
  /**
227
253
  * Parses marker options provided via query and sets corresponding attributes
228
254
  * on marker object.
229
255
  * Options adhere to the following format
230
256
  * [optionName]:[optionValue]
231
- * @param {List[String]} optionsList List of option strings.
257
+ * @param {Array<string>} optionsList List of option strings.
232
258
  * @param {object} marker Marker object to configure.
259
+ * @returns {void}
233
260
  */
234
- const parseMarkerOptions = (optionsList, marker) => {
261
+ function parseMarkerOptions(optionsList, marker) {
235
262
  for (const options of optionsList) {
236
263
  const optionParts = options.split(':');
237
264
  // Ensure we got an option name and value
@@ -258,15 +285,16 @@ const parseMarkerOptions = (optionsList, marker) => {
258
285
  break;
259
286
  }
260
287
  }
261
- };
288
+ }
262
289
 
263
290
  /**
264
291
  * Parses markers provided via query into a list of marker objects.
265
292
  * @param {object} query Request query parameters.
266
293
  * @param {object} options Configuration options.
267
294
  * @param {Function} transformer Optional transform function.
295
+ * @returns {Array<object>} An array of marker objects.
268
296
  */
269
- const extractMarkersFromQuery = (query, options, transformer) => {
297
+ function extractMarkersFromQuery(query, options, transformer) {
270
298
  // Return an empty list if no markers have been provided
271
299
  if (!query.marker) {
272
300
  return [];
@@ -342,9 +370,16 @@ const extractMarkersFromQuery = (query, options, transformer) => {
342
370
  markers.push(marker);
343
371
  }
344
372
  return markers;
345
- };
346
-
347
- const calcZForBBox = (bbox, w, h, query) => {
373
+ }
374
+ /**
375
+ * Calculates the zoom level for a given bounding box.
376
+ * @param {Array<number>} bbox Bounding box as [minx, miny, maxx, maxy].
377
+ * @param {number} w Width of the image.
378
+ * @param {number} h Height of the image.
379
+ * @param {object} query Request query parameters.
380
+ * @returns {number} Calculated zoom level.
381
+ */
382
+ function calcZForBBox(bbox, w, h, query) {
348
383
  let z = 25;
349
384
 
350
385
  const padding = query.padding !== undefined ? parseFloat(query.padding) : 0.1;
@@ -363,9 +398,27 @@ const calcZForBBox = (bbox, w, h, query) => {
363
398
  z = Math.max(Math.log(Math.max(w, h) / 256) / Math.LN2, Math.min(25, z));
364
399
 
365
400
  return z;
366
- };
401
+ }
367
402
 
368
- const respondImage = (
403
+ /**
404
+ * Responds with an image.
405
+ * @param {object} options Configuration options.
406
+ * @param {object} item Item object containing map and other information.
407
+ * @param {number} z Zoom level.
408
+ * @param {number} lon Longitude of the center.
409
+ * @param {number} lat Latitude of the center.
410
+ * @param {number} bearing Map bearing.
411
+ * @param {number} pitch Map pitch.
412
+ * @param {number} width Width of the image.
413
+ * @param {number} height Height of the image.
414
+ * @param {number} scale Scale factor.
415
+ * @param {string} format Image format.
416
+ * @param {object} res Express response object.
417
+ * @param {Buffer|null} overlay Optional overlay image.
418
+ * @param {string} mode Rendering mode ('tile' or 'static').
419
+ * @returns {Promise<void>}
420
+ */
421
+ async function respondImage(
369
422
  options,
370
423
  item,
371
424
  z,
@@ -380,7 +433,7 @@ const respondImage = (
380
433
  res,
381
434
  overlay = null,
382
435
  mode = 'tile',
383
- ) => {
436
+ ) {
384
437
  if (
385
438
  Math.abs(lon) > 180 ||
386
439
  Math.abs(lat) > 85.06 ||
@@ -413,7 +466,8 @@ const respondImage = (
413
466
  } else {
414
467
  pool = item.map.renderersStatic[scale];
415
468
  }
416
- pool.acquire((err, renderer) => {
469
+
470
+ pool.acquire(async (err, renderer) => {
417
471
  // For 512px tiles, use the actual maplibre-native zoom. For 256px tiles, use zoom - 1
418
472
  let mlglZ;
419
473
  if (width === 512) {
@@ -472,8 +526,8 @@ const respondImage = (
472
526
  height: height * scale,
473
527
  });
474
528
  }
475
-
476
529
  // HACK(Part 2) 256px tiles are a zoom level lower than maplibre-native default tiles. this hack allows tileserver-gl to support zoom 0 256px tiles, which would actually be zoom -1 in maplibre-native. Since zoom -1 isn't supported, a double sized zoom 0 tile is requested and resized here.
530
+
477
531
  if (z === 0 && width === 256) {
478
532
  image.resize(width * scale, height * scale);
479
533
  }
@@ -527,7 +581,10 @@ const respondImage = (
527
581
  dither: formatOptions.dither,
528
582
  });
529
583
  } else if (format === 'jpeg') {
530
- image.jpeg({ quality: formatOptions.quality || formatQuality || 80 });
584
+ image.jpeg({
585
+ quality: formatOptions.quality || formatQuality || 80,
586
+ progressive: formatOptions.progressive,
587
+ });
531
588
  } else if (format === 'webp') {
532
589
  image.webp({ quality: formatOptions.quality || formatQuality || 90 });
533
590
  }
@@ -544,320 +601,410 @@ const respondImage = (
544
601
  });
545
602
  });
546
603
  });
547
- };
604
+ }
548
605
 
549
- const existingFonts = {};
550
- let maxScaleFactor = 2;
606
+ /**
607
+ * Handles requests for tile images.
608
+ * @param {object} options - Configuration options for the server.
609
+ * @param {object} repo - The repository object holding style data.
610
+ * @param {object} req - Express request object.
611
+ * @param {string} req.params.id - The id of the style.
612
+ * @param {string} req.params.p1 - The tile size parameter, if available.
613
+ * @param {string} req.params.p2 - The z parameter.
614
+ * @param {string} req.params.p3 - The x parameter.
615
+ * @param {string} req.params.p4 - The y parameter.
616
+ * @param {string} req.params.scale - The scale parameter.
617
+ * @param {string} req.params.format - The format of the image.
618
+ * @param {object} res - Express response object.
619
+ * @param {Function} next - Express next middleware function.
620
+ * @param {number} maxScaleFactor - The maximum scale factor allowed.
621
+ * @param defailtTileSize
622
+ * @returns {Promise<void>}
623
+ */
624
+ async function handleTileRequest(
625
+ options,
626
+ repo,
627
+ req,
628
+ res,
629
+ next,
630
+ maxScaleFactor,
631
+ defailtTileSize,
632
+ ) {
633
+ const {
634
+ id,
635
+ p1: tileSize,
636
+ p2: zParam,
637
+ p3: xParam,
638
+ p4: yParam,
639
+ scale: scaleParam,
640
+ format,
641
+ } = req.params;
642
+ const item = repo[id];
643
+ if (!item) {
644
+ return res.sendStatus(404);
645
+ }
551
646
 
552
- export const serve_rendered = {
553
- init: async (options, repo) => {
554
- maxScaleFactor = Math.min(Math.floor(options.maxScaleFactor || 3), 9);
555
- let scalePattern = '';
556
- for (let i = 2; i <= maxScaleFactor; i++) {
557
- scalePattern += i.toFixed();
647
+ const modifiedSince = req.get('if-modified-since');
648
+ const cc = req.get('cache-control');
649
+ if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) {
650
+ if (
651
+ new Date(item.lastModified).getTime() ===
652
+ new Date(modifiedSince).getTime()
653
+ ) {
654
+ return res.sendStatus(304);
558
655
  }
559
- scalePattern = `@[${scalePattern}]x`;
656
+ }
657
+ const z = parseFloat(zParam) | 0;
658
+ const x = parseFloat(xParam) | 0;
659
+ const y = parseFloat(yParam) | 0;
660
+ const scale = allowedScales(scaleParam, maxScaleFactor);
560
661
 
561
- const app = express().disable('x-powered-by');
662
+ let parsedTileSize = parseInt(defailtTileSize, 10);
663
+ if (tileSize) {
664
+ parsedTileSize = parseInt(allowedTileSizes(tileSize), 10);
562
665
 
563
- app.get(
564
- `/:id/(:tileSize(256|512)/)?:z(\\d+)/:x(\\d+)/:y(\\d+):scale(${scalePattern})?.:format([\\w]+)`,
565
- (req, res, next) => {
566
- const item = repo[req.params.id];
567
- if (!item) {
568
- return res.sendStatus(404);
569
- }
570
-
571
- const modifiedSince = req.get('if-modified-since');
572
- const cc = req.get('cache-control');
573
- if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) {
574
- if (new Date(item.lastModified) <= new Date(modifiedSince)) {
575
- return res.sendStatus(304);
576
- }
577
- }
666
+ if (parsedTileSize == null) {
667
+ return res.status(400).send('Invalid Tile Size');
668
+ }
669
+ }
578
670
 
579
- const z = req.params.z | 0;
580
- const x = req.params.x | 0;
581
- const y = req.params.y | 0;
582
- const scale = getScale(req.params.scale);
583
- const format = req.params.format;
584
- const tileSize = parseInt(req.params.tileSize, 10) || 256;
585
-
586
- if (
587
- z < 0 ||
588
- x < 0 ||
589
- y < 0 ||
590
- z > 22 ||
591
- x >= Math.pow(2, z) ||
592
- y >= Math.pow(2, z)
593
- ) {
594
- return res.status(404).send('Out of bounds');
595
- }
671
+ if (
672
+ scale == null ||
673
+ z < 0 ||
674
+ x < 0 ||
675
+ y < 0 ||
676
+ z > 22 ||
677
+ x >= Math.pow(2, z) ||
678
+ y >= Math.pow(2, z)
679
+ ) {
680
+ return res.status(400).send('Out of bounds');
681
+ }
596
682
 
597
- const tileCenter = mercator.ll(
598
- [
599
- ((x + 0.5) / (1 << z)) * (256 << z),
600
- ((y + 0.5) / (1 << z)) * (256 << z),
601
- ],
602
- z,
603
- );
683
+ const tileCenter = mercator.ll(
684
+ [((x + 0.5) / (1 << z)) * (256 << z), ((y + 0.5) / (1 << z)) * (256 << z)],
685
+ z,
686
+ );
604
687
 
605
- // prettier-ignore
606
- return respondImage(
607
- options, item, z, tileCenter[0], tileCenter[1], 0, 0, tileSize, tileSize, scale, format, res,
608
- );
609
- },
610
- );
688
+ // prettier-ignore
689
+ return await respondImage(
690
+ options, item, z, tileCenter[0], tileCenter[1], 0, 0, parsedTileSize, parsedTileSize, scale, format, res,
691
+ );
692
+ }
611
693
 
612
- if (options.serveStaticMaps !== false) {
613
- const staticPattern = `/:id/static/:raw(raw)?/%s/:width(\\d+)x:height(\\d+):scale(${scalePattern})?.:format([\\w]+)`;
694
+ /**
695
+ * Handles requests for static map images.
696
+ * @param {object} options - Configuration options for the server.
697
+ * @param {object} repo - The repository object holding style data.
698
+ * @param {object} req - Express request object.
699
+ * @param {object} res - Express response object.
700
+ * @param {string} req.params.p2 - The raw or static parameter.
701
+ * @param {string} req.params.p3 - The staticType parameter.
702
+ * @param {string} req.params.p4 - The width parameter.
703
+ * @param {string} req.params.p5 - The height parameter.
704
+ * @param {string} req.params.scale - The scale parameter.
705
+ * @param {string} req.params.format - The format of the image.
706
+ * @param {Function} next - Express next middleware function.
707
+ * @param {number} maxScaleFactor - The maximum scale factor allowed.
708
+ * @param verbose
709
+ * @returns {Promise<void>}
710
+ */
711
+ async function handleStaticRequest(
712
+ options,
713
+ repo,
714
+ req,
715
+ res,
716
+ next,
717
+ maxScaleFactor,
718
+ ) {
719
+ const {
720
+ id,
721
+ p2: raw,
722
+ p3: staticType,
723
+ p4: widthAndHeight,
724
+ scale: scaleParam,
725
+ format,
726
+ } = req.params;
727
+ const item = repo[id];
728
+
729
+ let parsedWidth = null;
730
+ let parsedHeight = null;
731
+ if (widthAndHeight) {
732
+ const sizeMatch = widthAndHeight.match(/^(\d+)x(\d+)$/);
733
+ if (sizeMatch) {
734
+ const width = parseInt(sizeMatch[1], 10);
735
+ const height = parseInt(sizeMatch[2], 10);
736
+ if (
737
+ isNaN(width) ||
738
+ isNaN(height) ||
739
+ width !== parseFloat(sizeMatch[1]) ||
740
+ height !== parseFloat(sizeMatch[2])
741
+ ) {
742
+ return res
743
+ .status(400)
744
+ .send('Invalid width or height provided in size parameter');
745
+ }
746
+ parsedWidth = width;
747
+ parsedHeight = height;
748
+ } else {
749
+ return res
750
+ .status(400)
751
+ .send('Invalid width or height provided in size parameter');
752
+ }
753
+ } else {
754
+ return res
755
+ .status(400)
756
+ .send('Invalid width or height provided in size parameter');
757
+ }
614
758
 
615
- const centerPattern = util.format(
616
- ':x(%s),:y(%s),:z(%s)(@:bearing(%s)(,:pitch(%s))?)?',
617
- FLOAT_PATTERN,
618
- FLOAT_PATTERN,
619
- FLOAT_PATTERN,
620
- FLOAT_PATTERN,
621
- FLOAT_PATTERN,
622
- );
759
+ const scale = allowedScales(scaleParam, maxScaleFactor);
760
+ let isRaw = raw === 'raw';
623
761
 
624
- app.get(
625
- util.format(staticPattern, centerPattern),
626
- async (req, res, next) => {
627
- try {
628
- const item = repo[req.params.id];
629
- if (!item) {
630
- return res.sendStatus(404);
631
- }
632
- const raw = req.params.raw;
633
- const z = +req.params.z;
634
- let x = +req.params.x;
635
- let y = +req.params.y;
636
- const bearing = +(req.params.bearing || '0');
637
- const pitch = +(req.params.pitch || '0');
638
- const w = req.params.width | 0;
639
- const h = req.params.height | 0;
640
- const scale = getScale(req.params.scale);
641
- const format = req.params.format;
642
-
643
- if (z < 0) {
644
- return res.status(404).send('Invalid zoom');
645
- }
762
+ const staticTypeMatch = staticType.match(staticTypeRegex);
763
+ if (!item || !format || !scale || !staticTypeMatch?.groups) {
764
+ return res.sendStatus(404);
765
+ }
646
766
 
647
- const transformer = raw
648
- ? mercator.inverse.bind(mercator)
649
- : item.dataProjWGStoInternalWGS;
767
+ if (staticTypeMatch.groups.lon) {
768
+ // Center Based Static Image
769
+ const z = parseFloat(staticTypeMatch.groups.zoom) || 0;
770
+ let x = parseFloat(staticTypeMatch.groups.lon) || 0;
771
+ let y = parseFloat(staticTypeMatch.groups.lat) || 0;
772
+ const bearing = parseFloat(staticTypeMatch.groups.bearing) || 0;
773
+ const pitch = parseInt(staticTypeMatch.groups.pitch) || 0;
774
+ if (z < 0) {
775
+ return res.status(404).send('Invalid zoom');
776
+ }
650
777
 
651
- if (transformer) {
652
- const ll = transformer([x, y]);
653
- x = ll[0];
654
- y = ll[1];
655
- }
778
+ const transformer = isRaw
779
+ ? mercator.inverse.bind(mercator)
780
+ : item.dataProjWGStoInternalWGS;
656
781
 
657
- const paths = extractPathsFromQuery(req.query, transformer);
658
- const markers = extractMarkersFromQuery(
659
- req.query,
660
- options,
661
- transformer,
662
- );
782
+ if (transformer) {
783
+ const ll = transformer([x, y]);
784
+ x = ll[0];
785
+ y = ll[1];
786
+ }
663
787
 
664
- // prettier-ignore
665
- const overlay = await renderOverlay(
666
- z, x, y, bearing, pitch, w, h, scale, paths, markers, req.query,
667
- );
788
+ const paths = extractPathsFromQuery(req.query, transformer);
789
+ const markers = extractMarkersFromQuery(req.query, options, transformer);
790
+ // prettier-ignore
791
+ const overlay = await renderOverlay(
792
+ z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, paths, markers, req.query,
793
+ );
794
+
795
+ // prettier-ignore
796
+ return await respondImage(
797
+ options, item, z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, format, res, overlay, 'static',
798
+ );
799
+ } else if (staticTypeMatch.groups.minx) {
800
+ // Area Based Static Image
801
+ const minx = parseFloat(staticTypeMatch.groups.minx) || 0;
802
+ const miny = parseFloat(staticTypeMatch.groups.miny) || 0;
803
+ const maxx = parseFloat(staticTypeMatch.groups.maxx) || 0;
804
+ const maxy = parseFloat(staticTypeMatch.groups.maxy) || 0;
805
+ const bbox = [minx, miny, maxx, maxy];
806
+ let center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2];
807
+
808
+ const transformer = isRaw
809
+ ? mercator.inverse.bind(mercator)
810
+ : item.dataProjWGStoInternalWGS;
811
+
812
+ if (transformer) {
813
+ const minCorner = transformer(bbox.slice(0, 2));
814
+ const maxCorner = transformer(bbox.slice(2));
815
+ bbox[0] = minCorner[0];
816
+ bbox[1] = minCorner[1];
817
+ bbox[2] = maxCorner[0];
818
+ bbox[3] = maxCorner[1];
819
+ center = transformer(center);
820
+ }
668
821
 
669
- // prettier-ignore
670
- return respondImage(
671
- options, item, z, x, y, bearing, pitch, w, h, scale, format, res, overlay, 'static',
672
- );
673
- } catch (e) {
674
- next(e);
675
- }
676
- },
822
+ const z = calcZForBBox(bbox, parsedWidth, parsedHeight, req.query);
823
+ const x = center[0];
824
+ const y = center[1];
825
+ const bearing = 0;
826
+ const pitch = 0;
827
+
828
+ const paths = extractPathsFromQuery(req.query, transformer);
829
+ const markers = extractMarkersFromQuery(req.query, options, transformer);
830
+ // prettier-ignore
831
+ const overlay = await renderOverlay(
832
+ z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, paths, markers, req.query,
677
833
  );
678
834
 
679
- const serveBounds = async (req, res, next) => {
680
- try {
681
- const item = repo[req.params.id];
682
- if (!item) {
683
- return res.sendStatus(404);
684
- }
685
- const raw = req.params.raw;
686
- const bbox = [
687
- +req.params.minx,
688
- +req.params.miny,
689
- +req.params.maxx,
690
- +req.params.maxy,
691
- ];
692
- let center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2];
693
-
694
- const transformer = raw
695
- ? mercator.inverse.bind(mercator)
696
- : item.dataProjWGStoInternalWGS;
697
-
698
- if (transformer) {
699
- const minCorner = transformer(bbox.slice(0, 2));
700
- const maxCorner = transformer(bbox.slice(2));
701
- bbox[0] = minCorner[0];
702
- bbox[1] = minCorner[1];
703
- bbox[2] = maxCorner[0];
704
- bbox[3] = maxCorner[1];
705
- center = transformer(center);
706
- }
707
-
708
- const w = req.params.width | 0;
709
- const h = req.params.height | 0;
710
- const scale = getScale(req.params.scale);
711
- const format = req.params.format;
835
+ // prettier-ignore
836
+ return await respondImage(
837
+ options, item, z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, format, res, overlay, 'static',
838
+ );
839
+ } else if (staticTypeMatch.groups.auto) {
840
+ // Area Static Image
841
+ const bearing = 0;
842
+ const pitch = 0;
843
+
844
+ const transformer = isRaw
845
+ ? mercator.inverse.bind(mercator)
846
+ : item.dataProjWGStoInternalWGS;
847
+
848
+ const paths = extractPathsFromQuery(req.query, transformer);
849
+ const markers = extractMarkersFromQuery(req.query, options, transformer);
850
+
851
+ // Extract coordinates from markers
852
+ const markerCoordinates = [];
853
+ for (const marker of markers) {
854
+ markerCoordinates.push(marker.location);
855
+ }
712
856
 
713
- const z = calcZForBBox(bbox, w, h, req.query);
714
- const x = center[0];
715
- const y = center[1];
716
- const bearing = 0;
717
- const pitch = 0;
857
+ // Create array with coordinates from markers and path
858
+ const coords = [].concat(paths.flat()).concat(markerCoordinates);
718
859
 
719
- const paths = extractPathsFromQuery(req.query, transformer);
720
- const markers = extractMarkersFromQuery(
721
- req.query,
722
- options,
723
- transformer,
724
- );
725
-
726
- // prettier-ignore
727
- const overlay = await renderOverlay(
728
- z, x, y, bearing, pitch, w, h, scale, paths, markers, req.query,
729
- );
860
+ // Check if we have at least one coordinate to calculate a bounding box
861
+ if (coords.length < 1) {
862
+ return res.status(400).send('No coordinates provided');
863
+ }
730
864
 
731
- // prettier-ignore
732
- return respondImage(
733
- options, item, z, x, y, bearing, pitch, w, h, scale, format, res, overlay, 'static',
734
- );
735
- } catch (e) {
736
- next(e);
737
- }
738
- };
865
+ const bbox = [Infinity, Infinity, -Infinity, -Infinity];
866
+ for (const pair of coords) {
867
+ bbox[0] = Math.min(bbox[0], pair[0]);
868
+ bbox[1] = Math.min(bbox[1], pair[1]);
869
+ bbox[2] = Math.max(bbox[2], pair[0]);
870
+ bbox[3] = Math.max(bbox[3], pair[1]);
871
+ }
739
872
 
740
- const boundsPattern = util.format(
741
- ':minx(%s),:miny(%s),:maxx(%s),:maxy(%s)',
742
- FLOAT_PATTERN,
743
- FLOAT_PATTERN,
744
- FLOAT_PATTERN,
745
- FLOAT_PATTERN,
746
- );
873
+ const bbox_ = mercator.convert(bbox, '900913');
874
+ const center = mercator.inverse([
875
+ (bbox_[0] + bbox_[2]) / 2,
876
+ (bbox_[1] + bbox_[3]) / 2,
877
+ ]);
878
+
879
+ // Calculate zoom level
880
+ const maxZoom = parseFloat(req.query.maxzoom);
881
+ let z = calcZForBBox(bbox, parsedWidth, parsedHeight, req.query);
882
+ if (maxZoom > 0) {
883
+ z = Math.min(z, maxZoom);
884
+ }
747
885
 
748
- app.get(util.format(staticPattern, boundsPattern), serveBounds);
886
+ const x = center[0];
887
+ const y = center[1];
749
888
 
750
- app.get('/:id/static/', (req, res, next) => {
751
- for (const key in req.query) {
752
- req.query[key.toLowerCase()] = req.query[key];
753
- }
754
- req.params.raw = true;
755
- req.params.format = (req.query.format || 'image/png').split('/').pop();
756
- const bbox = (req.query.bbox || '').split(',');
757
- req.params.minx = bbox[0];
758
- req.params.miny = bbox[1];
759
- req.params.maxx = bbox[2];
760
- req.params.maxy = bbox[3];
761
- req.params.width = req.query.width || '256';
762
- req.params.height = req.query.height || '256';
763
- if (req.query.scale) {
764
- req.params.width /= req.query.scale;
765
- req.params.height /= req.query.scale;
766
- req.params.scale = `@${req.query.scale}`;
767
- }
889
+ // prettier-ignore
890
+ const overlay = await renderOverlay(
891
+ z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, paths, markers, req.query,
892
+ );
768
893
 
769
- return serveBounds(req, res, next);
770
- });
894
+ // prettier-ignore
895
+ return await respondImage(
896
+ options, item, z, x, y, bearing, pitch, parsedWidth, parsedHeight, scale, format, res, overlay, 'static',
897
+ );
898
+ } else {
899
+ return res.sendStatus(404);
900
+ }
901
+ }
902
+ const existingFonts = {};
903
+ let maxScaleFactor = 2;
771
904
 
772
- const autoPattern = 'auto';
905
+ export const serve_rendered = {
906
+ /**
907
+ * Initializes the serve_rendered module.
908
+ * @param {object} options Configuration options.
909
+ * @param {object} repo Repository object.
910
+ * @param {object} programOpts - An object containing the program options.
911
+ * @returns {Promise<express.Application>} A promise that resolves to the Express app.
912
+ */
913
+ init: async function (options, repo, programOpts) {
914
+ const { verbose, tileSize: defailtTileSize = 256 } = programOpts;
915
+ maxScaleFactor = Math.min(Math.floor(options.maxScaleFactor || 3), 9);
916
+ const app = express().disable('x-powered-by');
773
917
 
774
- app.get(
775
- util.format(staticPattern, autoPattern),
776
- async (req, res, next) => {
777
- try {
778
- const item = repo[req.params.id];
779
- if (!item) {
780
- return res.sendStatus(404);
781
- }
782
- const raw = req.params.raw;
783
- const w = req.params.width | 0;
784
- const h = req.params.height | 0;
785
- const bearing = 0;
786
- const pitch = 0;
787
- const scale = getScale(req.params.scale);
788
- const format = req.params.format;
789
-
790
- const transformer = raw
791
- ? mercator.inverse.bind(mercator)
792
- : item.dataProjWGStoInternalWGS;
793
-
794
- const paths = extractPathsFromQuery(req.query, transformer);
795
- const markers = extractMarkersFromQuery(
796
- req.query,
797
- options,
798
- transformer,
918
+ /**
919
+ * Handles requests for tile images.
920
+ * @param {object} req - Express request object.
921
+ * @param {object} res - Express response object.
922
+ * @param {string} req.params.id - The id of the style.
923
+ * @param {string} [req.params.p1] - The tile size or static parameter, if available.
924
+ * @param {string} req.params.p2 - The z, static, or raw parameter.
925
+ * @param {string} req.params.p3 - The x or staticType parameter.
926
+ * @param {string} req.params.p4 - The y or width parameter.
927
+ * @param {string} req.params.scale - The scale parameter.
928
+ * @param {string} req.params.format - The format of the image.
929
+ * @returns {Promise<void>}
930
+ */
931
+ app.get(
932
+ `/:id{/:p1}/:p2/:p3/:p4{@:scale}{.:format}`,
933
+ async (req, res, next) => {
934
+ try {
935
+ const { p1, p2, id, p3, p4, scale, format } = req.params;
936
+ const requestType =
937
+ (!p1 && p2 === 'static') || (p1 === 'static' && p2 === 'raw')
938
+ ? 'static'
939
+ : 'tile';
940
+ if (verbose) {
941
+ console.log(
942
+ `Handling rendered %s request for: /styles/%s%s/%s/%s/%s%s.%s`,
943
+ requestType,
944
+ String(id).replace(/\n|\r/g, ''),
945
+ p1 ? '/' + String(p1).replace(/\n|\r/g, '') : '',
946
+ String(p2).replace(/\n|\r/g, ''),
947
+ String(p3).replace(/\n|\r/g, ''),
948
+ String(p4).replace(/\n|\r/g, ''),
949
+ scale ? '@' + String(scale).replace(/\n|\r/g, '') : '',
950
+ String(format).replace(/\n|\r/g, ''),
799
951
  );
952
+ }
800
953
 
801
- // Extract coordinates from markers
802
- const markerCoordinates = [];
803
- for (const marker of markers) {
804
- markerCoordinates.push(marker.location);
805
- }
806
-
807
- // Create array with coordinates from markers and path
808
- const coords = [].concat(paths.flat()).concat(markerCoordinates);
809
-
810
- // Check if we have at least one coordinate to calculate a bounding box
811
- if (coords.length < 1) {
812
- return res.status(400).send('No coordinates provided');
813
- }
814
-
815
- const bbox = [Infinity, Infinity, -Infinity, -Infinity];
816
- for (const pair of coords) {
817
- bbox[0] = Math.min(bbox[0], pair[0]);
818
- bbox[1] = Math.min(bbox[1], pair[1]);
819
- bbox[2] = Math.max(bbox[2], pair[0]);
820
- bbox[3] = Math.max(bbox[3], pair[1]);
821
- }
822
-
823
- const bbox_ = mercator.convert(bbox, '900913');
824
- const center = mercator.inverse([
825
- (bbox_[0] + bbox_[2]) / 2,
826
- (bbox_[1] + bbox_[3]) / 2,
827
- ]);
828
-
829
- // Calculate zoom level
830
- const maxZoom = parseFloat(req.query.maxzoom);
831
- let z = calcZForBBox(bbox, w, h, req.query);
832
- if (maxZoom > 0) {
833
- z = Math.min(z, maxZoom);
954
+ if (requestType === 'static') {
955
+ // Route to static if p2 is static
956
+ if (options.serveStaticMaps !== false) {
957
+ return handleStaticRequest(
958
+ options,
959
+ repo,
960
+ req,
961
+ res,
962
+ next,
963
+ maxScaleFactor,
964
+ );
834
965
  }
835
-
836
- const x = center[0];
837
- const y = center[1];
838
-
839
- // prettier-ignore
840
- const overlay = await renderOverlay(
841
- z, x, y, bearing, pitch, w, h, scale, paths, markers, req.query,
842
- );
843
-
844
- // prettier-ignore
845
- return respondImage(
846
- options, item, z, x, y, bearing, pitch, w, h, scale, format, res, overlay, 'static',
847
- );
848
- } catch (e) {
849
- next(e);
966
+ return res.sendStatus(404);
850
967
  }
851
- },
852
- );
853
- }
854
968
 
855
- app.get('/(:tileSize(256|512)/)?:id.json', (req, res, next) => {
969
+ return handleTileRequest(
970
+ options,
971
+ repo,
972
+ req,
973
+ res,
974
+ next,
975
+ maxScaleFactor,
976
+ defailtTileSize,
977
+ );
978
+ } catch (e) {
979
+ console.log(e);
980
+ return next(e);
981
+ }
982
+ },
983
+ );
984
+
985
+ /**
986
+ * Handles requests for rendered tilejson endpoint.
987
+ * @param {object} req - Express request object.
988
+ * @param {object} res - Express response object.
989
+ * @param {string} req.params.id - The id of the tilejson
990
+ * @param {string} [req.params.tileSize] - The size of the tile, if specified.
991
+ * @returns {void}
992
+ */
993
+ app.get('{/:tileSize}/:id.json', (req, res, next) => {
856
994
  const item = repo[req.params.id];
857
995
  if (!item) {
858
996
  return res.sendStatus(404);
859
997
  }
860
998
  const tileSize = parseInt(req.params.tileSize, 10) || undefined;
999
+ if (verbose) {
1000
+ console.log(
1001
+ `Handling rendered tilejson request for: /styles/%s%s.json`,
1002
+ req.params.tileSize
1003
+ ? String(req.params.tileSize).replace(/\n|\r/g, '') + '/'
1004
+ : '',
1005
+ String(req.params.id).replace(/\n|\r/g, ''),
1006
+ );
1007
+ }
861
1008
  const info = clone(item.tileJSON);
862
1009
  info.tiles = getTileUrls(
863
1010
  req,
@@ -874,7 +1021,17 @@ export const serve_rendered = {
874
1021
  Object.assign(existingFonts, fonts);
875
1022
  return app;
876
1023
  },
877
- add: async (options, repo, params, id, publicUrl, dataResolver) => {
1024
+ /**
1025
+ * Adds a new item to the repository.
1026
+ * @param {object} options Configuration options.
1027
+ * @param {object} repo Repository object.
1028
+ * @param {object} params Parameters object.
1029
+ * @param {string} id ID of the item.
1030
+ * @param {object} programOpts - An object containing the program options
1031
+ * @param {Function} dataResolver Function to resolve data.
1032
+ * @returns {Promise<void>}
1033
+ */
1034
+ add: async function (options, repo, params, id, programOpts, dataResolver) {
878
1035
  const map = {
879
1036
  renderers: [],
880
1037
  renderersStatic: [],
@@ -882,23 +1039,45 @@ export const serve_rendered = {
882
1039
  sourceTypes: {},
883
1040
  };
884
1041
 
1042
+ const { publicUrl, verbose } = programOpts;
1043
+
885
1044
  let styleJSON;
1045
+ /**
1046
+ * Creates a pool of renderers.
1047
+ * @param {number} ratio Pixel ratio
1048
+ * @param {string} mode Rendering mode ('tile' or 'static').
1049
+ * @param {number} min Minimum pool size.
1050
+ * @param {number} max Maximum pool size.
1051
+ * @returns {object} The created pool
1052
+ */
886
1053
  const createPool = (ratio, mode, min, max) => {
1054
+ /**
1055
+ * Creates a renderer
1056
+ * @param {number} ratio Pixel ratio
1057
+ * @param {Function} createCallback Function that returns the renderer when created
1058
+ * @returns {void}
1059
+ */
887
1060
  const createRenderer = (ratio, createCallback) => {
888
1061
  const renderer = new mlgl.Map({
889
1062
  mode,
890
1063
  ratio,
891
1064
  request: async (req, callback) => {
892
1065
  const protocol = req.url.split(':')[0];
893
- // console.log('Handling request:', req);
1066
+ if (verbose) {
1067
+ console.log('Handling request:', req);
1068
+ }
894
1069
  if (protocol === 'sprites') {
895
1070
  const dir = options.paths[protocol];
896
1071
  const file = decodeURIComponent(req.url).substring(
897
1072
  protocol.length + 3,
898
1073
  );
899
- fs.readFile(path.join(dir, file), (err, data) => {
900
- callback(err, { data: data });
901
- });
1074
+ readFile(path.join(dir, file))
1075
+ .then((data) => {
1076
+ callback(null, { data: data });
1077
+ })
1078
+ .catch((err) => {
1079
+ callback(err, null);
1080
+ });
902
1081
  } else if (protocol === 'fonts') {
903
1082
  const parts = req.url.split('/');
904
1083
  const fontstack = decodeURIComponent(parts[2]);
@@ -928,88 +1107,57 @@ export const serve_rendered = {
928
1107
  const y = parts[5].split('.')[0] | 0;
929
1108
  const format = parts[5].split('.')[1];
930
1109
 
931
- if (sourceType === 'pmtiles') {
932
- let tileinfo = await getPMtilesTile(source, z, x, y);
933
- let data = tileinfo.data;
934
- let headers = tileinfo.header;
935
- if (data == undefined) {
936
- if (options.verbose)
937
- console.log('MBTiles error, serving empty', err);
938
- createEmptyResponse(
939
- sourceInfo.format,
940
- sourceInfo.color,
941
- callback,
1110
+ const fetchTile = await fetchTileData(
1111
+ source,
1112
+ sourceType,
1113
+ z,
1114
+ x,
1115
+ y,
1116
+ );
1117
+ if (fetchTile == null) {
1118
+ if (verbose) {
1119
+ console.log(
1120
+ 'fetchTile error on %s, serving empty response',
1121
+ req.url,
942
1122
  );
943
- return;
944
- } else {
945
- const response = {};
946
- response.data = data;
947
- if (headers['Last-Modified']) {
948
- response.modified = new Date(headers['Last-Modified']);
949
- }
950
-
951
- if (format === 'pbf') {
952
- if (options.dataDecoratorFunc) {
953
- response.data = options.dataDecoratorFunc(
954
- sourceId,
955
- 'data',
956
- response.data,
957
- z,
958
- x,
959
- y,
960
- );
961
- }
962
- }
963
-
964
- callback(null, response);
965
1123
  }
966
- } else if (sourceType === 'mbtiles') {
967
- source.getTile(z, x, y, async (err, data, headers) => {
968
- if (err) {
969
- if (options.verbose)
970
- console.log('MBTiles error, serving empty', err);
971
- createEmptyResponse(
972
- sourceInfo.format,
973
- sourceInfo.color,
974
- callback,
975
- );
976
- return;
977
- }
978
-
979
- const response = {};
980
- if (headers['Last-Modified']) {
981
- response.modified = new Date(headers['Last-Modified']);
982
- }
983
-
984
- if (format === 'pbf') {
985
- try {
986
- response.data = await gunzipP(data);
987
- } catch (err) {
988
- console.log(
989
- 'Skipping incorrect header for tile mbtiles://%s/%s/%s/%s.pbf',
990
- id,
991
- z,
992
- x,
993
- y,
994
- );
995
- }
996
- if (options.dataDecoratorFunc) {
997
- response.data = options.dataDecoratorFunc(
998
- sourceId,
999
- 'data',
1000
- response.data,
1001
- z,
1002
- x,
1003
- y,
1004
- );
1005
- }
1006
- } else {
1007
- response.data = data;
1008
- }
1009
-
1010
- callback(null, response);
1011
- });
1124
+ createEmptyResponse(
1125
+ sourceInfo.format,
1126
+ sourceInfo.color,
1127
+ callback,
1128
+ );
1129
+ return;
1012
1130
  }
1131
+
1132
+ const response = {};
1133
+ response.data = fetchTile.data;
1134
+ let headers = fetchTile.headers;
1135
+
1136
+ if (headers['Last-Modified']) {
1137
+ response.modified = new Date(headers['Last-Modified']);
1138
+ }
1139
+
1140
+ if (format === 'pbf') {
1141
+ let isGzipped =
1142
+ response.data
1143
+ .slice(0, 2)
1144
+ .indexOf(Buffer.from([0x1f, 0x8b])) === 0;
1145
+ if (isGzipped) {
1146
+ response.data = await gunzipP(response.data);
1147
+ }
1148
+ if (options.dataDecoratorFunc) {
1149
+ response.data = options.dataDecoratorFunc(
1150
+ sourceId,
1151
+ 'data',
1152
+ response.data,
1153
+ z,
1154
+ x,
1155
+ y,
1156
+ );
1157
+ }
1158
+ }
1159
+
1160
+ callback(null, response);
1013
1161
  } else if (protocol === 'http' || protocol === 'https') {
1014
1162
  try {
1015
1163
  const response = await axios.get(req.url, {
@@ -1052,9 +1200,13 @@ export const serve_rendered = {
1052
1200
  );
1053
1201
  }
1054
1202
 
1055
- fs.readFile(file, (err, data) => {
1056
- callback(err, { data: data });
1057
- });
1203
+ readFile(file)
1204
+ .then((data) => {
1205
+ callback(null, { data: data });
1206
+ })
1207
+ .catch((err) => {
1208
+ callback(err, null);
1209
+ });
1058
1210
  } else {
1059
1211
  throw Error(
1060
1212
  `File does not exist: "${req.url}" - resolved to "${file}"`,
@@ -1288,7 +1440,13 @@ export const serve_rendered = {
1288
1440
  );
1289
1441
  }
1290
1442
  },
1291
- remove: (repo, id) => {
1443
+ /**
1444
+ * Removes an item from the repository.
1445
+ * @param {object} repo Repository object.
1446
+ * @param {string} id ID of the item to remove.
1447
+ * @returns {void}
1448
+ */
1449
+ remove: function (repo, id) {
1292
1450
  const item = repo[id];
1293
1451
  if (item) {
1294
1452
  item.map.renderers.forEach((pool) => {