tileserver-gl-light 5.5.0-pre.1 → 5.5.0-pre.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.
Files changed (32) hide show
  1. package/CHANGELOG.md +4 -2
  2. package/package.json +9 -9
  3. package/public/resources/maplibre-gl-inspect.js +2823 -2770
  4. package/public/resources/maplibre-gl-inspect.js.map +1 -1
  5. package/public/resources/maplibre-gl.css +1 -1
  6. package/public/resources/maplibre-gl.js +4 -4
  7. package/public/resources/maplibre-gl.js.map +1 -1
  8. package/src/main.js +16 -17
  9. package/src/pmtiles_adapter.js +3 -3
  10. package/src/promises.js +1 -1
  11. package/src/render.js +270 -93
  12. package/src/serve_data.js +6 -8
  13. package/src/serve_light.js +0 -1
  14. package/src/serve_rendered.js +372 -205
  15. package/src/server.js +22 -27
  16. package/src/utils.js +17 -18
  17. package/test/fixtures/visual/encoded-path-auto.png +0 -0
  18. package/test/fixtures/visual/linecap-linejoin-bevel-square.png +0 -0
  19. package/test/fixtures/visual/linecap-linejoin-round-round.png +0 -0
  20. package/test/fixtures/visual/path-auto.png +0 -0
  21. package/test/fixtures/visual/static-bbox.png +0 -0
  22. package/test/fixtures/visual/static-bearing-pitch.png +0 -0
  23. package/test/fixtures/visual/static-bearing.png +0 -0
  24. package/test/fixtures/visual/static-border-global.png +0 -0
  25. package/test/fixtures/visual/static-lat-lng.png +0 -0
  26. package/test/fixtures/visual/static-markers.png +0 -0
  27. package/test/fixtures/visual/static-multiple-paths.png +0 -0
  28. package/test/fixtures/visual/static-path-border-isolated.png +0 -0
  29. package/test/fixtures/visual/static-path-border-stroke.png +0 -0
  30. package/test/fixtures/visual/static-path-latlng.png +0 -0
  31. package/test/fixtures/visual/static-pixel-ratio-2x.png +0 -0
  32. package/test/static_images.js +241 -0
package/src/main.js CHANGED
@@ -12,7 +12,8 @@ import fs from 'node:fs';
12
12
  import fsp from 'node:fs/promises';
13
13
  import path from 'path';
14
14
  import { fileURLToPath } from 'url';
15
- import axios from 'axios';
15
+ import { Readable } from 'node:stream';
16
+ import { pipeline } from 'node:stream/promises';
16
17
  import { server } from './server.js';
17
18
  import { isValidRemoteUrl } from './utils.js';
18
19
  import { openPMtiles, getPMtilesInfo } from './pmtiles_adapter.js';
