tileserver-gl-light 4.5.0 → 4.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,11 +13,11 @@ jobs:
13
13
  steps:
14
14
  - name: Check out repository ✨ (non-dependabot)
15
15
  if: ${{ github.actor != 'dependabot[bot]' }}
16
- uses: actions/checkout@v3
16
+ uses: actions/checkout@v4
17
17
 
18
18
  - name: Check out repository 🎉 (dependabot)
19
19
  if: ${{ github.actor == 'dependabot[bot]' }}
20
- uses: actions/checkout@v3
20
+ uses: actions/checkout@v4
21
21
  with:
22
22
  ref: ${{ github.event.pull_request.head.sha }}
23
23
 
@@ -23,7 +23,7 @@ jobs:
23
23
  language: [javascript]
24
24
  steps:
25
25
  - name: Checkout
26
- uses: actions/checkout@v3
26
+ uses: actions/checkout@v4
27
27
  - name: Initialize CodeQL
28
28
  uses: github/codeql-action/init@v2
29
29
  with:
@@ -13,11 +13,11 @@ jobs:
13
13
  steps:
14
14
  - name: Check out repository ✨ (non-dependabot)
15
15
  if: ${{ github.actor != 'dependabot[bot]' }}
16
- uses: actions/checkout@v3
16
+ uses: actions/checkout@v4
17
17
 
18
18
  - name: Check out repository 🎉 (dependabot)
19
19
  if: ${{ github.actor == 'dependabot[bot]' }}
20
- uses: actions/checkout@v3
20
+ uses: actions/checkout@v4
21
21
  with:
22
22
  ref: ${{ github.event.pull_request.head.sha }}
23
23
 
@@ -19,7 +19,7 @@ jobs:
19
19
  runs-on: ubuntu-20.04
20
20
  steps:
21
21
  - name: Check out repository ✨
22
- uses: actions/checkout@v3
22
+ uses: actions/checkout@v4
23
23
 
24
24
  - name: Update apt-get 🚀
25
25
  run: sudo apt-get update -qq
@@ -0,0 +1,18 @@
1
+ # Read the Docs configuration file for Sphinx projects
2
+ # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
3
+
4
+ # Required
5
+ version: 2
6
+
7
+ # Set the version of Python and other tools you might need
8
+ build:
9
+ os: ubuntu-22.04
10
+ tools:
11
+ python: "3.11"
12
+
13
+ # Build documentation in the doc/help/ directory with Sphinx
14
+ sphinx:
15
+ configuration: docs/conf.py
16
+
17
+ formats:
18
+ - pdf
package/docs/config.rst CHANGED
@@ -33,6 +33,8 @@ Example:
33
33
  "serveAllStyles": false,
34
34
  "serveStaticMaps": true,
35
35
  "allowRemoteMarkerIcons": true,
36
+ "allowInlineMarkerImages": true,
37
+ "staticAttributionText": "© OpenMapTiles © OpenStreetMaps",
36
38
  "tileMargin": 0
37
39
  },
