tileserver-gl-light 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.
@@ -7,21 +7,21 @@ import url from 'url';
7
7
  import util from 'util';
8
8
  import zlib from 'zlib';
9
9
  import sharp from 'sharp'; // sharp has to be required before node-canvas. see https://github.com/lovell/sharp/issues/371
10
- import pkg from 'canvas';
10
+ import { createCanvas, Image } from 'canvas';
11
11
  import clone from 'clone';
12
12
  import Color from 'color';
13
13
  import express from 'express';
14
+ import sanitize from 'sanitize-filename';
14
15
  import SphericalMercator from '@mapbox/sphericalmercator';
15
16
  import mlgl from '@maplibre/maplibre-gl-native';
16
17
  import MBTiles from '@mapbox/mbtiles';
17
18
  import proj4 from 'proj4';
18
19
  import request from 'request';
19
- import {getFontsPbf, getTileUrls, fixTileJSONCenter} from './utils.js';
20
+ import { getFontsPbf, getTileUrls, fixTileJSONCenter } from './utils.js';
20
21
 
21
- const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+\.?\\d+)';
22
+ const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+.?\\d+)';
22
23
  const httpTester = /^(http(s)?:)?\/\//;
23
24
 
24
- const {createCanvas} = pkg;
25
25
  const mercator = new SphericalMercator();
26
26
  const getScale = (scale) => (scale || '@1x').slice(1, 2) | 0;
27
27
 
@@ -38,7 +38,7 @@ const extensionToFormat = {
38
38
  '.jpg': 'jpeg',
39
39
  '.jpeg': 'jpeg',
40
40
  '.png': 'png',
41
- '.webp': 'webp'
41
+ '.webp': 'webp',
42
42
  };
43
43
 
44
44
  /**
@@ -46,18 +46,19 @@ const extensionToFormat = {
46
46
  * string is for unknown or unsupported formats.
47
47
  */
48
48
  const cachedEmptyResponses = {
49
- '': Buffer.alloc(0)
49
+ '': Buffer.alloc(0),
50
50
  };
51
51
 
52
52
  /**
53
53
  * Create an appropriate mlgl response for http errors.
54
+ *
54
55
  * @param {string} format The format (a sharp format or 'pbf').
55
56
  * @param {string} color The background color (or empty string for transparent).
56
57
  * @param {Function} callback The mlgl callback.
57
58
  */
58
59
  function createEmptyResponse(format, color, callback) {
59
60
  if (!format || format === 'pbf') {
60
- callback(null, {data: cachedEmptyResponses['']});
61
+ callback(null, { data: cachedEmptyResponses[''] });
61
62
  return;
62
63
  }
63
64
 
@@ -71,7 +72,7 @@ function createEmptyResponse(format, color, callback) {
71
72
  const cacheKey = `${format},${color}`;
72
73
  const data = cachedEmptyResponses[cacheKey];
73
74
  if (data) {
74
- callback(null, {data: data});
75
+ callback(null, { data: data });
75
76
  return;
76
77
  }
77
78
 
@@ -83,47 +84,422 @@ function createEmptyResponse(format, color, callback) {
83
84
  raw: {
84
85
  width: 1,
85
86
  height: 1,
86
- channels: channels
87
- }
88
- }).toFormat(format).toBuffer((err, buffer, info) => {
89
- if (!err) {
90
- cachedEmptyResponses[cacheKey] = buffer;
91
- }
92
- callback(null, {data: buffer});
93
- });
87
+ channels: channels,
88
+ },
89
+ })
90
+ .toFormat(format)
91
+ .toBuffer((err, buffer, info) => {
92
+ if (!err) {
93
+ cachedEmptyResponses[cacheKey] = buffer;
94
+ }
95
+ callback(null, { data: buffer });
96
+ });
94
97
  }
95
98
 