@@ -163,7 +164,6 @@ const startWithInputFile = async (inputFile) => {
163
164
  inputFile = path.resolve(process.cwd(), inputFile);
164
165
  inputFilePath = path.dirname(inputFile);
165
166
 
166
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- Validating local file from CLI argument
167
167
  const inputFileStats = await fsp.stat(inputFile);
168
168
  if (!inputFileStats.isFile() || inputFileStats.size === 0) {
169
169
  console.log(`ERROR: Not a valid input file: ${inputFile}`);
@@ -314,7 +314,6 @@ const startWithInputFile = async (inputFile) => {
314
314
  }
315
315
  };
316
316
 
317
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- Config path from CLI argument is expected behavior
318
317
  fs.stat(path.resolve(opts.config), async (err, stats) => {
319
318
  if (err || !stats.isFile() || stats.size === 0) {
320
319
  let inputFile;
@@ -331,7 +330,6 @@ fs.stat(path.resolve(opts.config), async (err, stats) => {
331
330
  const files = await fsp.readdir(process.cwd());
332
331
  for (const filename of files) {
333
332
  if (filename.endsWith('.mbtiles') || filename.endsWith('.pmtiles')) {
334
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- Scanning current directory for tile files
335
333
  const inputFilesStats = await fsp.stat(filename);
336
334
  if (inputFilesStats.isFile() && inputFilesStats.size > 0) {
337
335
  inputFile = filename;
@@ -346,25 +344,26 @@ fs.stat(path.resolve(opts.config), async (err, stats) => {
346
344
  const url =
347
345
  'https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/zurich_switzerland.mbtiles';
348
346
  const filename = 'zurich_switzerland.mbtiles';
349
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- Writing demo file to known filename
347
+
350
348
  const writer = fs.createWriteStream(filename);
351
349
  console.log(`No input file found`);
352
350
  console.log(`[DEMO] Downloading sample data (${filename}) from ${url}`);
353
351
 
354
352
  try {
355
- const response = await axios({
356
- url,
357
- method: 'GET',
358
- responseType: 'stream',
359
- });
360
-
361
- response.data.pipe(writer);
362
- writer.on('finish', () => startWithInputFile(filename));
363
- writer.on('error', (err) =>
364
- console.error(`Error writing file: ${err}`),
365
- );
353
+ const response = await fetch(url);
354
+
355
+ if (!response.ok) {
356
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
357
+ }
358
+
359
+ // Convert web ReadableStream to Node.js Readable stream and pipe to file
360
+ const nodeStream = Readable.fromWeb(response.body);
361
+ await pipeline(nodeStream, writer);
362
+
363
+ console.log('Download complete');
364
+ startWithInputFile(filename);
366
365
  } catch (error) {
367
- console.error(`Error downloading file: ${error}`);
366
+ console.error(`Error downloading file: ${error.message || error}`);
368
367
  }
369
368
  }
370
369
  }
@@ -81,7 +81,7 @@ class S3Source {
81
81
 
82
82
  // Format 1: s3://endpoint/bucket/key (S3-compatible storage)
83
83
  // Example: s3://storage.example.com/mybucket/path/to/tiles.pmtile
84
- const endpointMatch = cleanUrl.match(/^s3:\/\/([^\/]+)\/([^\/]+)\/(.+)$/);
84
+ const endpointMatch = cleanUrl.match(/^s3:\/\/([^/]+)\/([^/]+)\/(.+)$/);
85
85
  if (endpointMatch) {
86
86
  return {
87
87
  endpoint: `https://${endpointMatch[1]}`,
@@ -95,7 +95,7 @@ class S3Source {
95
95
 
96
96
  // Format 2: s3://bucket/key (AWS S3 default)
97
97
  // Example: s3://my-bucket/path/to/tiles.pmtiles
98
- const awsMatch = cleanUrl.match(/^s3:\/\/([^\/]+)\/(.+)$/);
98
+ const awsMatch = cleanUrl.match(/^s3:\/\/([^/]+)\/(.+)$/);
99
99
  if (awsMatch) {
100
100
  return {
101
101
  endpoint: null, // Use default AWS endpoint
@@ -316,7 +316,7 @@ export function openPMtiles(
316
316
  if (verbose >= 2) {
317
317
  console.log(`Opening PMTiles from local file: ${filePath}`);
318
318
  }
319
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- Opening local PMTiles file specified in config or CLI
319
+
320
320
  const fd = fs.openSync(filePath, 'r');
321
321
  const source = new PMTilesFileSource(fd);
322
322
  pmtiles = new PMTiles(source);
package/src/promises.js CHANGED
@@ -8,7 +8,7 @@ export const existsP = async (path) => {
8
8
  try {
9
9
  await fsp.access(path); // Defaults to F_OK: indicating that the file is visible to the calling process
10
10
  return true;
11
- } catch (err) {
11
+ } catch {
12
12
  return false;
13
13
  }
14
14
  };
package/src/render.js CHANGED
@@ -5,6 +5,17 @@ import { SphericalMercator } from '@mapbox/sphericalmercator';
5
5
 
6
6
  const mercator = new SphericalMercator();
7
7
 
8
+ // Constants
9
+ const CONSTANTS = {
10
+ DEFAULT_LINE_WIDTH: 1,
11
+ DEFAULT_BORDER_WIDTH_RATIO: 0.1, // 10% of line width
12
+ DEFAULT_FILL_COLOR: 'rgba(255,255,255,0.4)',
13
+ DEFAULT_STROKE_COLOR: 'rgba(0,64,255,0.7)',
14
+ MAX_LINE_WIDTH: 500,
15
+ MAX_BORDER_WIDTH: 250,
16
+ MARKER_LOAD_TIMEOUT: 5000,
17
+ };
18
+
8
19
  /**
9
20
  * Transforms coordinates to pixels.
10
21
  * @param {Array<number>} ll - Longitude/Latitude coordinate pair.
@@ -17,6 +28,73 @@ const precisePx = (ll, zoom) => {
17
28
  return [px[0] * scale, px[1] * scale];
18
29
  };
19
30
 
31
+ /**
32
+ * Validates if a string is a valid color value.
33
+ * @param {string} color - Color string to validate.
34
+ * @returns {boolean} True if valid color.
35
+ */
36
+ const isValidColor = (color) => {
37
+ if (!color || typeof color !== 'string') {
38
+ return false;
39
+ }
40
+
41
+ // Allow 'none' and 'transparent' keywords
42
+ if (color === 'none' || color === 'transparent') {
43
+ return true;
44
+ }
45
+
46
+ // Basic validation for common formats
47
+ const hexPattern = /^#([0-9A-Fa-f]{3}){1,2}$/; // 3 or 6 digits
48
+ const hexAlphaPattern = /^#([0-9A-Fa-f]{8})$/; // 8 digits with alpha
49
+ const rgbPattern = /^rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)$/;
50
+ const rgbaPattern = /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)$/;
51
+ const namedColors = [
52
+ 'red',
53
+ 'blue',
54
+ 'green',
55
+ 'yellow',
56
+ 'black',
57
+ 'white',
58
+ 'gray',
59
+ 'grey',
60
+ 'orange',
61
+ 'purple',
62
+ 'pink',
63
+ 'brown',
64
+ 'cyan',
65
+ 'magenta',
66
+ ];
67
+
68
+ return (
69
+ hexPattern.test(color) ||
70
+ hexAlphaPattern.test(color) ||
71
+ rgbPattern.test(color) ||
72
+ rgbaPattern.test(color) ||
73
+ namedColors.includes(color.toLowerCase())
74
+ );
75
+ };
76
+
77
+ /**
78
+ * Safely parses a numeric value with bounds checking.
79
+ * @param {string|number} value - Value to parse.
80
+ * @param {number} defaultValue - Default value if parsing fails.
81
+ * @param {number} min - Minimum allowed value.
82
+ * @param {number} max - Maximum allowed value.
83
+ * @returns {number} Parsed and bounded value.
84
+ */
85
+ const safeParseNumber = (
86
+ value,
87
+ defaultValue,
88
+ min = -Infinity,
89
+ max = Infinity,
90
+ ) => {
91
+ const parsed = Number(value);
92
+ if (isNaN(parsed)) {
93
+ return defaultValue;
94
+ }
95
+ return Math.max(min, Math.min(max, parsed));
96
+ };
97
+
20
98
  /**
21
99
  * Draws a marker in canvas context.
22
100
  * @param {CanvasRenderingContext2D} ctx - Canvas context object.
@@ -25,22 +103,28 @@ const precisePx = (ll, zoom) => {
25
103
  * @returns {Promise<void>} A promise that resolves when the marker is drawn.
26
104
  */
27
105
  const drawMarker = (ctx, marker, z) => {
28
- return new Promise((resolve) => {
106
+ return new Promise((resolve, reject) => {
29
107
  const img = new Image();
30
108
  const pixelCoords = precisePx(marker.location, z);
31
109
 
110
+ // Add timeout to prevent hanging on slow/failed image loads
111
+ const timeout = setTimeout(() => {
112
+ reject(new Error(`Marker image load timeout: ${marker.icon}`));
113
+ }, CONSTANTS.MARKER_LOAD_TIMEOUT);
114
+
32
115
  const getMarkerCoordinates = (imageWidth, imageHeight, scale) => {
33
116
  // Images are placed with their top-left corner at the provided location
34
117
  // within the canvas but we expect icons to be centered and above it.
35
118
 
36
- // Substract half of the images width from the x-coordinate to center
119
+ // Subtract half of the image's width from the x-coordinate to center
37
120
  // the image in relation to the provided location
38
121
  let xCoordinate = pixelCoords[0] - imageWidth / 2;
39
- // Substract the images height from the y-coordinate to place it above
122
+
123
+ // Subtract the image's height from the y-coordinate to place it above
40
124
  // the provided location
41
125
  let yCoordinate = pixelCoords[1] - imageHeight;
42
126
 
43
- // Since image placement is dependent on the size offsets have to be
127
+ // Since image placement is dependent on the size, offsets have to be
44
128
  // scaled as well. Additionally offsets are provided as either positive or
45
129
  // negative values so we always add them
46
130
  if (marker.offsetX) {
@@ -57,30 +141,38 @@ const drawMarker = (ctx, marker, z) => {
57
141
  };
58
142
 
59
143
  const drawOnCanvas = () => {
60
- // Check if the images should be resized before beeing drawn
61
- const defaultScale = 1;
62
- const scale = marker.scale ? marker.scale : defaultScale;
63
-
64
- // Calculate scaled image sizes
65
- const imageWidth = img.width * scale;
66
- const imageHeight = img.height * scale;
67
-
68
- // Pass the desired sizes to get correlating coordinates
69
- const coords = getMarkerCoordinates(imageWidth, imageHeight, scale);
70
-
71
- // Draw the image on canvas
72
- if (scale != defaultScale) {
73
- ctx.drawImage(img, coords.x, coords.y, imageWidth, imageHeight);
74
- } else {
75
- ctx.drawImage(img, coords.x, coords.y);
144
+ clearTimeout(timeout);
145
+
146
+ try {
147
+ // Check if the image should be resized before being drawn
148
+ const defaultScale = 1;
149
+ const scale = marker.scale ? marker.scale : defaultScale;
150
+
151
+ // Calculate scaled image sizes
152
+ const imageWidth = img.width * scale;
153
+ const imageHeight = img.height * scale;
154
+
155
+ // Pass the desired sizes to get correlating coordinates
156
+ const coords = getMarkerCoordinates(imageWidth, imageHeight, scale);
157
+
158
+ // Draw the image on canvas
159
+ if (scale !== defaultScale) {
160
+ ctx.drawImage(img, coords.x, coords.y, imageWidth, imageHeight);
161
+ } else {
162
+ ctx.drawImage(img, coords.x, coords.y);
163
+ }
164
+
165
+ // Resolve the promise when image has been drawn
166
+ resolve();
167
+ } catch (error) {
168
+ reject(new Error(`Failed to draw marker: ${error.message}`));
76
169
  }
77
- // Resolve the promise when image has been drawn
78
- resolve();
79
170
  };
80
171
 
81
172
  img.onload = drawOnCanvas;
82
- img.onerror = (err) => {
83
- throw err;
173
+ img.onerror = () => {
174
+ clearTimeout(timeout);
175
+ reject(new Error(`Failed to load marker image: ${marker.icon}`));
84
176
  };
85
177
  img.src = marker.icon;
86
178
  });
@@ -89,7 +181,7 @@ const drawMarker = (ctx, marker, z) => {
89
181
  /**
90
182
  * Draws a list of markers onto a canvas.
91
183
  * Wraps drawing of markers into list of promises and awaits them.
92
- * It's required because images are expected to load asynchronous in canvas js
184
+ * It's required because images are expected to load asynchronously in canvas js
93
185
  * even when provided from a local disk.
94
186
  * @param {CanvasRenderingContext2D} ctx - Canvas context object.
95
187
  * @param {Array<object>} markers - Marker objects parsed by extractMarkersFromQuery.
@@ -105,7 +197,26 @@ const drawMarkers = async (ctx, markers, z) => {
105
197
  }
106
198
 
107
199
  // Await marker drawings before continuing
108
- await Promise.all(markerPromises);
200
+ // Use Promise.allSettled to continue even if some markers fail
201
+ const results = await Promise.allSettled(markerPromises);
202
+
203
+ // Log any failures
204
+ results.forEach((result, index) => {
205
+ if (result.status === 'rejected') {
206
+ console.warn(`Marker ${index} failed to render:`, result.reason);
207
+ }
208
+ });
209
+ };
210
+
211
+ /**
212
+ * Extracts an option value from a path query string.
213
+ * @param {Array<string>} splitPaths - Path string split by pipe character.
214
+ * @param {string} optionName - Name of the option to extract.
215
+ * @returns {string|undefined} Option value or undefined if not found.
216
+ */
217
+ const getInlineOption = (splitPaths, optionName) => {
218
+ const found = splitPaths.find((x) => x.startsWith(`${optionName}:`));
219
+ return found ? found.replace(`${optionName}:`, '') : undefined;
109
220
  };
110
221
 
111
222
  /**
@@ -118,21 +229,25 @@ const drawMarkers = async (ctx, markers, z) => {
118
229
  * @returns {void}
119
230
  */
120
231
  const drawPath = (ctx, path, query, pathQuery, z) => {
121
- const splitPaths = pathQuery.split('|');
122
-
123
232
  if (!path || path.length < 2) {
124
- return null;
233
+ return;
125
234
  }
126
235
 
236
+ const splitPaths = pathQuery.split('|');
237
+
238
+ // Start the path - transform coordinates to pixels on canvas and draw lines between points
127
239
  ctx.beginPath();
128
240
 
129
- // Transform coordinates to pixel on canvas and draw lines between points
130
- for (const pair of path) {
241
+ for (const [i, pair] of path.entries()) {
131
242
  const px = precisePx(pair, z);
132
- ctx.lineTo(px[0], px[1]);
243
+ if (i === 0) {
244
+ ctx.moveTo(px[0], px[1]);
245
+ } else {
246
+ ctx.lineTo(px[0], px[1]);
247
+ }
133
248
  }
134
249
 
135
- // Check if first coordinate matches last coordinate
250
+ // Check if first coordinate matches last coordinate (closed path)
136
251
  if (
137
252
  path[0][0] === path[path.length - 1][0] &&
138
253
  path[0][1] === path[path.length - 1][1]
@@ -140,77 +255,130 @@ const drawPath = (ctx, path, query, pathQuery, z) => {
140
255
  ctx.closePath();
141
256
  }
142
257
 
143
- // Optionally fill drawn shape with a rgba color from query
144
- const pathHasFill = splitPaths.filter((x) => x.startsWith('fill')).length > 0;
258
+ // --- FILL Logic ---
259
+ const inlineFill = getInlineOption(splitPaths, 'fill');
260
+ const pathHasFill = inlineFill !== undefined;
261
+
145
262
  if (query.fill !== undefined || pathHasFill) {
146
- if ('fill' in query) {
147
- ctx.fillStyle = query.fill || 'rgba(255,255,255,0.4)';
148
- }
263
+ let fillColor;
264
+
149
265
  if (pathHasFill) {
150
- ctx.fillStyle = splitPaths
151
- .find((x) => x.startsWith('fill:'))
152
- .replace('fill:', '');
266
+ fillColor = inlineFill;
267
+ } else if ('fill' in query) {
268
+ fillColor = query.fill || CONSTANTS.DEFAULT_FILL_COLOR;
269
+ } else {
270
+ fillColor = CONSTANTS.DEFAULT_FILL_COLOR;
153
271
  }
154
- ctx.fill();
155
- }
156
272
 
157
- // Get line width from query and fall back to 1 if not provided
158
- const pathHasWidth =
159
- splitPaths.filter((x) => x.startsWith('width')).length > 0;
160
- if (query.width !== undefined || pathHasWidth) {
161
- let lineWidth = 1;
162
- // Get line width from query
163
- if ('width' in query) {
164
- lineWidth = Number(query.width);
273
+ // Validate color before using
274
+ if (isValidColor(fillColor)) {
275
+ ctx.fillStyle = fillColor;
276
+ ctx.fill();
277
+ } else {
278
+ console.warn(`Invalid fill color: ${fillColor}, using default`);
279
+ ctx.fillStyle = CONSTANTS.DEFAULT_FILL_COLOR;
280
+ ctx.fill();
165
281
  }
166
- // Get line width from path in query
167
- if (pathHasWidth) {
168
- lineWidth = Number(
169
- splitPaths.find((x) => x.startsWith('width:')).replace('width:', ''),
170
- );
171
- }
172
- // Get border width from query and fall back to 10% of line width
173
- const borderWidth =
174
- query.borderwidth !== undefined
175
- ? parseFloat(query.borderwidth)
176
- : lineWidth * 0.1;
177
-
178
- // Set rendering style for the start and end points of the path
179
- // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap
180
- ctx.lineCap = query.linecap || 'butt';
181
-
182
- // Set rendering style for overlapping segments of the path with differing directions
183
- // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin
184
- ctx.lineJoin = query.linejoin || 'miter';
185
-
186
- // In order to simulate a border we draw the path two times with the first
187
- // beeing the wider border part.
188
- if (query.border !== undefined && borderWidth > 0) {
282
+ }
283
+
284
+ // --- WIDTH & BORDER Logic ---
285
+ const inlineWidth = getInlineOption(splitPaths, 'width');
286
+ const pathHasWidth = inlineWidth !== undefined;
287
+ const inlineBorder = getInlineOption(splitPaths, 'border');
288
+ const inlineBorderWidth = getInlineOption(splitPaths, 'borderwidth');
289
+ const pathHasBorder = inlineBorder !== undefined;
290
+
291
+ // Parse line width with validation
292
+ let lineWidth = CONSTANTS.DEFAULT_LINE_WIDTH;
293
+ if (pathHasWidth) {
294
+ lineWidth = safeParseNumber(
295
+ inlineWidth,
296
+ CONSTANTS.DEFAULT_LINE_WIDTH,
297
+ 0,
298
+ CONSTANTS.MAX_LINE_WIDTH,
299
+ );
300
+ } else if ('width' in query) {
301
+ lineWidth = safeParseNumber(
302
+ query.width,
303
+ CONSTANTS.DEFAULT_LINE_WIDTH,
304
+ 0,
305
+ CONSTANTS.MAX_LINE_WIDTH,
306
+ );
307
+ }
308
+
309
+ // Get border width with validation
310
+ // Default: 10% of line width
311
+ let borderWidth = lineWidth * CONSTANTS.DEFAULT_BORDER_WIDTH_RATIO;
312
+ if (pathHasBorder && inlineBorderWidth) {
313
+ borderWidth = safeParseNumber(
314
+ inlineBorderWidth,
315
+ borderWidth,
316
+ 0,
317
+ CONSTANTS.MAX_BORDER_WIDTH,
318
+ );
319
+ } else if (query.borderwidth !== undefined) {
320
+ borderWidth = safeParseNumber(
321
+ query.borderwidth,
322
+ borderWidth,
323
+ 0,
324
+ CONSTANTS.MAX_BORDER_WIDTH,
325
+ );
326
+ }
327
+
328
+ // Set rendering style for the start and end points of the path
329
+ // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap
330
+ const validLineCaps = ['butt', 'round', 'square'];
331
+ ctx.lineCap = validLineCaps.includes(query.linecap) ? query.linecap : 'butt';
332
+
333
+ // Set rendering style for overlapping segments of the path with differing directions
334
+ // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin
335
+ const validLineJoins = ['miter', 'round', 'bevel'];
336
+ ctx.lineJoin = validLineJoins.includes(query.linejoin)
337
+ ? query.linejoin
338
+ : 'miter';
339
+
340
+ // The final border color, prioritized by inline over global query
341
+ const finalBorder = pathHasBorder ? inlineBorder : query.border;
342
+
343
+ // In order to simulate a border we draw the path two times with the first
344
+ // being the wider border part.
345
+ if (finalBorder !== undefined && borderWidth > 0) {
346
+ // Validate border color
347
+ if (isValidColor(finalBorder)) {
189
348
  // We need to double the desired border width and add it to the line width
190
349
  // in order to get the desired border on each side of the line.
191
350
  ctx.lineWidth = lineWidth + borderWidth * 2;
192
- // Set border style as rgba
193
- ctx.strokeStyle = query.border;
351
+ ctx.strokeStyle = finalBorder;
194
352
  ctx.stroke();
353
+ } else {
354
+ console.warn(`Invalid border color: ${finalBorder}, skipping border`);
195
355
  }
196
- ctx.lineWidth = lineWidth;
197
356
  }
198
357
 
199
- const pathHasStroke =
200
- splitPaths.filter((x) => x.startsWith('stroke')).length > 0;
201
- if (query.stroke !== undefined || pathHasStroke) {
202
- if ('stroke' in query) {
203
- ctx.strokeStyle = query.stroke;
204
- }
205
- // Path Stroke gets higher priority
206
- if (pathHasStroke) {
207
- ctx.strokeStyle = splitPaths
208
- .find((x) => x.startsWith('stroke:'))
209
- .replace('stroke:', '');
210
- }
358
+ // Set line width for the main stroke
359
+ ctx.lineWidth = lineWidth;
360
+
361
+ // --- STROKE Logic ---
362
+ const inlineStroke = getInlineOption(splitPaths, 'stroke');
363
+ const pathHasStroke = inlineStroke !== undefined;
364
+
365
+ let strokeColor;
366
+ if (pathHasStroke) {
367
+ strokeColor = inlineStroke;
368
+ } else if ('stroke' in query) {
369
+ strokeColor = query.stroke;
211
370
  } else {
212
- ctx.strokeStyle = 'rgba(0,64,255,0.7)';
371
+ strokeColor = CONSTANTS.DEFAULT_STROKE_COLOR;
213
372
  }
373
+
374
+ // Validate stroke color
375
+ if (isValidColor(strokeColor)) {
376
+ ctx.strokeStyle = strokeColor;
377
+ } else {
378
+ console.warn(`Invalid stroke color: ${strokeColor}, using default`);
379
+ ctx.strokeStyle = CONSTANTS.DEFAULT_STROKE_COLOR;
380
+ }
381
+
214
382
  ctx.stroke();
215
383
  };
216
384
 
@@ -260,23 +428,32 @@ export const renderOverlay = async (
260
428
  const canvas = createCanvas(scale * w, scale * h);
261
429
  const ctx = canvas.getContext('2d');
262
430
  ctx.scale(scale, scale);
431
+
263
432
  if (bearing) {
264
433
  ctx.translate(w / 2, h / 2);
265
434
  ctx.rotate((-bearing / 180) * Math.PI);
266
435
  ctx.translate(-center[0], -center[1]);
267
436
  } else {
268
- // optimized path
437
+ // Optimized path
269
438
  ctx.translate(-center[0] + w / 2, -center[1] + h / 2);
270
439
  }
271
440
 
272
441
  // Draw provided paths if any
273
442
  paths.forEach((path, i) => {
274
443
  const pathQuery = Array.isArray(query.path) ? query.path.at(i) : query.path;
275
- drawPath(ctx, path, query, pathQuery, z);
444
+ try {
445
+ drawPath(ctx, path, query, pathQuery, z);
446
+ } catch (error) {
447
+ console.error(`Error drawing path ${i}:`, error);
448
+ }
276
449
  });
277
450
 
278
451
  // Await drawing of markers before rendering the canvas
279
- await drawMarkers(ctx, markers, z);
452
+ try {
453
+ await drawMarkers(ctx, markers, z);
454
+ } catch (error) {
455
+ console.error('Error drawing markers:', error);
456
+ }
280
457
 
281
458
  return canvas.toBuffer();
282
459
  };
package/src/serve_data.js CHANGED
@@ -12,7 +12,6 @@ import { SphericalMercator } from '@mapbox/sphericalmercator';
12
12
  import {
13
13
  fixTileJSONCenter,
14
14
  getTileUrls,
15
- isS3Url,
16
15
  isValidRemoteUrl,
17
16
  fetchTileData,
18
17
  } from './utils.js';
@@ -31,9 +30,9 @@ const packageJson = JSON.parse(
31
30
  );
32
31
 
33
32
  const isLight = packageJson.name.slice(-6) === '-light';
34
- const serve_rendered = (
35
- await import(`${!isLight ? `./serve_rendered.js` : `./serve_light.js`}`)
36
- ).serve_rendered;
33
+ const { serve_rendered } = await import(
34
+ `${!isLight ? `./serve_rendered.js` : `./serve_light.js`}`
35
+ );
37
36
 
38
37
  export const serve_data = {
39
38
  /**
@@ -69,7 +68,7 @@ export const serve_data = {
69
68
  String(req.params.format).replace(/\n|\r/g, ''),
70
69
  );
71
70
  }
72
- // eslint-disable-next-line security/detect-object-injection -- req.params.id is route parameter, validated by Express
71
+
73
72
  const item = repo[req.params.id];
74
73
  if (!item) {
75
74
  return res.sendStatus(404);
@@ -193,7 +192,7 @@ export const serve_data = {
193
192
  String(req.params.y).replace(/\n|\r/g, ''),
194
193
  );
195
194
  }
196
- // eslint-disable-next-line security/detect-object-injection -- req.params.id is route parameter, validated by Express
195
+
197
196
  const item = repo?.[req.params.id];
198
197
  if (!item) return res.sendStatus(404);
199
198
  if (!item.source) return res.status(404).send('Missing source');
@@ -304,7 +303,7 @@ export const serve_data = {
304
303
  String(req.params.id).replace(/\n|\r/g, ''),
305
304
  );
306
305
  }
307
- // eslint-disable-next-line security/detect-object-injection -- req.params.id is route parameter, validated by Express
306
+
308
307
  const item = repo[req.params.id];
309
308
  if (!item) {
310
309
  return res.sendStatus(404);
@@ -373,7 +372,6 @@ export const serve_data = {
373
372
 
374
373
  // Only check file stats for local files, not remote URLs
375
374
  if (!isValidRemoteUrl(inputFile)) {
376
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- inputFile is from config file, validated above
377
375
  const inputFileStats = await fsp.stat(inputFile);
378
376
  if (!inputFileStats.isFile() || inputFileStats.size === 0) {
379
377
  throw Error(`Not valid input file: "${inputFile}"`);
@@ -1,4 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-empty-function */
2
1
  /* eslint-disable @typescript-eslint/no-unused-vars */
3
2
  'use strict';
4
3