38
40
  "styles": {
@@ -140,7 +142,13 @@ It is recommended to also use the ``serveAllFonts`` option when using this optio
140
142
  -----------
141
143
 
142
144
  Optional string to be rendered into the raster tiles (and static maps) as watermark (bottom-left corner).
143
- Can be used for hard-coding attributions etc. (can also be specified per-style).
145
+ Not used by default.
146
+
147
+ ``staticAttributionText``
148
+ -----------
149
+
150
+ Optional string to be rendered in the static images endpoint. Text will be rendered in the bottom-right corner,
151
+ and styled similar to attribution on web-based maps (text only, links not supported).
144
152
  Not used by default.
145
153
 
146
154
  ``allowRemoteMarkerIcons``
@@ -150,6 +158,13 @@ Allows the rendering of marker icons fetched via http(s) hyperlinks.
150
158
  For security reasons only allow this if you can control the origins from where the markers are fetched!
151
159
  Default is to disallow fetching of icons from remote sources.
152
160
 
161
+ ``allowInlineMarkerImages``
162
+ --------------
163
+ Allows the rendering of inline marker icons or base64 urls.
164
+ For security reasons only allow this if you can control the origins from where the markers are fetched!
165
+ Not used by default.
166
+
167
+
153
168
  ``styles``
154
169
  ==========
155
170
 
@@ -21,7 +21,7 @@ Endpoint data can be configured to be cached by Cloudflare. For example to cache
21
21
 
22
22
  Create a rule which matches ``hostname (equal)`` and ``URI Path (ends with)`` for ``.pbf`` and ``.json`` fields. Set cache status to eligible for cache to enable the caching and overwrite the ``Edge TTL`` with ``Browser TTL`` to be 7 days (depends on your application usage).
23
23
 
24
- This will ensure that Cloudflare will cache your tiles on Cloudflare side for seven days aswell on the client side. If the tileserver is down or user has no internet access it will try to use cached tiles from Cloudflare or local.
24
+ This will ensure that your tiles are cached on the client side and by Cloudflare for seven days. If the tileserver is down or user has no internet access it will try to use cached tiles.
25
25
 
26
26
  Note that ``Browser TTL`` will overwrite expiration dates on the client device. If you rebuild your maps, old tiles will be rendered until it expires or cache is cleared on the client device.
27
27
 
@@ -115,7 +115,7 @@ An example nginx reverse proxy server configuration for HTTPS connections. It en
115
115
 
116
116
  location / {
117
117
  # This include directive sets up required headers for proxy and proxy cache.
118
- # Aswell it includes the required ``X-Forwarded-*`` headers for tileserver to propely generate tiles.
118
+ # As well it includes the required ``X-Forwarded-*`` headers for tileserver to properly generate tiles.
119
119
  include proxy_params;
120
120
 
121
121
  proxy_pass http://127.0.0.1:8080/;
@@ -35,12 +35,22 @@ Static images
35
35
 
36
36
  * All the static image endpoints additionally support following query parameters:
37
37
 
38
- * ``path`` - comma-separated ``lng,lat``, pipe-separated pairs
38
+ * ``path`` - ``((fill|stroke|width)\:[^\|]+\|)*(enc:.+|-?\d+(\.\d*)?,-?\d+(\.\d*)?(\|-?\d+(\.\d*)?,-?\d+(\.\d*)?)+)``
39
+
40
+ * comma-separated ``lng,lat``, pipe-separated pairs
41
+
42
+ * e.g. ``path=5.9,45.8|5.9,47.8|10.5,47.8|10.5,45.8|5.9,45.8``
43
+
44
+ * `Google Encoded Polyline Format <https://developers.google.com/maps/documentation/utilities/polylinealgorithm>`_
45
+
46
+ * e.g. ``path=enc:_p~iF~ps|U_ulLnnqC_mqNvxq`@``
47
+ * If 'enc:' is used, the rest of the path parameter is considered to be part of the encoded polyline string -- do not specify the coordinate pairs.
48
+
49
+ * With options (fill|stroke|width)
50
+
51
+ * e.g. ``path=stroke:yellow|width:2|fill:green|5.9,45.8|5.9,47.8|10.5,47.8|10.5,45.8|5.9,45.8`` or ``path=stroke:blue|width:1|fill:yellow|enc:_p~iF~ps|U_ulLnnqC_mqNvxq`@``
39
52
 
40
- * e.g. ``5.9,45.8|5.9,47.8|10.5,47.8|10.5,45.8|5.9,45.8``
41
53
  * can be provided multiple times
42
- * or pass the path as per [Maptiler Cloud API](https://docs.maptiler.com/cloud/api/static-maps/)
43
- * Match pattern: ((fill|stroke|width)\:[^\|]+\|)*((enc:.+)|((-?\d+\.?\d*,-?\d+\.?\d*\|)+(-?\d+\.?\d*,-?\d+\.?\d*)))
44
54
 
45
55
  * ``latlng`` - indicates coordinates are in ``lat,lng`` order rather than the usual ``lng,lat``
46
56
  * ``fill`` - color to use as the fill (e.g. ``red``, ``rgba(255,255,255,0.5)``, ``#0000ff``)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tileserver-gl-light",
3
- "version": "4.5.0",
3
+ "version": "4.5.2",
4
4
  "description": "Map tile server for JSON GL styles - serving vector tiles",
5
5
  "main": "src/main.js",
6
6
  "bin": "src/main.js",
@@ -19,7 +19,7 @@
19
19
  "dependencies": {
20
20
  "@mapbox/glyph-pbf-composite": "0.0.3",
21
21
  "@mapbox/mbtiles": "0.12.1",
22
- "@mapbox/polyline": "^1.2.0",
22
+ "@mapbox/polyline": "^1.2.1",
23
23
  "@mapbox/sphericalmercator": "1.2.0",
24
24
  "@mapbox/vector-tile": "1.3.1",
25
25
  "@maplibre/maplibre-gl-style-spec": "18.0.0",
@@ -27,14 +27,14 @@
27
27
  "chokidar": "3.5.3",
28
28
  "clone": "2.1.2",
29
29
  "color": "4.2.3",
30
- "commander": "11.0.0",
30
+ "commander": "11.1.0",
31
31
  "cors": "2.8.5",
32
32
  "express": "4.18.2",
33
33
  "handlebars": "4.7.8",
34
34
  "http-shutdown": "1.2.2",
35
35
  "morgan": "1.10.0",
36
36
  "pbf": "3.2.1",
37
- "proj4": "2.9.0",
37
+ "proj4": "2.9.1",
38
38
  "request": "2.88.2",
39
39
  "sanitize-filename": "1.6.3",
40
40
  "tileserver-gl-styles": "2.0.0"
@@ -22,8 +22,8 @@ import { getFontsPbf, getTileUrls, fixTileJSONCenter } from './utils.js';
22
22
 
23
23
  const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+.?\\d+)';
24
24
  const PATH_PATTERN =
25
- /^((fill|stroke|width)\:[^\|]+\|)*((enc:.+)|((-?\d+\.?\d*,-?\d+\.?\d*\|)+(-?\d+\.?\d*,-?\d+\.?\d*)))/;
26
- const httpTester = /^(http(s)?:)?\/\//;
25
+ /^((fill|stroke|width)\:[^\|]+\|)*(enc:.+|-?\d+(\.\d*)?,-?\d+(\.\d*)?(\|-?\d+(\.\d*)?,-?\d+(\.\d*)?)+)/;
26
+ const httpTester = /^\/\//;
27
27
 
28
28
  const mercator = new SphericalMercator();
29
29
  const getScale = (scale) => (scale || '@1x').slice(1, 2) | 0;
@@ -54,7 +54,6 @@ const cachedEmptyResponses = {
54
54
 
55
55
  /**
56
56
  * Create an appropriate mlgl response for http errors.
57
- *
58
57
  * @param {string} format The format (a sharp format or 'pbf').
59
58
  * @param {string} color The background color (or empty string for transparent).
60
59
  * @param {Function} callback The mlgl callback.
@@ -102,7 +101,6 @@ function createEmptyResponse(format, color, callback) {
102
101
  /**
103
102
  * Parses coordinate pair provided to pair of floats and ensures the resulting
104
103
  * pair is a longitude/latitude combination depending on lnglat query parameter.
105
- *
106
104
  * @param {List} coordinatePair Coordinate pair.
107
105
  * @param coordinates
108
106
  * @param {object} query Request query parameters.
@@ -127,7 +125,6 @@ const parseCoordinatePair = (coordinates, query) => {
127
125
 
128
126
  /**
129
127
  * Parses a coordinate pair from query arguments and optionally transforms it.
130
- *
131
128
  * @param {List} coordinatePair Coordinate pair.
132
129
  * @param {object} query Request query parameters.
133
130
  * @param {Function} transformer Optional transform function.
@@ -145,7 +142,6 @@ const parseCoordinates = (coordinatePair, query, transformer) => {
145
142
 
146
143
  /**
147
144
  * Parses paths provided via query into a list of path objects.
148
- *
149
145
  * @param {object} query Request query parameters.
150
146
  * @param {Function} transformer Optional transform function.
151
147
  */
@@ -162,25 +158,13 @@ const extractPathsFromQuery = (query, transformer) => {
162
158
  // Iterate through paths, parse and validate them
163
159
  for (const providedPath of providedPaths) {
164
160
  // Logic for pushing coords to path when path includes google polyline
165
- if (
166
- providedPath.includes('enc:') &&
167
- PATH_PATTERN.test(decodeURIComponent(providedPath))
168
- ) {
169
- const encodedPaths = providedPath.split(',');
170
- for (const path of encodedPaths) {
171
- const line = path
172
- .split('|')
173
- .filter(
174
- (x) =>
175
- !x.startsWith('fill') &&
176
- !x.startsWith('stroke') &&
177
- !x.startsWith('width'),
178
- )
179
- .join('')
180
- .replace('enc:', '');
181
- const coords = polyline.decode(line).map(([lat, lng]) => [lng, lat]);
182
- paths.push(coords);
183
- }
161
+ if (providedPath.includes('enc:') && PATH_PATTERN.test(providedPath)) {
162
+ // +4 because 'enc:' is 4 characters, everything after 'enc:' is considered to be part of the polyline
163
+ const encIndex = providedPath.indexOf('enc:') + 4;
164
+ const coords = polyline
165
+ .decode(providedPath.substring(encIndex))
166
+ .map(([lat, lng]) => [lng, lat]);
167
+ paths.push(coords);
184
168
  } else {
185
169
  // Iterate through paths, parse and validate them
186
170
  const currentPath = [];
@@ -220,7 +204,6 @@ const extractPathsFromQuery = (query, transformer) => {
220
204
  * on marker object.
221
205
  * Options adhere to the following format
222
206
  * [optionName]:[optionValue]
223
- *
224
207
  * @param {List[String]} optionsList List of option strings.
225
208
  * @param {object} marker Marker object to configure.
226
209
  */
@@ -255,7 +238,6 @@ const parseMarkerOptions = (optionsList, marker) => {
255
238
 
256
239
  /**
257
240
  * Parses markers provided via query into a list of marker objects.
258
- *
259
241
  * @param {object} query Request query parameters.
260
242
  * @param {object} options Configuration options.
261
243
  * @param {Function} transformer Optional transform function.
@@ -294,7 +276,10 @@ const extractMarkersFromQuery = (query, options, transformer) => {
294
276
  let iconURI = markerParts[1];
295
277
  // Check if icon is served via http otherwise marker icons are expected to
296
278
  // be provided as filepaths relative to configured icon path
297
- if (!(iconURI.startsWith('http://') || iconURI.startsWith('https://'))) {
279
+ const isRemoteURL =
280
+ iconURI.startsWith('http://') || iconURI.startsWith('https://');
281
+ const isDataURL = iconURI.startsWith('data:');
282
+ if (!(isRemoteURL || isDataURL)) {
298
283
  // Sanitize URI with sanitize-filename
299
284
  // https://www.npmjs.com/package/sanitize-filename#details
300
285
  iconURI = sanitize(iconURI);
@@ -307,7 +292,9 @@ const extractMarkersFromQuery = (query, options, transformer) => {
307
292
  iconURI = path.resolve(options.paths.icons, iconURI);
308
293
 
309
294
  // When we encounter a remote icon check if the configuration explicitly allows them.
310
- } else if (options.allowRemoteMarkerIcons !== true) {
295
+ } else if (isRemoteURL && options.allowRemoteMarkerIcons !== true) {
296
+ continue;
297
+ } else if (isDataURL && options.allowInlineMarkerImages !== true) {
311
298
  continue;
312
299
  }
313
300
 
@@ -335,7 +322,6 @@ const extractMarkersFromQuery = (query, options, transformer) => {
335
322
 
336
323
  /**
337
324
  * Transforms coordinates to pixels.
338
- *
339
325
  * @param {List[Number]} ll Longitude/Latitude coordinate pair.
340
326
  * @param {number} zoom Map zoom level.
341
327
  */
@@ -347,7 +333,6 @@ const precisePx = (ll, zoom) => {
347
333
 
348
334
  /**
349
335
  * Draws a marker in cavans context.
350
- *
351
336
  * @param {object} ctx Canvas context object.
352
337
  * @param {object} marker Marker object parsed by extractMarkersFromQuery.
353
338
  * @param {number} z Map zoom level.
@@ -419,7 +404,6 @@ const drawMarker = (ctx, marker, z) => {
419
404
  * Wraps drawing of markers into list of promises and awaits them.
420
405
  * It's required because images are expected to load asynchronous in canvas js
421
406
  * even when provided from a local disk.
422
- *
423
407
  * @param {object} ctx Canvas context object.
424
408
  * @param {List[Object]} markers Marker objects parsed by extractMarkersFromQuery.
425
409
  * @param {number} z Map zoom level.
@@ -438,117 +422,107 @@ const drawMarkers = async (ctx, markers, z) => {
438
422
 
439
423
  /**
440
424
  * Draws a list of coordinates onto a canvas and styles the resulting path.
441
- *
442
425
  * @param {object} ctx Canvas context object.
443
426
  * @param {List[Number]} path List of coordinates.
444
427
  * @param {object} query Request query parameters.
428
+ * @param {string} pathQuery Path query parameter.
445
429
  * @param {number} z Map zoom level.
446
430
  */
447
- const drawPath = (ctx, path, query, z) => {
448
- const renderPath = (splitPaths) => {
449
- if (!path || path.length < 2) {
450
- return null;
451
- }
431
+ const drawPath = (ctx, path, query, pathQuery, z) => {
432
+ const splitPaths = pathQuery.split('|');
452
433
 
453
- ctx.beginPath();
434
+ if (!path || path.length < 2) {
435
+ return null;
436
+ }
454
437
 
455
- // Transform coordinates to pixel on canvas and draw lines between points
456
- for (const pair of path) {
457
- const px = precisePx(pair, z);
458
- ctx.lineTo(px[0], px[1]);
459
- }
438
+ ctx.beginPath();
460
439
 
461
- // Check if first coordinate matches last coordinate
462
- if (
463
- path[0][0] === path[path.length - 1][0] &&
464
- path[0][1] === path[path.length - 1][1]
465
- ) {
466
- ctx.closePath();
467
- }
440
+ // Transform coordinates to pixel on canvas and draw lines between points
441
+ for (const pair of path) {
442
+ const px = precisePx(pair, z);
443
+ ctx.lineTo(px[0], px[1]);
444
+ }
468
445
 
469
- // Optionally fill drawn shape with a rgba color from query
470
- const pathHasFill =
471
- splitPaths.filter((x) => x.startsWith('fill')).length > 0;
472
- if (query.fill !== undefined || pathHasFill) {
473
- if ('fill' in query) {
474
- ctx.fillStyle = query.fill || 'rgba(255,255,255,0.4)';
475
- }
476
- if (pathHasFill) {
477
- ctx.fillStyle = splitPaths
478
- .find((x) => x.startsWith('fill:'))
479
- .replace('fill:', '');
480
- }
481
- ctx.fill();
482
- }
446
+ // Check if first coordinate matches last coordinate
447
+ if (
448
+ path[0][0] === path[path.length - 1][0] &&
449
+ path[0][1] === path[path.length - 1][1]
450
+ ) {
451
+ ctx.closePath();
452
+ }
483
453
 
484
- // Get line width from query and fall back to 1 if not provided
485
- const pathHasWidth =
486
- splitPaths.filter((x) => x.startsWith('width')).length > 0;
487
- if (query.width !== undefined || pathHasWidth) {
488
- let lineWidth = 1;
489
- // Get line width from query
490
- if ('width' in query) {
491
- lineWidth = Number(query.width);
492
- }
493
- // Get line width from path in query
494
- if (pathHasWidth) {
495
- lineWidth = Number(
496
- splitPaths.find((x) => x.startsWith('width:')).replace('width:', ''),
497
- );
498
- }
499
- // Get border width from query and fall back to 10% of line width
500
- const borderWidth =
501
- query.borderwidth !== undefined
502
- ? parseFloat(query.borderwidth)
503
- : lineWidth * 0.1;
504
-
505
- // Set rendering style for the start and end points of the path
506
- // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap
507
- ctx.lineCap = query.linecap || 'butt';
508
-
509
- // Set rendering style for overlapping segments of the path with differing directions
510
- // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin
511
- ctx.lineJoin = query.linejoin || 'miter';
512
-
513
- // In order to simulate a border we draw the path two times with the first
514
- // beeing the wider border part.
515
- if (query.border !== undefined && borderWidth > 0) {
516
- // We need to double the desired border width and add it to the line width
517
- // in order to get the desired border on each side of the line.
518
- ctx.lineWidth = lineWidth + borderWidth * 2;
519
- // Set border style as rgba
520
- ctx.strokeStyle = query.border;
521
- ctx.stroke();
522
- }
523
- ctx.lineWidth = lineWidth;
454
+ // Optionally fill drawn shape with a rgba color from query
455
+ const pathHasFill = splitPaths.filter((x) => x.startsWith('fill')).length > 0;
456
+ if (query.fill !== undefined || pathHasFill) {
457
+ if ('fill' in query) {
458
+ ctx.fillStyle = query.fill || 'rgba(255,255,255,0.4)';
459
+ }
460
+ if (pathHasFill) {
461
+ ctx.fillStyle = splitPaths
462
+ .find((x) => x.startsWith('fill:'))
463
+ .replace('fill:', '');
524
464
  }
465
+ ctx.fill();
466
+ }
525
467
 
526
- const pathHasStroke =
527
- splitPaths.filter((x) => x.startsWith('stroke')).length > 0;
528
- if (query.stroke !== undefined || pathHasStroke) {
529
- if ('stroke' in query) {
530
- ctx.strokeStyle = query.stroke;
531
- }
532
- // Path Width gets higher priority
533
- if (pathHasWidth) {
534
- ctx.strokeStyle = splitPaths
535
- .find((x) => x.startsWith('stroke:'))
536
- .replace('stroke:', '');
537
- }
538
- } else {
539
- ctx.strokeStyle = 'rgba(0,64,255,0.7)';
468
+ // Get line width from query and fall back to 1 if not provided
469
+ const pathHasWidth =
470
+ splitPaths.filter((x) => x.startsWith('width')).length > 0;
471
+ if (query.width !== undefined || pathHasWidth) {
472
+ let lineWidth = 1;
473
+ // Get line width from query
474
+ if ('width' in query) {
475
+ lineWidth = Number(query.width);
476
+ }
477
+ // Get line width from path in query
478
+ if (pathHasWidth) {
479
+ lineWidth = Number(
480
+ splitPaths.find((x) => x.startsWith('width:')).replace('width:', ''),
481
+ );
540
482
  }
541
- ctx.stroke();
542
- };
483
+ // Get border width from query and fall back to 10% of line width
484
+ const borderWidth =
485
+ query.borderwidth !== undefined
486
+ ? parseFloat(query.borderwidth)
487
+ : lineWidth * 0.1;
488
+
489
+ // Set rendering style for the start and end points of the path
490
+ // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap
491
+ ctx.lineCap = query.linecap || 'butt';
492
+
493
+ // Set rendering style for overlapping segments of the path with differing directions
494
+ // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin
495
+ ctx.lineJoin = query.linejoin || 'miter';
496
+
497
+ // In order to simulate a border we draw the path two times with the first
498
+ // beeing the wider border part.
499
+ if (query.border !== undefined && borderWidth > 0) {
500
+ // We need to double the desired border width and add it to the line width
501
+ // in order to get the desired border on each side of the line.
502
+ ctx.lineWidth = lineWidth + borderWidth * 2;
503
+ // Set border style as rgba
504
+ ctx.strokeStyle = query.border;
505
+ ctx.stroke();
506
+ }
507
+ ctx.lineWidth = lineWidth;
508
+ }
543
509
 
544
- // Check if path in query is valid
545
- if (Array.isArray(query.path)) {
546
- for (let i = 0; i < query.path.length; i += 1) {
547
- renderPath(decodeURIComponent(query.path.at(i)).split('|'));
510
+ const pathHasStroke =
511
+ splitPaths.filter((x) => x.startsWith('stroke')).length > 0;
512
+ if (query.stroke !== undefined || pathHasStroke) {
513
+ if ('stroke' in query) {
514
+ ctx.strokeStyle = query.stroke;
515
+ }
516
+ // Path Stroke gets higher priority
517
+ if (pathHasStroke) {
518
+ ctx.strokeStyle = splitPaths
519
+ .find((x) => x.startsWith('stroke:'))
520
+ .replace('stroke:', '');
548
521
  }
549
522
  } else {
550
- renderPath(decodeURIComponent(query.path).split('|'));
523
+ ctx.strokeStyle = 'rgba(0,64,255,0.7)';
551
524
  }
525
+ ctx.stroke();
552
526
  };
553
527
 
554
528
  const renderOverlay = async (
@@ -592,9 +566,10 @@ const renderOverlay = async (
592
566
  }
593
567
 
594
568
  // Draw provided paths if any
595
- for (const path of paths) {
596
- drawPath(ctx, path, query, z);
597
- }
569
+ paths.forEach((path, i) => {
570
+ const pathQuery = Array.isArray(query.path) ? query.path.at(i) : query.path;
571
+ drawPath(ctx, path, query, pathQuery, z);
572
+ });
598
573
 
599
574
  // Await drawing of markers before rendering the canvas
600
575
  await drawMarkers(ctx, markers, z);
@@ -798,6 +773,35 @@ export const serve_rendered = {
798
773
  composite_array.push({ input: canvas.toBuffer() });
799
774
  }
800
775
 
776
+ if (opt_mode === 'static' && item.staticAttributionText) {
777
+ const canvas = createCanvas(scale * width, scale * height);
778
+ const ctx = canvas.getContext('2d');
779
+ ctx.scale(scale, scale);
780
+
781
+ ctx.font = '10px sans-serif';
782
+ const text = item.staticAttributionText;
783
+ const textMetrics = ctx.measureText(text);
784
+ const textWidth = textMetrics.width;
785
+ const textHeight = 14;
786
+
787
+ const padding = 6;
788
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
789
+ ctx.fillRect(
790
+ width - textWidth - padding,
791
+ height - textHeight - padding,
792
+ textWidth + padding,
793
+ textHeight + padding,
794
+ );
795
+ ctx.fillStyle = 'rgba(0,0,0,.8)';
796
+ ctx.fillText(
797
+ item.staticAttributionText,
798
+ width - textWidth - padding / 2,
799
+ height - textHeight + 8,
800
+ );
801
+
802
+ composite_array.push({ input: canvas.toBuffer() });
803
+ }
804
+
801
805
  if (composite_array.length > 0) {
802
806
  image.composite(composite_array);
803
807
  }
@@ -897,35 +901,118 @@ export const serve_rendered = {
897
901
  app.get(
898
902
  util.format(staticPattern, centerPattern),
899
903
  async (req, res, next) => {
904
+ try {
905
+ const item = repo[req.params.id];
906
+ if (!item) {
907
+ return res.sendStatus(404);
908
+ }
909
+ const raw = req.params.raw;
910
+ const z = +req.params.z;
911
+ let x = +req.params.x;
912
+ let y = +req.params.y;
913
+ const bearing = +(req.params.bearing || '0');
914
+ const pitch = +(req.params.pitch || '0');
915
+ const w = req.params.width | 0;
916
+ const h = req.params.height | 0;
917
+ const scale = getScale(req.params.scale);
918
+ const format = req.params.format;
919
+
920
+ if (z < 0) {
921
+ return res.status(404).send('Invalid zoom');
922
+ }
923
+
924
+ const transformer = raw
925
+ ? mercator.inverse.bind(mercator)
926
+ : item.dataProjWGStoInternalWGS;
927
+
928
+ if (transformer) {
929
+ const ll = transformer([x, y]);
930
+ x = ll[0];
931
+ y = ll[1];
932
+ }
933
+
934
+ const paths = extractPathsFromQuery(req.query, transformer);
935
+ const markers = extractMarkersFromQuery(
936
+ req.query,
937
+ options,
938
+ transformer,
939
+ );
940
+ const overlay = await renderOverlay(
941
+ z,
942
+ x,
943
+ y,
944
+ bearing,
945
+ pitch,
946
+ w,
947
+ h,
948
+ scale,
949
+ paths,
950
+ markers,
951
+ req.query,
952
+ );
953
+
954
+ return respondImage(
955
+ item,
956
+ z,
957
+ x,
958
+ y,
959
+ bearing,
960
+ pitch,
961
+ w,
962
+ h,
963
+ scale,
964
+ format,
965
+ res,
966
+ next,
967
+ overlay,
968
+ 'static',
969
+ );
970
+ } catch (e) {
971
+ next(e);
972
+ }
973
+ },
974
+ );
975
+
976
+ const serveBounds = async (req, res, next) => {
977
+ try {
900
978
  const item = repo[req.params.id];
901
979
  if (!item) {
902
980
  return res.sendStatus(404);
903
981
  }
904
982
  const raw = req.params.raw;
905
- const z = +req.params.z;
906
- let x = +req.params.x;
907
- let y = +req.params.y;
908
- const bearing = +(req.params.bearing || '0');
909
- const pitch = +(req.params.pitch || '0');
910
- const w = req.params.width | 0;
911
- const h = req.params.height | 0;
912
- const scale = getScale(req.params.scale);
913
- const format = req.params.format;
914
-
915
- if (z < 0) {
916
- return res.status(404).send('Invalid zoom');
917
- }
983
+ const bbox = [
984
+ +req.params.minx,
985
+ +req.params.miny,
986
+ +req.params.maxx,
987
+ +req.params.maxy,
988
+ ];
989
+ let center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2];
918
990
 
919
991
  const transformer = raw
920
992
  ? mercator.inverse.bind(mercator)
921
993
  : item.dataProjWGStoInternalWGS;
922
994
 
923
995
  if (transformer) {
924
- const ll = transformer([x, y]);
925
- x = ll[0];
926
- y = ll[1];
996
+ const minCorner = transformer(bbox.slice(0, 2));
997
+ const maxCorner = transformer(bbox.slice(2));
998
+ bbox[0] = minCorner[0];
999
+ bbox[1] = minCorner[1];
1000
+ bbox[2] = maxCorner[0];
1001
+ bbox[3] = maxCorner[1];
1002
+ center = transformer(center);
927
1003
  }
928
1004
 
1005
+ const w = req.params.width | 0;
1006
+ const h = req.params.height | 0;
1007
+ const scale = getScale(req.params.scale);
1008
+ const format = req.params.format;
1009
+
1010
+ const z = calcZForBBox(bbox, w, h, req.query);
1011
+ const x = center[0];
1012
+ const y = center[1];
1013
+ const bearing = 0;
1014
+ const pitch = 0;
1015
+
929
1016
  const paths = extractPathsFromQuery(req.query, transformer);
930
1017
  const markers = extractMarkersFromQuery(
931
1018
  req.query,
@@ -945,7 +1032,6 @@ export const serve_rendered = {
945
1032
  markers,
946
1033
  req.query,
947
1034
  );
948
-
949
1035
  return respondImage(
950
1036
  item,
951
1037
  z,
@@ -962,83 +1048,9 @@ export const serve_rendered = {
962
1048
  overlay,
963
1049
  'static',
964
1050
  );
965
- },
966
- );
967
-
968
- const serveBounds = async (req, res, next) => {
969
- const item = repo[req.params.id];
970
- if (!item) {
971
- return res.sendStatus(404);
1051
+ } catch (e) {
1052
+ next(e);
972
1053
  }
973
- const raw = req.params.raw;
974
- const bbox = [
975
- +req.params.minx,
976
- +req.params.miny,
977
- +req.params.maxx,
978
- +req.params.maxy,
979
- ];
980
- let center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2];
981
-
982
- const transformer = raw
983
- ? mercator.inverse.bind(mercator)
984
- : item.dataProjWGStoInternalWGS;
985
-
986
- if (transformer) {
987
- const minCorner = transformer(bbox.slice(0, 2));
988
- const maxCorner = transformer(bbox.slice(2));
989
- bbox[0] = minCorner[0];
990
- bbox[1] = minCorner[1];
991
- bbox[2] = maxCorner[0];
992
- bbox[3] = maxCorner[1];
993
- center = transformer(center);
994
- }
995
-
996
- const w = req.params.width | 0;
997
- const h = req.params.height | 0;
998
- const scale = getScale(req.params.scale);
999
- const format = req.params.format;
1000
-
1001
- const z = calcZForBBox(bbox, w, h, req.query);
1002
- const x = center[0];
1003
- const y = center[1];
1004
- const bearing = 0;
1005
- const pitch = 0;
1006
-
1007
- const paths = extractPathsFromQuery(req.query, transformer);
1008
- const markers = extractMarkersFromQuery(
1009
- req.query,
1010
- options,
1011
- transformer,
1012
- );
1013
- const overlay = await renderOverlay(
1014
- z,
1015
- x,
1016
- y,
1017
- bearing,
1018
- pitch,
1019
- w,
1020
- h,
1021
- scale,
1022
- paths,
1023
- markers,
1024
- req.query,
1025
- );
1026
- return respondImage(
1027
- item,
1028
- z,
1029
- x,
1030
- y,
1031
- bearing,
1032
- pitch,
1033
- w,
1034
- h,
1035
- scale,
1036
- format,
1037
- res,
1038
- next,
1039
- overlay,
1040
- 'static',
1041
- );
1042
1054
  };
1043
1055
 
1044
1056
  const boundsPattern = util.format(
@@ -1078,97 +1090,101 @@ export const serve_rendered = {
1078
1090
  app.get(
1079
1091
  util.format(staticPattern, autoPattern),
1080
1092
  async (req, res, next) => {
1081
- const item = repo[req.params.id];
1082
- if (!item) {
1083
- return res.sendStatus(404);
1084
- }
1085
- const raw = req.params.raw;
1086
- const w = req.params.width | 0;
1087
- const h = req.params.height | 0;
1088
- const bearing = 0;
1089
- const pitch = 0;
1090
- const scale = getScale(req.params.scale);
1091
- const format = req.params.format;
1093
+ try {
1094
+ const item = repo[req.params.id];
1095
+ if (!item) {
1096
+ return res.sendStatus(404);
1097
+ }
1098
+ const raw = req.params.raw;
1099
+ const w = req.params.width | 0;
1100
+ const h = req.params.height | 0;
1101
+ const bearing = 0;
1102
+ const pitch = 0;
1103
+ const scale = getScale(req.params.scale);
1104
+ const format = req.params.format;
1105
+
1106
+ const transformer = raw
1107
+ ? mercator.inverse.bind(mercator)
1108
+ : item.dataProjWGStoInternalWGS;
1109
+
1110
+ const paths = extractPathsFromQuery(req.query, transformer);
1111
+ const markers = extractMarkersFromQuery(
1112
+ req.query,
1113
+ options,
1114
+ transformer,
1115
+ );
1092
1116
 
1093
- const transformer = raw
1094
- ? mercator.inverse.bind(mercator)
1095
- : item.dataProjWGStoInternalWGS;
1117
+ // Extract coordinates from markers
1118
+ const markerCoordinates = [];
1119
+ for (const marker of markers) {
1120
+ markerCoordinates.push(marker.location);
1121
+ }
1096
1122
 
1097
- const paths = extractPathsFromQuery(req.query, transformer);
1098
- const markers = extractMarkersFromQuery(
1099
- req.query,
1100
- options,
1101
- transformer,
1102
- );
1123
+ // Create array with coordinates from markers and path
1124
+ const coords = [].concat(paths.flat()).concat(markerCoordinates);
1103
1125
 
1104
- // Extract coordinates from markers
1105
- const markerCoordinates = [];
1106
- for (const marker of markers) {
1107
- markerCoordinates.push(marker.location);
1108
- }
1126
+ // Check if we have at least one coordinate to calculate a bounding box
1127
+ if (coords.length < 1) {
1128
+ return res.status(400).send('No coordinates provided');
1129
+ }
1109
1130
 
1110
- // Create array with coordinates from markers and path
1111
- const coords = [].concat(paths.flat()).concat(markerCoordinates);
1131
+ const bbox = [Infinity, Infinity, -Infinity, -Infinity];
1132
+ for (const pair of coords) {
1133
+ bbox[0] = Math.min(bbox[0], pair[0]);
1134
+ bbox[1] = Math.min(bbox[1], pair[1]);
1135
+ bbox[2] = Math.max(bbox[2], pair[0]);
1136
+ bbox[3] = Math.max(bbox[3], pair[1]);
1137
+ }
1112
1138
 
1113
- // Check if we have at least one coordinate to calculate a bounding box
1114
- if (coords.length < 1) {
1115
- return res.status(400).send('No coordinates provided');
1116
- }
1139
+ const bbox_ = mercator.convert(bbox, '900913');
1140
+ const center = mercator.inverse([
1141
+ (bbox_[0] + bbox_[2]) / 2,
1142
+ (bbox_[1] + bbox_[3]) / 2,
1143
+ ]);
1144
+
1145
+ // Calculate zoom level
1146
+ const maxZoom = parseFloat(req.query.maxzoom);
1147
+ let z = calcZForBBox(bbox, w, h, req.query);
1148
+ if (maxZoom > 0) {
1149
+ z = Math.min(z, maxZoom);
1150
+ }
1117
1151
 
1118
- const bbox = [Infinity, Infinity, -Infinity, -Infinity];
1119
- for (const pair of coords) {
1120
- bbox[0] = Math.min(bbox[0], pair[0]);
1121
- bbox[1] = Math.min(bbox[1], pair[1]);
1122
- bbox[2] = Math.max(bbox[2], pair[0]);
1123
- bbox[3] = Math.max(bbox[3], pair[1]);
1124
- }
1152
+ const x = center[0];
1153
+ const y = center[1];
1154
+
1155
+ const overlay = await renderOverlay(
1156
+ z,
1157
+ x,
1158
+ y,
1159
+ bearing,
1160
+ pitch,
1161
+ w,
1162
+ h,
1163
+ scale,
1164
+ paths,
1165
+ markers,
1166
+ req.query,
1167
+ );
1125
1168
 
1126
- const bbox_ = mercator.convert(bbox, '900913');
1127
- const center = mercator.inverse([
1128
- (bbox_[0] + bbox_[2]) / 2,
1129
- (bbox_[1] + bbox_[3]) / 2,
1130
- ]);
1131
-
1132
- // Calculate zoom level
1133
- const maxZoom = parseFloat(req.query.maxzoom);
1134
- let z = calcZForBBox(bbox, w, h, req.query);
1135
- if (maxZoom > 0) {
1136
- z = Math.min(z, maxZoom);
1169
+ return respondImage(
1170
+ item,
1171
+ z,
1172
+ x,
1173
+ y,
1174
+ bearing,
1175
+ pitch,
1176
+ w,
1177
+ h,
1178
+ scale,
1179
+ format,
1180
+ res,
1181
+ next,
1182
+ overlay,
1183
+ 'static',
1184
+ );
1185
+ } catch (e) {
1186
+ next(e);
1137
1187
  }
1138
-
1139
- const x = center[0];
1140
- const y = center[1];
1141
-
1142
- const overlay = await renderOverlay(
1143
- z,
1144
- x,
1145
- y,
1146
- bearing,
1147
- pitch,
1148
- w,
1149
- h,
1150
- scale,
1151
- paths,
1152
- markers,
1153
- req.query,
1154
- );
1155
-
1156
- return respondImage(
1157
- item,
1158
- z,
1159
- x,
1160
- y,
1161
- bearing,
1162
- pitch,
1163
- w,
1164
- h,
1165
- scale,
1166
- format,
1167
- res,
1168
- next,
1169
- overlay,
1170
- 'static',
1171
- );
1172
1188
  },
1173
1189
  );
1174
1190
  }
@@ -1393,6 +1409,8 @@ export const serve_rendered = {
1393
1409
  dataProjWGStoInternalWGS: null,
1394
1410
  lastModified: new Date().toUTCString(),
1395
1411
  watermark: params.watermark || options.watermark,
1412
+ staticAttributionText:
1413
+ params.staticAttributionText || options.staticAttributionText,
1396
1414
  };
1397
1415
  repo[id] = repoobj;
1398
1416
 
@@ -9,7 +9,7 @@ import { validate } from '@maplibre/maplibre-gl-style-spec';
9
9
 
10
10
  import { getPublicUrl } from './utils.js';
11
11
 
12
- const httpTester = /^(http(s)?:)?\/\//;
12
+ const httpTester = /^\/\//;
13
13
 
14
14
  const fixUrl = (req, url, publicUrl, opt_nokey) => {
15
15
  if (!url || typeof url !== 'string' || url.indexOf('local://') !== 0) {
package/src/server.js CHANGED
@@ -114,7 +114,6 @@ function start(opts) {
114
114
  /**
115
115
  * Recursively get all files within a directory.
116
116
  * Inspired by https://stackoverflow.com/a/45130990/10133863
117
- *
118
117
  * @param {string} directory Absolute path to a directory to get files from.
119
118
  */
120
119
  const getFiles = async (directory) => {
@@ -592,7 +591,6 @@ function start(opts) {
592
591
 
593
592
  /**
594
593
  * Stop the server gracefully
595
- *
596
594
  * @param {string} signal Name of the received signal
597
595
  */
598
596
  function stopGracefully(signal) {
package/src/utils.js CHANGED
@@ -8,7 +8,6 @@ import glyphCompose from '@mapbox/glyph-pbf-composite';
8
8
 
9
9
  /**
10
10
  * Generate new URL object
11
- *
12
11
  * @param req
13
12
  * @params {object} req - Express request
14
13
  * @returns {URL} object
package/test/static.js CHANGED
@@ -171,6 +171,18 @@ describe('Static endpoints', function () {
171
171
  '?path=-10,-10|-20,-20',
172
172
  );
173
173
  });
174
+
175
+ describe('encoded path', function () {
176
+ testStatic(
177
+ prefix,
178
+ 'auto/20x20',
179
+ 'png',
180
+ 200,
181
+ 2,
182
+ /image\/png/,
183
+ '?path=' + encodeURIComponent('enc:{{biGwvyGoUi@s_A|{@'),
184
+ );
185
+ });
174
186
  });
175
187
 
176
188
  describe('invalid requests return 4xx', function () {