96
- const extractPathFromQuery = (query, transformer) => {
97
- const pathParts = (query.path || '').split('|');
98
- const path = [];
99
- for (const pair of pathParts) {
100
- const pairParts = pair.split(',');
101
- if (pairParts.length === 2) {
102
- let pair;
103
- if (query.latlng === '1' || query.latlng === 'true') {
104
- pair = [+(pairParts[1]), +(pairParts[0])];
105
- } else {
106
- pair = [+(pairParts[0]), +(pairParts[1])];
99
+ /**
100
+ * Parses coordinate pair provided to pair of floats and ensures the resulting
101
+ * pair is a longitude/latitude combination depending on lnglat query parameter.
102
+ *
103
+ * @param {List} coordinatePair Coordinate pair.
104
+ * @param coordinates
105
+ * @param {object} query Request query parameters.
106
+ */
107
+ const parseCoordinatePair = (coordinates, query) => {
108
+ const firstCoordinate = parseFloat(coordinates[0]);
109
+ const secondCoordinate = parseFloat(coordinates[1]);
110
+
111
+ // Ensure provided coordinates could be parsed and abort if not
112
+ if (isNaN(firstCoordinate) || isNaN(secondCoordinate)) {
113
+ return null;
114
+ }
115
+
116
+ // Check if coordinates have been provided as lat/lng pair instead of the
117
+ // ususal lng/lat pair and ensure resulting pair is lng/lat
118
+ if (query.latlng === '1' || query.latlng === 'true') {
119
+ return [secondCoordinate, firstCoordinate];
120
+ }
121
+
122
+ return [firstCoordinate, secondCoordinate];
123
+ };
124
+
125
+ /**
126
+ * Parses a coordinate pair from query arguments and optionally transforms it.
127
+ *
128
+ * @param {List} coordinatePair Coordinate pair.
129
+ * @param {object} query Request query parameters.
130
+ * @param {Function} transformer Optional transform function.
131
+ */
132
+ const parseCoordinates = (coordinatePair, query, transformer) => {
133
+ const parsedCoordinates = parseCoordinatePair(coordinatePair, query);
134
+
135
+ // Transform coordinates
136
+ if (transformer) {
137
+ return transformer(parsedCoordinates);
138
+ }
139
+
140
+ return parsedCoordinates;
141
+ };
142
+
143
+ /**
144
+ * Parses paths provided via query into a list of path objects.
145
+ *
146
+ * @param {object} query Request query parameters.
147
+ * @param {Function} transformer Optional transform function.
148
+ */
149
+ const extractPathsFromQuery = (query, transformer) => {
150
+ // Return an empty list if no paths have been provided
151
+ if (!query.path) {
152
+ return [];
153
+ }
154
+
155
+ const paths = [];
156
+
157
+ // Check if multiple paths have been provided and mimic a list if it's a
158
+ // single path.
159
+ const providedPaths = Array.isArray(query.path) ? query.path : [query.path];
160
+
161
+ // Iterate through paths, parse and validate them
162
+ for (const provided_path of providedPaths) {
163
+ const currentPath = [];
164
+
165
+ // Extract coordinate-list from path
166
+ const pathParts = (provided_path || '').split('|');
167
+
168
+ // Iterate through coordinate-list, parse the coordinates and validate them
169
+ for (const pair of pathParts) {
170
+ // Extract coordinates from coordinate pair
171
+ const pairParts = pair.split(',');
172
+
173
+ // Ensure we have two coordinates
174
+ if (pairParts.length === 2) {
175
+ const pair = parseCoordinates(pairParts, query, transformer);
176
+
177
+ // Ensure coordinates could be parsed and skip them if not
178
+ if (pair === null) {
179
+ continue;
180
+ }
181
+
182
+ // Add the coordinate-pair to the current path if they are valid
183
+ currentPath.push(pair);
107
184
  }
108
- if (transformer) {
109
- pair = transformer(pair);
185
+ }
186
+
187
+ // Extend list of paths with current path if it contains coordinates
188
+ if (currentPath.length) {
189
+ paths.push(currentPath);
190
+ }
191
+ }
192
+ return paths;
193
+ };
194
+
195
+ /**
196
+ * Parses marker options provided via query and sets corresponding attributes
197
+ * on marker object.
198
+ * Options adhere to the following format
199
+ * [optionName]:[optionValue]
200
+ *
201
+ * @param {List[String]} optionsList List of option strings.
202
+ * @param {object} marker Marker object to configure.
203
+ */
204
+ const parseMarkerOptions = (optionsList, marker) => {
205
+ for (const options of optionsList) {
206
+ const optionParts = options.split(':');
207
+ // Ensure we got an option name and value
208
+ if (optionParts.length < 2) {
209
+ continue;
210
+ }
211
+
212
+ switch (optionParts[0]) {
213
+ // Scale factor to up- or downscale icon
214
+ case 'scale':
215
+ // Scale factors must not be negative
216
+ marker.scale = Math.abs(parseFloat(optionParts[1]));
217
+ break;
218
+ // Icon offset as positive or negative pixel value in the following
219
+ // format [offsetX],[offsetY] where [offsetY] is optional
220
+ case 'offset':
221
+ const providedOffset = optionParts[1].split(',');
222
+ // Set X-axis offset
223
+ marker.offsetX = parseFloat(providedOffset[0]);
224
+ // Check if an offset has been provided for Y-axis
225
+ if (providedOffset.length > 1) {
226
+ marker.offsetY = parseFloat(providedOffset[1]);
227
+ }
228
+ break;
229
+ }
230
+ }
231
+ };
232
+
233
+ /**
234
+ * Parses markers provided via query into a list of marker objects.
235
+ *
236
+ * @param {object} query Request query parameters.
237
+ * @param {object} options Configuration options.
238
+ * @param {Function} transformer Optional transform function.
239
+ */
240
+ const extractMarkersFromQuery = (query, options, transformer) => {
241
+ // Return an empty list if no markers have been provided
242
+ if (!query.marker) {
243
+ return [];
244
+ }
245
+
246
+ const markers = [];
247
+
248
+ // Check if multiple markers have been provided and mimic a list if it's a
249
+ // single maker.
250
+ const providedMarkers = Array.isArray(query.marker)
251
+ ? query.marker
252
+ : [query.marker];
253
+
254
+ // Iterate through provided markers which can have one of the following
255
+ // formats
256
+ // [location]|[pathToFileTelativeToConfiguredIconPath]
257
+ // [location]|[pathToFile...]|[option]|[option]|...
258
+ for (const providedMarker of providedMarkers) {
259
+ const markerParts = providedMarker.split('|');
260
+ // Ensure we got at least a location and an icon uri
261
+ if (markerParts.length < 2) {
262
+ continue;
263
+ }
264
+
265
+ const locationParts = markerParts[0].split(',');
266
+ // Ensure the locationParts contains two items
267
+ if (locationParts.length !== 2) {
268
+ continue;
269
+ }
270
+
271
+ let iconURI = markerParts[1];
272
+ // Check if icon is served via http otherwise marker icons are expected to
273
+ // be provided as filepaths relative to configured icon path
274
+ if (!(iconURI.startsWith('http://') || iconURI.startsWith('https://'))) {
275
+ // Sanitize URI with sanitize-filename
276
+ // https://www.npmjs.com/package/sanitize-filename#details
277
+ iconURI = sanitize(iconURI);
278
+
279
+ // If the selected icon is not part of available icons skip it
280
+ if (!options.paths.availableIcons.includes(iconURI)) {
281
+ continue;
110
282
  }
111
- path.push(pair);
283
+
284
+ iconURI = path.resolve(options.paths.icons, iconURI);
285
+
286
+ // When we encounter a remote icon check if the configuration explicitly allows them.
287
+ } else if (options.allowRemoteMarkerIcons !== true) {
288
+ continue;
289
+ }
290
+
291
+ // Ensure marker location could be parsed
292
+ const location = parseCoordinates(locationParts, query, transformer);
293
+ if (location === null) {
294
+ continue;
295
+ }
296
+
297
+ const marker = {};
298
+
299
+ marker.location = location;
300
+ marker.icon = iconURI;
301
+
302
+ // Check if options have been provided
303
+ if (markerParts.length > 2) {
304
+ parseMarkerOptions(markerParts.slice(2), marker);
112
305
  }
306
+
307
+ // Add marker to list
308
+ markers.push(marker);
309
+ }
310
+ return markers;
311
+ };
312
+
313
+ /**
314
+ * Transforms coordinates to pixels.
315
+ *
316
+ * @param {List[Number]} ll Longitude/Latitude coordinate pair.
317
+ * @param {number} zoom Map zoom level.
318
+ */
319
+ const precisePx = (ll, zoom) => {
320
+ const px = mercator.px(ll, 20);
321
+ const scale = Math.pow(2, zoom - 20);
322
+ return [px[0] * scale, px[1] * scale];
323
+ };
324
+
325
+ /**
326
+ * Draws a marker in cavans context.
327
+ *
328
+ * @param {object} ctx Canvas context object.
329
+ * @param {object} marker Marker object parsed by extractMarkersFromQuery.
330
+ * @param {number} z Map zoom level.
331
+ */
332
+ const drawMarker = (ctx, marker, z) => {
333
+ return new Promise((resolve) => {
334
+ const img = new Image();
335
+ const pixelCoords = precisePx(marker.location, z);
336
+
337
+ const getMarkerCoordinates = (imageWidth, imageHeight, scale) => {
338
+ // Images are placed with their top-left corner at the provided location
339
+ // within the canvas but we expect icons to be centered and above it.
340
+
341
+ // Substract half of the images width from the x-coordinate to center
342
+ // the image in relation to the provided location
343
+ let xCoordinate = pixelCoords[0] - imageWidth / 2;
344
+ // Substract the images height from the y-coordinate to place it above
345
+ // the provided location
346
+ let yCoordinate = pixelCoords[1] - imageHeight;
347
+
348
+ // Since image placement is dependent on the size offsets have to be
349
+ // scaled as well. Additionally offsets are provided as either positive or
350
+ // negative values so we always add them
351
+ if (marker.offsetX) {
352
+ xCoordinate = xCoordinate + marker.offsetX * scale;
353
+ }
354
+ if (marker.offsetY) {
355
+ yCoordinate = yCoordinate + marker.offsetY * scale;
356
+ }
357
+
358
+ return {
359
+ x: xCoordinate,
360
+ y: yCoordinate,
361
+ };
362
+ };
363
+
364
+ const drawOnCanvas = () => {
365
+ // Check if the images should be resized before beeing drawn
366
+ const defaultScale = 1;
367
+ const scale = marker.scale ? marker.scale : defaultScale;
368
+
369
+ // Calculate scaled image sizes
370
+ const imageWidth = img.width * scale;
371
+ const imageHeight = img.height * scale;
372
+
373
+ // Pass the desired sizes to get correlating coordinates
374
+ const coords = getMarkerCoordinates(imageWidth, imageHeight, scale);
375
+
376
+ // Draw the image on canvas
377
+ if (scale != defaultScale) {
378
+ ctx.drawImage(img, coords.x, coords.y, imageWidth, imageHeight);
379
+ } else {
380
+ ctx.drawImage(img, coords.x, coords.y);
381
+ }
382
+ // Resolve the promise when image has been drawn
383
+ resolve();
384
+ };
385
+
386
+ img.onload = drawOnCanvas;
387
+ img.onerror = (err) => {
388
+ throw err;
389
+ };
390
+ img.src = marker.icon;
391
+ });
392
+ };
393
+
394
+ /**
395
+ * Draws a list of markers onto a canvas.
396
+ * Wraps drawing of markers into list of promises and awaits them.
397
+ * It's required because images are expected to load asynchronous in canvas js
398
+ * even when provided from a local disk.
399
+ *
400
+ * @param {object} ctx Canvas context object.
401
+ * @param {List[Object]} markers Marker objects parsed by extractMarkersFromQuery.
402
+ * @param {number} z Map zoom level.
403
+ */
404
+ const drawMarkers = async (ctx, markers, z) => {
405
+ const markerPromises = [];
406
+
407
+ for (const marker of markers) {
408
+ // Begin drawing marker
409
+ markerPromises.push(drawMarker(ctx, marker, z));
113
410
  }
114
- return path;
411
+
412
+ // Await marker drawings before continuing
413
+ await Promise.all(markerPromises);
115
414
  };
116
415
 
117
- const renderOverlay = (z, x, y, bearing, pitch, w, h, scale,
118
- path, query) => {
416
+ /**
417
+ * Draws a list of coordinates onto a canvas and styles the resulting path.
418
+ *
419
+ * @param {object} ctx Canvas context object.
420
+ * @param {List[Number]} path List of coordinates.
421
+ * @param {object} query Request query parameters.
422
+ * @param {number} z Map zoom level.
423
+ */
424
+ const drawPath = (ctx, path, query, z) => {
119
425
  if (!path || path.length < 2) {
120
426
  return null;
121
427
  }
122
- const precisePx = (ll, zoom) => {
123
- const px = mercator.px(ll, 20);
124
- const scale = Math.pow(2, zoom - 20);
125
- return [px[0] * scale, px[1] * scale];
126
- };
428
+
429
+ ctx.beginPath();
430
+
431
+ // Transform coordinates to pixel on canvas and draw lines between points
432
+ for (const pair of path) {
433
+ const px = precisePx(pair, z);
434
+ ctx.lineTo(px[0], px[1]);
435
+ }
436
+
437
+ // Check if first coordinate matches last coordinate
438
+ if (
439
+ path[0][0] === path[path.length - 1][0] &&
440
+ path[0][1] === path[path.length - 1][1]
441
+ ) {
442
+ ctx.closePath();
443
+ }
444
+
445
+ // Optionally fill drawn shape with a rgba color from query
446
+ if (query.fill !== undefined) {
447
+ ctx.fillStyle = query.fill;
448
+ ctx.fill();
449
+ }
450
+
451
+ // Get line width from query and fall back to 1 if not provided
452
+ const lineWidth = query.width !== undefined ? parseFloat(query.width) : 1;
453
+
454
+ // Ensure line width is valid
455
+ if (lineWidth > 0) {
456
+ // Get border width from query and fall back to 10% of line width
457
+ const borderWidth =
458
+ query.borderwidth !== undefined
459
+ ? parseFloat(query.borderwidth)
460
+ : lineWidth * 0.1;
461
+
462
+ // Set rendering style for the start and end points of the path
463
+ // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap
464
+ ctx.lineCap = query.linecap || 'butt';
465
+
466
+ // Set rendering style for overlapping segments of the path with differing directions
467
+ // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin
468
+ ctx.lineJoin = query.linejoin || 'miter';
469
+
470
+ // In order to simulate a border we draw the path two times with the first
471
+ // beeing the wider border part.
472
+ if (query.border !== undefined && borderWidth > 0) {
473
+ // We need to double the desired border width and add it to the line width
474
+ // in order to get the desired border on each side of the line.
475
+ ctx.lineWidth = lineWidth + borderWidth * 2;
476
+ // Set border style as rgba
477
+ ctx.strokeStyle = query.border;
478
+ ctx.stroke();
479
+ }
480
+
481
+ ctx.lineWidth = lineWidth;
482
+ ctx.strokeStyle = query.stroke || 'rgba(0,64,255,0.7)';
483
+ ctx.stroke();
484
+ }
485
+ };
486
+
487
+ const renderOverlay = async (
488
+ z,
489
+ x,
490
+ y,
491
+ bearing,
492
+ pitch,
493
+ w,
494
+ h,
495
+ scale,
496
+ paths,
497
+ markers,
498
+ query,
499
+ ) => {
500
+ if ((!paths || paths.length === 0) && (!markers || markers.length === 0)) {
501
+ return null;
502
+ }
127
503
 
128
504
  const center = precisePx([x, y], z);
129
505
 
@@ -131,7 +507,7 @@ const renderOverlay = (z, x, y, bearing, pitch, w, h, scale,
131
507
  const maxEdge = center[1] + h / 2;
132
508
  const minEdge = center[1] - h / 2;
133
509
  if (maxEdge > mapHeight) {
134
- center[1] -= (maxEdge - mapHeight);
510
+ center[1] -= maxEdge - mapHeight;
135
511
  } else if (minEdge < 0) {
136
512
  center[1] -= minEdge;
137
513
  }
@@ -141,49 +517,39 @@ const renderOverlay = (z, x, y, bearing, pitch, w, h, scale,
141
517
  ctx.scale(scale, scale);
142
518
  if (bearing) {
143
519
  ctx.translate(w / 2, h / 2);
144
- ctx.rotate(-bearing / 180 * Math.PI);
520
+ ctx.rotate((-bearing / 180) * Math.PI);
145
521
  ctx.translate(-center[0], -center[1]);
146
522
  } else {
147
523
  // optimized path
148
524
  ctx.translate(-center[0] + w / 2, -center[1] + h / 2);
149
525
  }
150
- const lineWidth = query.width !== undefined ?
151
- parseFloat(query.width) : 1;
152
- ctx.lineWidth = lineWidth;
153
- ctx.strokeStyle = query.stroke || 'rgba(0,64,255,0.7)';
154
- ctx.fillStyle = query.fill || 'rgba(255,255,255,0.4)';
155
- ctx.beginPath();
156
- for (const pair of path) {
157
- const px = precisePx(pair, z);
158
- ctx.lineTo(px[0], px[1]);
159
- }
160
- if (path[0][0] === path[path.length - 1][0] &&
161
- path[0][1] === path[path.length - 1][1]) {
162
- ctx.closePath();
163
- }
164
- ctx.fill();
165
- if (lineWidth > 0) {
166
- ctx.stroke();
526
+
527
+ // Draw provided paths if any
528
+ for (const path of paths) {
529
+ drawPath(ctx, path, query, z);
167
530
  }
168
531
 
532
+ // Await drawing of markers before rendering the canvas
533
+ await drawMarkers(ctx, markers, z);
534
+
169
535
  return canvas.toBuffer();
170
536
  };
171
537
 
172
538
  const calcZForBBox = (bbox, w, h, query) => {
173
539
  let z = 25;
174
540
 
175
- const padding = query.padding !== undefined ?
176
- parseFloat(query.padding) : 0.1;
541
+ const padding = query.padding !== undefined ? parseFloat(query.padding) : 0.1;
177
542
 
178
543
  const minCorner = mercator.px([bbox[0], bbox[3]], z);
179
544
  const maxCorner = mercator.px([bbox[2], bbox[1]], z);
180
545
  const w_ = w / (1 + 2 * padding);
181
546
  const h_ = h / (1 + 2 * padding);
182
547
 
183
- z -= Math.max(
548
+ z -=
549
+ Math.max(
184
550
  Math.log((maxCorner[0] - minCorner[0]) / w_),
185
- Math.log((maxCorner[1] - minCorner[1]) / h_)
186
- ) / Math.LN2;
551
+ Math.log((maxCorner[1] - minCorner[1]) / h_),
552
+ ) / Math.LN2;
187
553
 
188
554
  z = Math.max(Math.log(Math.max(w, h) / 256) / Math.LN2, Math.min(25, z));
189
555
 
@@ -225,14 +591,36 @@ export const serve_rendered = {
225
591
 
226
592
  const app = express().disable('x-powered-by');
227
593
 
228
- const respondImage = (item, z, lon, lat, bearing, pitch, width, height, scale, format, res, next, opt_overlay, opt_mode='tile') => {
229
- if (Math.abs(lon) > 180 || Math.abs(lat) > 85.06 ||
230
- lon !== lon || lat !== lat) {
594
+ const respondImage = (
595
+ item,
596
+ z,
597
+ lon,
598
+ lat,
599
+ bearing,
600
+ pitch,
601
+ width,
602
+ height,
603
+ scale,
604
+ format,
605
+ res,
606
+ next,
607
+ opt_overlay,
608
+ opt_mode = 'tile',
609
+ ) => {
610
+ if (
611
+ Math.abs(lon) > 180 ||
612
+ Math.abs(lat) > 85.06 ||
613
+ lon !== lon ||
614
+ lat !== lat
615
+ ) {
231
616
  return res.status(400).send('Invalid center');
232
617
  }
233
- if (Math.min(width, height) <= 0 ||
618
+ if (
619
+ Math.min(width, height) <= 0 ||
234
620
  Math.max(width, height) * scale > (options.maxSize || 2048) ||
235
- width !== width || height !== height) {
621
+ width !== width ||
622
+ height !== height
623
+ ) {
236
624
  return res.status(400).send('Invalid size');
237
625
  }
238
626
  if (format === 'png' || format === 'webp') {
@@ -256,7 +644,7 @@ export const serve_rendered = {
256
644
  bearing: bearing,
257
645
  pitch: pitch,
258
646
  width: width,
259
- height: height
647
+ height: height,
260
648
  };
261
649
  if (z === 0) {
262
650
  params.width *= 2;
@@ -296,18 +684,21 @@ export const serve_rendered = {
296
684
  raw: {
297
685
  width: params.width * scale,
298
686
  height: params.height * scale,
299
- channels: 4
300
- }
687
+ channels: 4,
688
+ },
301
689
  });
302
690
 
303
691
  if (z > 2 && tileMargin > 0) {
304
692
  const [_, y] = mercator.px(params.center, z);
305
- let yoffset = Math.max(Math.min(0, y - 128 - tileMargin), y + 128 + tileMargin - Math.pow(2, z + 8));
693
+ let yoffset = Math.max(
694
+ Math.min(0, y - 128 - tileMargin),
695
+ y + 128 + tileMargin - Math.pow(2, z + 8),
696
+ );
306
697
  image.extract({
307
698
  left: tileMargin * scale,
308
699
  top: (tileMargin + yoffset) * scale,
309
700
  width: width * scale,
310
- height: height * scale
701
+ height: height * scale,
311
702
  });
312
703
  }
313
704
 
@@ -317,7 +708,7 @@ export const serve_rendered = {
317
708
  }
318
709
 
319
710
  if (opt_overlay) {
320
- image.composite([{input: opt_overlay}]);
711
+ image.composite([{ input: opt_overlay }]);
321
712
  }
322
713
  if (item.watermark) {
323
714
  const canvas = createCanvas(scale * width, scale * height);
@@ -330,17 +721,17 @@ export const serve_rendered = {
330
721
  ctx.fillStyle = 'rgba(0,0,0,.4)';
331
722
  ctx.fillText(item.watermark, 5, height - 5);
332
723
 
333
- image.composite([{input: canvas.toBuffer()}]);
724
+ image.composite([{ input: canvas.toBuffer() }]);
334
725
  }
335
726
 
336
727
  const formatQuality = (options.formatQuality || {})[format];
337
728
 
338
729
  if (format === 'png') {
339
- image.png({adaptiveFiltering: false});
730
+ image.png({ adaptiveFiltering: false });
340
731
  } else if (format === 'jpeg') {
341
- image.jpeg({quality: formatQuality || 80});
732
+ image.jpeg({ quality: formatQuality || 80 });
342
733
  } else if (format === 'webp') {
343
- image.webp({quality: formatQuality || 90});
734
+ image.webp({ quality: formatQuality || 90 });
344
735
  }
345
736
  image.toBuffer((err, buffer, info) => {
346
737
  if (!buffer) {
@@ -349,7 +740,7 @@ export const serve_rendered = {
349
740
 
350
741
  res.set({
351
742
  'Last-Modified': item.lastModified,
352
- 'Content-Type': `image/${format}`
743
+ 'Content-Type': `image/${format}`,
353
744
  });
354
745
  return res.status(200).send(buffer);
355
746
  });
@@ -357,91 +748,162 @@ export const serve_rendered = {
357
748
  });
358
749
  };
359
750
 
360
- app.get(`/:id/:z(\\d+)/:x(\\d+)/:y(\\d+):scale(${scalePattern})?.:format([\\w]+)`, (req, res, next) => {
361
- const item = repo[req.params.id];
362
- if (!item) {
363
- return res.sendStatus(404);
364
- }
365
-
366
- const modifiedSince = req.get('if-modified-since'); const cc = req.get('cache-control');
367
- if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) {
368
- if (new Date(item.lastModified) <= new Date(modifiedSince)) {
369
- return res.sendStatus(304);
370
- }
371
- }
372
-
373
- const z = req.params.z | 0;
374
- const x = req.params.x | 0;
375
- const y = req.params.y | 0;
376
- const scale = getScale(req.params.scale);
377
- const format = req.params.format;
378
- if (z < 0 || x < 0 || y < 0 ||
379
- z > 22 || x >= Math.pow(2, z) || y >= Math.pow(2, z)) {
380
- return res.status(404).send('Out of bounds');
381
- }
382
- const tileSize = 256;
383
- const tileCenter = mercator.ll([
384
- ((x + 0.5) / (1 << z)) * (256 << z),
385
- ((y + 0.5) / (1 << z)) * (256 << z)
386
- ], z);
387
- return respondImage(item, z, tileCenter[0], tileCenter[1], 0, 0, tileSize, tileSize, scale, format, res, next);
388
- });
389
-
390
- if (options.serveStaticMaps !== false) {
391
- const staticPattern =
392
- `/:id/static/:raw(raw)?/%s/:width(\\d+)x:height(\\d+):scale(${scalePattern})?.:format([\\w]+)`;
393
-
394
- const centerPattern =
395
- util.format(':x(%s),:y(%s),:z(%s)(@:bearing(%s)(,:pitch(%s))?)?',
396
- FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN,
397
- FLOAT_PATTERN, FLOAT_PATTERN);
398
-
399
- app.get(util.format(staticPattern, centerPattern), (req, res, next) => {
751
+ app.get(
752
+ `/:id/:z(\\d+)/:x(\\d+)/:y(\\d+):scale(${scalePattern})?.:format([\\w]+)`,
753
+ (req, res, next) => {
400
754
  const item = repo[req.params.id];
401
755
  if (!item) {
402
756
  return res.sendStatus(404);
403
757
  }
404
- const raw = req.params.raw;
405
- const z = +req.params.z;
406
- let x = +req.params.x;
407
- let y = +req.params.y;
408
- const bearing = +(req.params.bearing || '0');
409
- const pitch = +(req.params.pitch || '0');
410
- const w = req.params.width | 0;
411
- const h = req.params.height | 0;
412
- const scale = getScale(req.params.scale);
413
- const format = req.params.format;
414
758
 
415
- if (z < 0) {
416
- return res.status(404).send('Invalid zoom');
759
+ const modifiedSince = req.get('if-modified-since');
760
+ const cc = req.get('cache-control');
761
+ if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) {
762
+ if (new Date(item.lastModified) <= new Date(modifiedSince)) {
763
+ return res.sendStatus(304);
764
+ }
417
765
  }
418
766
 
419
- const transformer = raw ?
420
- mercator.inverse.bind(mercator) : item.dataProjWGStoInternalWGS;
421
-
422
- if (transformer) {
423
- const ll = transformer([x, y]);
424
- x = ll[0];
425
- y = ll[1];
767
+ const z = req.params.z | 0;
768
+ const x = req.params.x | 0;
769
+ const y = req.params.y | 0;
770
+ const scale = getScale(req.params.scale);
771
+ const format = req.params.format;
772
+ if (
773
+ z < 0 ||
774
+ x < 0 ||
775
+ y < 0 ||
776
+ z > 22 ||
777
+ x >= Math.pow(2, z) ||
778
+ y >= Math.pow(2, z)
779
+ ) {
780
+ return res.status(404).send('Out of bounds');
426
781
  }
782
+ const tileSize = 256;
783
+ const tileCenter = mercator.ll(
784
+ [
785
+ ((x + 0.5) / (1 << z)) * (256 << z),
786
+ ((y + 0.5) / (1 << z)) * (256 << z),
787
+ ],
788
+ z,
789
+ );
790
+ return respondImage(
791
+ item,
792
+ z,
793
+ tileCenter[0],
794
+ tileCenter[1],
795
+ 0,
796
+ 0,
797
+ tileSize,
798
+ tileSize,
799
+ scale,
800
+ format,
801
+ res,
802
+ next,
803
+ );
804
+ },
805
+ );
806
+
807
+ if (options.serveStaticMaps !== false) {
808
+ const staticPattern = `/:id/static/:raw(raw)?/%s/:width(\\d+)x:height(\\d+):scale(${scalePattern})?.:format([\\w]+)`;
809
+
810
+ const centerPattern = util.format(
811
+ ':x(%s),:y(%s),:z(%s)(@:bearing(%s)(,:pitch(%s))?)?',
812
+ FLOAT_PATTERN,
813
+ FLOAT_PATTERN,
814
+ FLOAT_PATTERN,
815
+ FLOAT_PATTERN,
816
+ FLOAT_PATTERN,
817
+ );
818
+
819
+ app.get(
820
+ util.format(staticPattern, centerPattern),
821
+ async (req, res, next) => {
822
+ const item = repo[req.params.id];
823
+ if (!item) {
824
+ return res.sendStatus(404);
825
+ }
826
+ const raw = req.params.raw;
827
+ const z = +req.params.z;
828
+ let x = +req.params.x;
829
+ let y = +req.params.y;
830
+ const bearing = +(req.params.bearing || '0');
831
+ const pitch = +(req.params.pitch || '0');
832
+ const w = req.params.width | 0;
833
+ const h = req.params.height | 0;
834
+ const scale = getScale(req.params.scale);
835
+ const format = req.params.format;
836
+
837
+ if (z < 0) {
838
+ return res.status(404).send('Invalid zoom');
839
+ }
427
840
 
428
- const path = extractPathFromQuery(req.query, transformer);
429
- const overlay = renderOverlay(z, x, y, bearing, pitch, w, h, scale, path, req.query);
841
+ const transformer = raw
842
+ ? mercator.inverse.bind(mercator)
843
+ : item.dataProjWGStoInternalWGS;
430
844
 
431
- return respondImage(item, z, x, y, bearing, pitch, w, h, scale, format, res, next, overlay, 'static');
432
- });
845
+ if (transformer) {
846
+ const ll = transformer([x, y]);
847
+ x = ll[0];
848
+ y = ll[1];
849
+ }
433
850
 
434
- const serveBounds = (req, res, next) => {
851
+ const paths = extractPathsFromQuery(req.query, transformer);
852
+ const markers = extractMarkersFromQuery(
853
+ req.query,
854
+ options,
855
+ transformer,
856
+ );
857
+ const overlay = await renderOverlay(
858
+ z,
859
+ x,
860
+ y,
861
+ bearing,
862
+ pitch,
863
+ w,
864
+ h,
865
+ scale,
866
+ paths,
867
+ markers,
868
+ req.query,
869
+ );
870
+
871
+ return respondImage(
872
+ item,
873
+ z,
874
+ x,
875
+ y,
876
+ bearing,
877
+ pitch,
878
+ w,
879
+ h,
880
+ scale,
881
+ format,
882
+ res,
883
+ next,
884
+ overlay,
885
+ 'static',
886
+ );
887
+ },
888
+ );
889
+
890
+ const serveBounds = async (req, res, next) => {
435
891
  const item = repo[req.params.id];
436
892
  if (!item) {
437
893
  return res.sendStatus(404);
438
894
  }
439
895
  const raw = req.params.raw;
440
- const bbox = [+req.params.minx, +req.params.miny, +req.params.maxx, +req.params.maxy];
896
+ const bbox = [
897
+ +req.params.minx,
898
+ +req.params.miny,
899
+ +req.params.maxx,
900
+ +req.params.maxy,
901
+ ];
441
902
  let center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2];
442
903
 
443
- const transformer = raw ?
444
- mercator.inverse.bind(mercator) : item.dataProjWGStoInternalWGS;
904
+ const transformer = raw
905
+ ? mercator.inverse.bind(mercator)
906
+ : item.dataProjWGStoInternalWGS;
445
907
 
446
908
  if (transformer) {
447
909
  const minCorner = transformer(bbox.slice(0, 2));
@@ -464,15 +926,50 @@ export const serve_rendered = {
464
926
  const bearing = 0;
465
927
  const pitch = 0;
466
928
 
467
- const path = extractPathFromQuery(req.query, transformer);
468
- const overlay = renderOverlay(z, x, y, bearing, pitch, w, h, scale, path, req.query);
469
-
470
- return respondImage(item, z, x, y, bearing, pitch, w, h, scale, format, res, next, overlay, 'static');
929
+ const paths = extractPathsFromQuery(req.query, transformer);
930
+ const markers = extractMarkersFromQuery(
931
+ req.query,
932
+ options,
933
+ transformer,
934
+ );
935
+ const overlay = await renderOverlay(
936
+ z,
937
+ x,
938
+ y,
939
+ bearing,
940
+ pitch,
941
+ w,
942
+ h,
943
+ scale,
944
+ paths,
945
+ markers,
946
+ req.query,
947
+ );
948
+ return respondImage(
949
+ item,
950
+ z,
951
+ x,
952
+ y,
953
+ bearing,
954
+ pitch,
955
+ w,
956
+ h,
957
+ scale,
958
+ format,
959
+ res,
960
+ next,
961
+ overlay,
962
+ 'static',
963
+ );
471
964
  };
472
965
 
473
- const boundsPattern =
474
- util.format(':minx(%s),:miny(%s),:maxx(%s),:maxy(%s)',
475
- FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN);
966
+ const boundsPattern = util.format(
967
+ ':minx(%s),:miny(%s),:maxx(%s),:maxy(%s)',
968
+ FLOAT_PATTERN,
969
+ FLOAT_PATTERN,
970
+ FLOAT_PATTERN,
971
+ FLOAT_PATTERN,
972
+ );
476
973
 
477
974
  app.get(util.format(staticPattern, boundsPattern), serveBounds);
478
975
 
@@ -500,48 +997,102 @@ export const serve_rendered = {
500
997
 
501
998
  const autoPattern = 'auto';
502
999
 
503
- app.get(util.format(staticPattern, autoPattern), (req, res, next) => {
504
- const item = repo[req.params.id];
505
- if (!item) {
506
- return res.sendStatus(404);
507
- }
508
- const raw = req.params.raw;
509
- const w = req.params.width | 0;
510
- const h = req.params.height | 0;
511
- const bearing = 0;
512
- const pitch = 0;
513
- const scale = getScale(req.params.scale);
514
- const format = req.params.format;
515
-
516
- const transformer = raw ?
517
- mercator.inverse.bind(mercator) : item.dataProjWGStoInternalWGS;
518
-
519
- const path = extractPathFromQuery(req.query, transformer);
520
- if (path.length < 2) {
521
- return res.status(400).send('Invalid path');
522
- }
1000
+ app.get(
1001
+ util.format(staticPattern, autoPattern),
1002
+ async (req, res, next) => {
1003
+ const item = repo[req.params.id];
1004
+ if (!item) {
1005
+ return res.sendStatus(404);
1006
+ }
1007
+ const raw = req.params.raw;
1008
+ const w = req.params.width | 0;
1009
+ const h = req.params.height | 0;
1010
+ const bearing = 0;
1011
+ const pitch = 0;
1012
+ const scale = getScale(req.params.scale);
1013
+ const format = req.params.format;
1014
+
1015
+ const transformer = raw
1016
+ ? mercator.inverse.bind(mercator)
1017
+ : item.dataProjWGStoInternalWGS;
1018
+
1019
+ const paths = extractPathsFromQuery(req.query, transformer);
1020
+ const markers = extractMarkersFromQuery(
1021
+ req.query,
1022
+ options,
1023
+ transformer,
1024
+ );
1025
+
1026
+ // Extract coordinates from markers
1027
+ const markerCoordinates = [];
1028
+ for (const marker of markers) {
1029
+ markerCoordinates.push(marker.location);
1030
+ }
523
1031
 
524
- const bbox = [Infinity, Infinity, -Infinity, -Infinity];
525
- for (const pair of path) {
526
- bbox[0] = Math.min(bbox[0], pair[0]);
527
- bbox[1] = Math.min(bbox[1], pair[1]);
528
- bbox[2] = Math.max(bbox[2], pair[0]);
529
- bbox[3] = Math.max(bbox[3], pair[1]);
530
- }
1032
+ // Create array with coordinates from markers and path
1033
+ const coords = [].concat(paths.flat()).concat(markerCoordinates);
531
1034
 
532
- const bbox_ = mercator.convert(bbox, '900913');
533
- const center = mercator.inverse(
534
- [(bbox_[0] + bbox_[2]) / 2, (bbox_[1] + bbox_[3]) / 2]
535
- );
1035
+ // Check if we have at least one coordinate to calculate a bounding box
1036
+ if (coords.length < 1) {
1037
+ return res.status(400).send('No coordinates provided');
1038
+ }
536
1039
 
537
- const z = calcZForBBox(bbox, w, h, req.query);
538
- const x = center[0];
539
- const y = center[1];
1040
+ const bbox = [Infinity, Infinity, -Infinity, -Infinity];
1041
+ for (const pair of coords) {
1042
+ bbox[0] = Math.min(bbox[0], pair[0]);
1043
+ bbox[1] = Math.min(bbox[1], pair[1]);
1044
+ bbox[2] = Math.max(bbox[2], pair[0]);
1045
+ bbox[3] = Math.max(bbox[3], pair[1]);
1046
+ }
540
1047
 
541
- const overlay = renderOverlay(z, x, y, bearing, pitch, w, h, scale, path, req.query);
1048
+ const bbox_ = mercator.convert(bbox, '900913');
1049
+ const center = mercator.inverse([
1050
+ (bbox_[0] + bbox_[2]) / 2,
1051
+ (bbox_[1] + bbox_[3]) / 2,
1052
+ ]);
1053
+
1054
+ // Calculate zoom level
1055
+ const maxZoom = parseFloat(req.query.maxzoom);
1056
+ let z = calcZForBBox(bbox, w, h, req.query);
1057
+ if (maxZoom > 0) {
1058
+ z = Math.min(z, maxZoom);
1059
+ }
542
1060
 
543
- return respondImage(item, z, x, y, bearing, pitch, w, h, scale, format, res, next, overlay, 'static');
544
- });
1061
+ const x = center[0];
1062
+ const y = center[1];
1063
+
1064
+ const overlay = await renderOverlay(
1065
+ z,
1066
+ x,
1067
+ y,
1068
+ bearing,
1069
+ pitch,
1070
+ w,
1071
+ h,
1072
+ scale,
1073
+ paths,
1074
+ markers,
1075
+ req.query,
1076
+ );
1077
+
1078
+ return respondImage(
1079
+ item,
1080
+ z,
1081
+ x,
1082
+ y,
1083
+ bearing,
1084
+ pitch,
1085
+ w,
1086
+ h,
1087
+ scale,
1088
+ format,
1089
+ res,
1090
+ next,
1091
+ overlay,
1092
+ 'static',
1093
+ );
1094
+ },
1095
+ );
545
1096
  }
546
1097
 
547
1098
  app.get('/:id.json', (req, res, next) => {
@@ -550,8 +1101,13 @@ export const serve_rendered = {
550
1101
  return res.sendStatus(404);
551
1102
  }
552
1103
  const info = clone(item.tileJSON);
553
- info.tiles = getTileUrls(req, info.tiles,
554
- `styles/${req.params.id}`, info.format, item.publicUrl);
1104
+ info.tiles = getTileUrls(
1105
+ req,
1106
+ info.tiles,
1107
+ `styles/${req.params.id}`,
1108
+ info.format,
1109
+ item.publicUrl,
1110
+ );
555
1111
  return res.send(info);
556
1112
  });
557
1113
 
@@ -561,7 +1117,7 @@ export const serve_rendered = {
561
1117
  const map = {
562
1118
  renderers: [],
563
1119
  renderers_static: [],
564
- sources: {}
1120
+ sources: {},
565
1121
  };
566
1122
 
567
1123
  let styleJSON;
@@ -577,19 +1133,26 @@ export const serve_rendered = {
577
1133
  const dir = options.paths[protocol];
578
1134
  const file = unescape(req.url).substring(protocol.length + 3);
579
1135
  fs.readFile(path.join(dir, file), (err, data) => {
580
- callback(err, {data: data});
1136
+ callback(err, { data: data });
581
1137
  });
582
1138
  } else if (protocol === 'fonts') {
583
1139
  const parts = req.url.split('/');
584
1140
  const fontstack = unescape(parts[2]);
585
1141
  const range = parts[3].split('.')[0];
586
1142
  getFontsPbf(
587
- null, options.paths[protocol], fontstack, range, existingFonts
588
- ).then((concated) => {
589
- callback(null, {data: concated});
590
- }, (err) => {
591
- callback(err, {data: null});
592
- });
1143
+ null,
1144
+ options.paths[protocol],
1145
+ fontstack,
1146
+ range,
1147
+ existingFonts,
1148
+ ).then(
1149
+ (concated) => {
1150
+ callback(null, { data: concated });
1151
+ },
1152
+ (err) => {
1153
+ callback(err, { data: null });
1154
+ },
1155
+ );
593
1156
  } else if (protocol === 'mbtiles') {
594
1157
  const parts = req.url.split('/');
595
1158
  const sourceId = parts[2];
@@ -601,8 +1164,13 @@ export const serve_rendered = {
601
1164
  const format = parts[5].split('.')[1];
602
1165
  source.getTile(z, x, y, (err, data, headers) => {
603
1166
  if (err) {
604
- if (options.verbose) console.log('MBTiles error, serving empty', err);
605
- createEmptyResponse(sourceInfo.format, sourceInfo.color, callback);
1167
+ if (options.verbose)
1168
+ console.log('MBTiles error, serving empty', err);
1169
+ createEmptyResponse(
1170
+ sourceInfo.format,
1171
+ sourceInfo.color,
1172
+ callback,
1173
+ );
606
1174
  return;
607
1175
  }
608
1176
 
@@ -615,11 +1183,23 @@ export const serve_rendered = {
615
1183
  try {
616
1184
  response.data = zlib.unzipSync(data);
617
1185
  } catch (err) {
618
- console.log('Skipping incorrect header for tile mbtiles://%s/%s/%s/%s.pbf', id, z, x, y);
1186
+ console.log(
1187
+ 'Skipping incorrect header for tile mbtiles://%s/%s/%s/%s.pbf',
1188
+ id,
1189
+ z,
1190
+ x,
1191
+ y,
1192
+ );
619
1193
  }
620
1194
  if (options.dataDecoratorFunc) {
621
1195
  response.data = options.dataDecoratorFunc(
622
- sourceId, 'data', response.data, z, x, y);
1196
+ sourceId,
1197
+ 'data',
1198
+ response.data,
1199
+ z,
1200
+ x,
1201
+ y,
1202
+ );
623
1203
  }
624
1204
  } else {
625
1205
  response.data = data;
@@ -628,36 +1208,39 @@ export const serve_rendered = {
628
1208
  callback(null, response);
629
1209
  });
630
1210
  } else if (protocol === 'http' || protocol === 'https') {
631
- request({
632
- url: req.url,
633
- encoding: null,
634
- gzip: true
635
- }, (err, res, body) => {
636
- const parts = url.parse(req.url);
637
- const extension = path.extname(parts.pathname).toLowerCase();
638
- const format = extensionToFormat[extension] || '';
639
- if (err || res.statusCode < 200 || res.statusCode >= 300) {
640
- // console.log('HTTP error', err || res.statusCode);
641
- createEmptyResponse(format, '', callback);
642
- return;
643
- }
1211
+ request(
1212
+ {
1213
+ url: req.url,
1214
+ encoding: null,
1215
+ gzip: true,
1216
+ },
1217
+ (err, res, body) => {
1218
+ const parts = url.parse(req.url);
1219
+ const extension = path.extname(parts.pathname).toLowerCase();
1220
+ const format = extensionToFormat[extension] || '';
1221
+ if (err || res.statusCode < 200 || res.statusCode >= 300) {
1222
+ // console.log('HTTP error', err || res.statusCode);
1223
+ createEmptyResponse(format, '', callback);
1224
+ return;
1225
+ }
644
1226
 
645
- const response = {};
646
- if (res.headers.modified) {
647
- response.modified = new Date(res.headers.modified);
648
- }
649
- if (res.headers.expires) {
650
- response.expires = new Date(res.headers.expires);
651
- }
652
- if (res.headers.etag) {
653
- response.etag = res.headers.etag;
654
- }
1227
+ const response = {};
1228
+ if (res.headers.modified) {
1229
+ response.modified = new Date(res.headers.modified);
1230
+ }
1231
+ if (res.headers.expires) {
1232
+ response.expires = new Date(res.headers.expires);
1233
+ }
1234
+ if (res.headers.etag) {
1235
+ response.etag = res.headers.etag;
1236
+ }
655
1237
 
656
- response.data = body;
657
- callback(null, response);
658
- });
1238
+ response.data = body;
1239
+ callback(null, response);
1240
+ },
1241
+ );
659
1242
  }
660
- }
1243
+ },
661
1244
  });
662
1245
  renderer.load(styleJSON);
663
1246
  createCallback(null, renderer);
@@ -668,7 +1251,7 @@ export const serve_rendered = {
668
1251
  create: createRenderer.bind(null, ratio),
669
1252
  destroy: (renderer) => {
670
1253
  renderer.release();
671
- }
1254
+ },
672
1255
  });
673
1256
  };
674
1257
 
@@ -682,16 +1265,20 @@ export const serve_rendered = {
682
1265
  }
683
1266
 
684
1267
  if (styleJSON.sprite && !httpTester.test(styleJSON.sprite)) {
685
- styleJSON.sprite = 'sprites://' +
1268
+ styleJSON.sprite =
1269
+ 'sprites://' +
686
1270
  styleJSON.sprite
687
- .replace('{style}', path.basename(styleFile, '.json'))
688
- .replace('{styleJsonFolder}', path.relative(options.paths.sprites, path.dirname(styleJSONPath)));
1271
+ .replace('{style}', path.basename(styleFile, '.json'))
1272
+ .replace(
1273
+ '{styleJsonFolder}',
1274
+ path.relative(options.paths.sprites, path.dirname(styleJSONPath)),
1275
+ );
689
1276
  }
690
1277
  if (styleJSON.glyphs && !httpTester.test(styleJSON.glyphs)) {
691
1278
  styleJSON.glyphs = `fonts://${styleJSON.glyphs}`;
692
1279
  }
693
1280
 
694
- for (const layer of (styleJSON.layers || [])) {
1281
+ for (const layer of styleJSON.layers || []) {
695
1282
  if (layer && layer.paint) {
696
1283
  // Remove (flatten) 3D buildings
697
1284
  if (layer.paint['fill-extrusion-height']) {
@@ -704,14 +1291,14 @@ export const serve_rendered = {
704
1291
  }
705
1292
 
706
1293
  const tileJSON = {
707
- 'tilejson': '2.0.0',
708
- 'name': styleJSON.name,
709
- 'attribution': '',
710
- 'minzoom': 0,
711
- 'maxzoom': 20,
712
- 'bounds': [-180, -85.0511, 180, 85.0511],
713
- 'format': 'png',
714
- 'type': 'baselayer'
1294
+ tilejson: '2.0.0',
1295
+ name: styleJSON.name,
1296
+ attribution: '',
1297
+ minzoom: 0,
1298
+ maxzoom: 20,
1299
+ bounds: [-180, -85.0511, 180, 85.0511],
1300
+ format: 'png',
1301
+ type: 'baselayer',
715
1302
  };
716
1303
  const attributionOverride = params.tilejson && params.tilejson.attribution;
717
1304
  Object.assign(tileJSON, params.tilejson || {});
@@ -724,7 +1311,7 @@ export const serve_rendered = {
724
1311
  map,
725
1312
  dataProjWGStoInternalWGS: null,
726
1313
  lastModified: new Date().toUTCString(),
727
- watermark: params.watermark || options.watermark
1314
+ watermark: params.watermark || options.watermark,
728
1315
  };
729
1316
  repo[id] = repoobj;
730
1317
 
@@ -738,8 +1325,8 @@ export const serve_rendered = {
738
1325
  delete source.url;
739
1326
 
740
1327
  let mbtilesFile = url.substring('mbtiles://'.length);
741
- const fromData = mbtilesFile[0] === '{' &&
742
- mbtilesFile[mbtilesFile.length - 1] === '}';
1328
+ const fromData =
1329
+ mbtilesFile[0] === '{' && mbtilesFile[mbtilesFile.length - 1] === '}';
743
1330
 
744
1331
  if (fromData) {
745
1332
  mbtilesFile = mbtilesFile.substr(1, mbtilesFile.length - 2);
@@ -754,52 +1341,58 @@ export const serve_rendered = {
754
1341
  }
755
1342
  }
756
1343
 
757
- queue.push(new Promise((resolve, reject) => {
758
- mbtilesFile = path.resolve(options.paths.mbtiles, mbtilesFile);
759
- const mbtilesFileStats = fs.statSync(mbtilesFile);
760
- if (!mbtilesFileStats.isFile() || mbtilesFileStats.size === 0) {
761
- throw Error(`Not valid MBTiles file: ${mbtilesFile}`);
762
- }
763
- map.sources[name] = new MBTiles(mbtilesFile + '?mode=ro', err => {
764
- map.sources[name].getInfo((err, info) => {
765
- if (err) {
766
- console.error(err);
767
- return;
768
- }
769
-
770
- if (!repoobj.dataProjWGStoInternalWGS && info.proj4) {
771
- // how to do this for multiple sources with different proj4 defs?
772
- const to3857 = proj4('EPSG:3857');
773
- const toDataProj = proj4(info.proj4);
774
- repoobj.dataProjWGStoInternalWGS = (xy) => to3857.inverse(toDataProj.forward(xy));
775
- }
776
-
777
- const type = source.type;
778
- Object.assign(source, info);
779
- source.type = type;
780
- source.tiles = [
781
- // meta url which will be detected when requested
782
- `mbtiles://${name}/{z}/{x}/{y}.${info.format || 'pbf'}`
783
- ];
784
- delete source.scheme;
785
-
786
- if (options.dataDecoratorFunc) {
787
- source = options.dataDecoratorFunc(name, 'tilejson', source);
788
- }
789
-
790
- if (!attributionOverride &&
791
- source.attribution && source.attribution.length > 0) {
792
- if (!tileJSON.attribution.includes(source.attribution)) {
793
- if (tileJSON.attribution.length > 0) {
794
- tileJSON.attribution += ' | ';
1344
+ queue.push(
1345
+ new Promise((resolve, reject) => {
1346
+ mbtilesFile = path.resolve(options.paths.mbtiles, mbtilesFile);
1347
+ const mbtilesFileStats = fs.statSync(mbtilesFile);
1348
+ if (!mbtilesFileStats.isFile() || mbtilesFileStats.size === 0) {
1349
+ throw Error(`Not valid MBTiles file: ${mbtilesFile}`);
1350
+ }
1351
+ map.sources[name] = new MBTiles(mbtilesFile + '?mode=ro', (err) => {
1352
+ map.sources[name].getInfo((err, info) => {
1353
+ if (err) {
1354
+ console.error(err);
1355
+ return;
1356
+ }
1357
+
1358
+ if (!repoobj.dataProjWGStoInternalWGS && info.proj4) {
1359
+ // how to do this for multiple sources with different proj4 defs?
1360
+ const to3857 = proj4('EPSG:3857');
1361
+ const toDataProj = proj4(info.proj4);
1362
+ repoobj.dataProjWGStoInternalWGS = (xy) =>
1363
+ to3857.inverse(toDataProj.forward(xy));
1364
+ }
1365
+
1366
+ const type = source.type;
1367
+ Object.assign(source, info);
1368
+ source.type = type;
1369
+ source.tiles = [
1370
+ // meta url which will be detected when requested
1371
+ `mbtiles://${name}/{z}/{x}/{y}.${info.format || 'pbf'}`,
1372
+ ];
1373
+ delete source.scheme;
1374
+
1375
+ if (options.dataDecoratorFunc) {
1376
+ source = options.dataDecoratorFunc(name, 'tilejson', source);
1377
+ }
1378
+
1379
+ if (
1380
+ !attributionOverride &&
1381
+ source.attribution &&
1382
+ source.attribution.length > 0
1383
+ ) {
1384
+ if (!tileJSON.attribution.includes(source.attribution)) {
1385
+ if (tileJSON.attribution.length > 0) {
1386
+ tileJSON.attribution += ' | ';
1387
+ }
1388
+ tileJSON.attribution += source.attribution;
795
1389
  }
796
- tileJSON.attribution += source.attribution;
797
1390
  }
798
- }
799
- resolve();
1391
+ resolve();
1392
+ });
800
1393
  });
801
- });
802
- }));
1394
+ }),
1395
+ );
803
1396
  }
804
1397
  }
805
1398
 
@@ -813,7 +1406,12 @@ export const serve_rendered = {
813
1406
  const minPoolSize = minPoolSizes[i];
814
1407
  const maxPoolSize = Math.max(minPoolSize, maxPoolSizes[j]);
815
1408
  map.renderers[s] = createPool(s, 'tile', minPoolSize, maxPoolSize);
816
- map.renderers_static[s] = createPool(s, 'static', minPoolSize, maxPoolSize);
1409
+ map.renderers_static[s] = createPool(
1410
+ s,
1411
+ 'static',
1412
+ minPoolSize,
1413
+ maxPoolSize,
1414
+ );
817
1415
  }
818
1416
  });
819
1417
 
@@ -830,5 +1428,5 @@ export const serve_rendered = {
830
1428
  });
831
1429
  }
832
1430
  delete repo[id];
833
- }
1431
+ },
834
1432
  };