tileserver-gl-light 5.4.1-pre.0 → 5.5.0-pre.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.
@@ -1,14 +1,252 @@
1
1
  import fs from 'node:fs';
2
- import { PMTiles, FetchSource } from 'pmtiles';
3
- import { isValidHttpUrl } from './utils.js';
2
+ import { PMTiles, FetchSource, EtagMismatch } from 'pmtiles';
3
+ import { isValidHttpUrl, isS3Url } from './utils.js';
4
+ import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
5
+ import { fromIni } from '@aws-sdk/credential-provider-ini';
4
6
 
7
+ /**
8
+ * S3 Source for PMTiles
9
+ * Supports:
10
+ * - AWS S3: s3://bucket-name/path/to/file.pmtiles
11
+ * - S3-compatible with endpoint: s3://endpoint-url/bucket/path/to/file.pmtiles
12
+ */
13
+ class S3Source {
14
+ /**
15
+ * Creates an S3Source instance.
16
+ * @param {string} s3Url - The S3 URL in one of the supported formats.
17
+ * @param {string} [s3Profile] - Optional AWS credential profile name from config.
18
+ * @param {boolean} [configRequestPayer] - Optional flag from config for requester pays buckets.
19
+ * @param {string} [configRegion] - Optional AWS region from config.
20
+ * @param {boolean} [verbose] - Whether to show verbose logging.
21
+ */
22
+ constructor(
23
+ s3Url,
24
+ s3Profile,
25
+ configRequestPayer,
26
+ configRegion,
27
+ verbose = false,
28
+ ) {
29
+ const parsed = this.parseS3Url(s3Url);
30
+ this.bucket = parsed.bucket;
31
+ this.key = parsed.key;
32
+ this.endpoint = parsed.endpoint;
33
+ this.url = s3Url;
34
+ this.verbose = verbose;
35
+
36
+ // Determine the final profile: Config takes precedence over URL
37
+ const profile = s3Profile || parsed.profile;
38
+
39
+ // Determine requestPayer: Config takes precedence over URL
40
+ this.requestPayer = configRequestPayer ?? parsed.requestPayer;
41
+
42
+ // Determine region: Config takes precedence over URL
43
+ this.region = configRegion || parsed.region;
44
+
45
+ // Create S3 client
46
+ this.s3Client = this.createS3Client(
47
+ parsed.endpoint,
48
+ this.region,
49
+ profile,
50
+ this.verbose,
51
+ );
52
+ }
53
+
54
+ /**
55
+ * Parses various S3 URL formats into bucket, key, endpoint, region, and profile.
56
+ * @param {string} url - The S3 URL to parse.
57
+ * @returns {object} - An object containing bucket, key, endpoint, region, and profile.
58
+ * @throws {Error} - Throws an error if the URL format is invalid.
59
+ */
60
+ parseS3Url(url) {
61
+ // Initialize with defaults/environment
62
+ let region = process.env.AWS_REGION || 'us-east-1';
63
+ let profile = null;
64
+ let requestPayer = false;
65
+
66
+ const queryString = url.split('?')[1];
67
+ if (queryString) {
68
+ const params = new URLSearchParams(queryString);
69
+ // Update profile if provided in url parameters
70
+ profile = params.get('profile') ?? profile;
71
+ // Update region if provided in url parameters
72
+ region = params.get('region') ?? region;
73
+ // Update requestPayer if provided in url parameters
74
+ const payerVal = params.get('requestPayer');
75
+ requestPayer = payerVal === 'true' || payerVal === '1';
76
+ }
77
+
78
+ // Clean URL for format detection (remove trailing slashes)
79
+ const baseUrl = url.split('?')[0];
80
+ const cleanUrl = baseUrl.replace(/\/+$/, '');
81
+
82
+ // Format 1: s3://endpoint/bucket/key (S3-compatible storage)
83
+ // Example: s3://storage.example.com/mybucket/path/to/tiles.pmtile
84
+ const endpointMatch = cleanUrl.match(/^s3:\/\/([^\/]+)\/([^\/]+)\/(.+)$/);
85
+ if (endpointMatch) {
86
+ return {
87
+ endpoint: `https://${endpointMatch[1]}`,
88
+ bucket: endpointMatch[2],
89
+ key: endpointMatch[3],
90
+ region,
91
+ profile,
92
+ requestPayer,
93
+ };
94
+ }
95
+
96
+ // Format 2: s3://bucket/key (AWS S3 default)
97
+ // Example: s3://my-bucket/path/to/tiles.pmtiles
98
+ const awsMatch = cleanUrl.match(/^s3:\/\/([^\/]+)\/(.+)$/);
99
+ if (awsMatch) {
100
+ return {
101
+ endpoint: null, // Use default AWS endpoint
102
+ bucket: awsMatch[1],
103
+ key: awsMatch[2],
104
+ region,
105
+ profile,
106
+ requestPayer,
107
+ };
108
+ }
109
+
110
+ throw new Error(`Invalid S3 URL format: ${url}`);
111
+ }
112
+
113
+ /**
114
+ * Creates an S3 client with optional custom endpoint and AWS profile support.
115
+ * @param {string|null} endpoint - The custom endpoint URL, or null for default AWS S3.
116
+ * @param {string} region - The AWS region.
117
+ * @param {string} [profile] - Optional AWS credential profile name.
118
+ * @param {boolean} [verbose] - Whether to show verbose logging.
119
+ * @returns {S3Client} - Configured S3Client instance.
120
+ */
121
+ createS3Client(endpoint, region, profile, verbose) {
122
+ const config = {
123
+ region: region,
124
+ requestHandler: {
125
+ connectionTimeout: 5000,
126
+ socketTimeout: 5000,
127
+ },
128
+ forcePathStyle: !!endpoint,
129
+ };
130
+
131
+ if (endpoint) {
132
+ config.endpoint = endpoint;
133
+ if (verbose >= 2) {
134
+ console.log(`Using custom S3 endpoint: ${endpoint}`);
135
+ }
136
+ }
137
+
138
+ if (profile) {
139
+ config.credentials = fromIni({ profile });
140
+ if (verbose >= 2) {
141
+ console.log(`Using AWS profile: ${profile}`);
142
+ }
143
+ }
144
+
145
+ return new S3Client(config);
146
+ }
147
+ /**
148
+ * Returns the unique key for this S3 source.
149
+ * @returns {string} - The S3 URL.
150
+ */
151
+ getKey() {
152
+ return this.url;
153
+ }
154
+
155
+ /**
156
+ * Fetches a byte range from the S3 object.
157
+ * @param {number} offset - The starting byte offset.
158
+ * @param {number} length - The number of bytes to fetch.
159
+ * @param {AbortSignal} [signal] - Optional abort signal for cancelling the request.
160
+ * @param {string} [etag] - Optional ETag for conditional requests.
161
+ * @returns {Promise<object>} - A promise that resolves to an object containing data, etag, expires, and cacheControl.
162
+ * @throws {EtagMismatch} - Throws if ETag doesn't match.
163
+ * @throws {Error} - Throws on S3 errors like NoSuchKey, AccessDenied, NoSuchBucket.
164
+ */
165
+ async getBytes(offset, length, signal, etag) {
166
+ try {
167
+ const commandParams = {
168
+ Bucket: this.bucket,
169
+ Key: this.key,
170
+ Range: `bytes=${offset}-${offset + length - 1}`,
171
+ IfMatch: etag,
172
+ };
173
+
174
+ if (this.requestPayer) {
175
+ commandParams.RequestPayer = 'requester';
176
+ }
177
+
178
+ const command = new GetObjectCommand(commandParams);
179
+
180
+ const response = await this.s3Client.send(command, {
181
+ abortSignal: signal,
182
+ });
183
+
184
+ const arr = await response.Body.transformToByteArray();
185
+
186
+ if (!arr) {
187
+ throw new Error('Failed to read S3 response body');
188
+ }
189
+
190
+ return {
191
+ data: arr.buffer,
192
+ etag: response.ETag,
193
+ expires: response.Expires?.toISOString(),
194
+ cacheControl: response.CacheControl,
195
+ };
196
+ } catch (error) {
197
+ // Handle AWS SDK errors
198
+ if (error.name === 'PreconditionFailed') {
199
+ throw new EtagMismatch();
200
+ }
201
+
202
+ if (error.name === 'NoSuchKey') {
203
+ throw new Error(`PMTiles file not found: ${this.bucket}/${this.key}`);
204
+ }
205
+
206
+ if (error.name === 'AccessDenied') {
207
+ throw new Error(
208
+ `Access denied: ${this.bucket}/${this.key}. Check credentials and bucket permissions.`,
209
+ );
210
+ }
211
+
212
+ if (error.name === 'NoSuchBucket') {
213
+ throw new Error(
214
+ `Bucket not found: ${this.bucket}. Check bucket name and endpoint.`,
215
+ );
216
+ }
217
+
218
+ console.error(`S3 error for ${this.bucket}/${this.key}:`, error.message);
219
+ throw error;
220
+ }
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Local file source for PMTiles using Node.js file descriptors.
226
+ */
5
227
  class PMTilesFileSource {
228
+ /**
229
+ * Creates a PMTilesFileSource instance.
230
+ * @param {number} fd - The file descriptor for the opened PMTiles file.
231
+ */
6
232
  constructor(fd) {
7
233
  this.fd = fd;
8
234
  }
235
+
236
+ /**
237
+ * Returns the unique key for this file source.
238
+ * @returns {number} - The file descriptor.
239
+ */
9
240
  getKey() {
10
241
  return this.fd;
11
242
  }
243
+
244
+ /**
245
+ * Reads a byte range from the local file.
246
+ * @param {number} offset - The starting byte offset.
247
+ * @param {number} length - The number of bytes to read.
248
+ * @returns {Promise<object>} - A promise that resolves to an object containing the data as an ArrayBuffer.
249
+ */
12
250
  async getBytes(offset, length) {
13
251
  const buffer = Buffer.alloc(length);
14
252
  await readFileBytes(this.fd, buffer, offset);
@@ -21,10 +259,11 @@ class PMTilesFileSource {
21
259
  }
22
260
 
23
261
  /**
24
- *
25
- * @param fd
26
- * @param buffer
27
- * @param offset
262
+ * Reads bytes from a file descriptor into a buffer.
263
+ * @param {number} fd - The file descriptor.
264
+ * @param {Buffer} buffer - The buffer to read data into.
265
+ * @param {number} offset - The file offset to start reading from.
266
+ * @returns {Promise<void>} - A promise that resolves when the read operation completes.
28
267
  */
29
268
  async function readFileBytes(fd, buffer, offset) {
30
269
  return new Promise((resolve, reject) => {
@@ -38,86 +277,200 @@ async function readFileBytes(fd, buffer, offset) {
38
277
  }
39
278
 
40
279
  /**
41
- *
42
- * @param FilePath
280
+ * Opens a PMTiles file from local filesystem, HTTP URL, or S3 URL.
281
+ * @param {string} filePath - The path to the PMTiles file.
282
+ * @param {string} [s3Profile] - Optional AWS credential profile name.
283
+ * @param {boolean} [requestPayer] - Optional flag for requester pays buckets.
284
+ * @param {string} [s3Region] - Optional AWS region.
285
+ * @param {boolean} [verbose] - Whether to show verbose logging.
286
+ * @returns {PMTiles} - A PMTiles instance.
43
287
  */
44
- export function openPMtiles(FilePath) {
288
+ export function openPMtiles(
289
+ filePath,
290
+ s3Profile,
291
+ requestPayer,
292
+ s3Region,
293
+ verbose = 0,
294
+ ) {
45
295
  let pmtiles = undefined;
46
296
 
47
- if (isValidHttpUrl(FilePath)) {
48
- const source = new FetchSource(FilePath);
297
+ if (isS3Url(filePath)) {
298
+ if (verbose >= 2) {
299
+ console.log(`Opening PMTiles from S3: ${filePath}`);
300
+ }
301
+ const source = new S3Source(
302
+ filePath,
303
+ s3Profile,
304
+ requestPayer,
305
+ s3Region,
306
+ verbose,
307
+ );
308
+ pmtiles = new PMTiles(source);
309
+ } else if (isValidHttpUrl(filePath)) {
310
+ if (verbose >= 2) {
311
+ console.log(`Opening PMTiles from HTTP: ${filePath}`);
312
+ }
313
+ const source = new FetchSource(filePath);
49
314
  pmtiles = new PMTiles(source);
50
315
  } else {
51
- const fd = fs.openSync(FilePath, 'r');
316
+ if (verbose >= 2) {
317
+ console.log(`Opening PMTiles from local file: ${filePath}`);
318
+ }
319
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- Opening local PMTiles file specified in config or CLI
320
+ const fd = fs.openSync(filePath, 'r');
52
321
  const source = new PMTilesFileSource(fd);
53
322
  pmtiles = new PMTiles(source);
54
323
  }
324
+
55
325
  return pmtiles;
56
326
  }
57
327
 
58
328
  /**
59
- *
60
- * @param pmtiles
329
+ * Retrieves metadata and header information from a PMTiles archive with retry logic for rate limiting.
330
+ * @param {PMTiles} pmtiles - The PMTiles instance.
331
+ * @param {string} inputFile - The input file path (used for error messages).
332
+ * @param {number} [maxRetries] - Maximum number of retry attempts for rate-limited requests.
333
+ * @returns {Promise<object>} - A promise that resolves to a metadata object containing format, bounds, zoom levels, and center.
334
+ * @throws {Error} - Throws an error if metadata cannot be retrieved after all retry attempts.
61
335
  */
62
- export async function getPMtilesInfo(pmtiles) {
63
- const header = await pmtiles.getHeader();
64
- const metadata = await pmtiles.getMetadata();
65
-
66
- //Add missing metadata from header
67
- metadata['format'] = getPmtilesTileType(header.tileType).type;
68
- metadata['minzoom'] = header.minZoom;
69
- metadata['maxzoom'] = header.maxZoom;
70
-
71
- if (header.minLon && header.minLat && header.maxLon && header.maxLat) {
72
- metadata['bounds'] = [
73
- header.minLon,
74
- header.minLat,
75
- header.maxLon,
76
- header.maxLat,
77
- ];
78
- } else {
79
- metadata['bounds'] = [-180, -85.05112877980659, 180, 85.0511287798066];
80
- }
336
+ export async function getPMtilesInfo(pmtiles, inputFile, maxRetries = 3) {
337
+ let lastError;
81
338
 
82
- if (header.centerZoom) {
83
- metadata['center'] = [
84
- header.centerLon,
85
- header.centerLat,
86
- header.centerZoom,
87
- ];
88
- } else {
89
- metadata['center'] = [
90
- header.centerLon,
91
- header.centerLat,
92
- parseInt(metadata['maxzoom']) / 2,
93
- ];
339
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
340
+ try {
341
+ const header = await pmtiles.getHeader();
342
+ const metadata = await pmtiles.getMetadata();
343
+
344
+ metadata['format'] = getPmtilesTileType(header.tileType).type;
345
+ metadata['minzoom'] = header.minZoom;
346
+ metadata['maxzoom'] = header.maxZoom;
347
+
348
+ // Check if bounds are defined (handles null, undefined, but allows 0)
349
+ const hasBounds =
350
+ typeof header.minLon === 'number' &&
351
+ typeof header.minLat === 'number' &&
352
+ typeof header.maxLon === 'number' &&
353
+ typeof header.maxLat === 'number' &&
354
+ !(
355
+ header.minLon === 0 &&
356
+ header.minLat === 0 &&
357
+ header.maxLon === 0 &&
358
+ header.maxLat === 0
359
+ );
360
+
361
+ if (hasBounds) {
362
+ metadata['bounds'] = [
363
+ header.minLon,
364
+ header.minLat,
365
+ header.maxLon,
366
+ header.maxLat,
367
+ ];
368
+ } else {
369
+ metadata['bounds'] = [-180, -85.05112877980659, 180, 85.0511287798066];
370
+ }
371
+
372
+ if (header.centerZoom) {
373
+ metadata['center'] = [
374
+ header.centerLon,
375
+ header.centerLat,
376
+ header.centerZoom,
377
+ ];
378
+ } else {
379
+ metadata['center'] = [
380
+ header.centerLon,
381
+ header.centerLat,
382
+ parseInt(metadata['maxzoom']) / 2,
383
+ ];
384
+ }
385
+
386
+ return metadata;
387
+ } catch (error) {
388
+ lastError = error;
389
+
390
+ if (
391
+ error.message &&
392
+ error.message.includes('429') &&
393
+ attempt < maxRetries - 1
394
+ ) {
395
+ const delay = Math.pow(2, attempt) * 1000;
396
+ console.warn(
397
+ `Rate limited fetching metadata, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`,
398
+ );
399
+ await new Promise((resolve) => setTimeout(resolve, delay));
400
+ continue;
401
+ }
402
+
403
+ // If not a 429 or last retry, throw immediately
404
+ if (!error.message?.includes('429') || attempt === maxRetries - 1) {
405
+ const errorMessage = `${error.message} for file: ${inputFile}`;
406
+ throw new Error(errorMessage);
407
+ }
408
+ }
94
409
  }
95
410
 
96
- return metadata;
411
+ // This should never be reached, but just in case
412
+ throw new Error(
413
+ `Failed to get PMTiles info after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}`,
414
+ );
97
415
  }
98
416
 
99
417
  /**
100
- *
101
- * @param pmtiles
102
- * @param z
103
- * @param x
104
- * @param y
418
+ * Fetches a tile from a PMTiles archive with retry logic for rate limiting and error handling.
419
+ * @param {PMTiles} pmtiles - The PMTiles instance.
420
+ * @param {number} z - The zoom level.
421
+ * @param {number} x - The x coordinate of the tile.
422
+ * @param {number} y - The y coordinate of the tile.
423
+ * @param {number} [maxRetries] - Maximum number of retry attempts for rate-limited requests.
424
+ * @returns {Promise<object>} - A promise that resolves to an object with data (Buffer or undefined) and header (content-type).
105
425
  */
106
- export async function getPMtilesTile(pmtiles, z, x, y) {
426
+ export async function getPMtilesTile(pmtiles, z, x, y, maxRetries = 3) {
107
427
  const header = await pmtiles.getHeader();
108
428
  const tileType = getPmtilesTileType(header.tileType);
109
- let zxyTile = await pmtiles.getZxy(z, x, y);
110
- if (zxyTile && zxyTile.data) {
111
- zxyTile = Buffer.from(zxyTile.data);
112
- } else {
113
- zxyTile = undefined;
429
+
430
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
431
+ try {
432
+ let zxyTile = await pmtiles.getZxy(z, x, y);
433
+
434
+ if (zxyTile && zxyTile.data) {
435
+ zxyTile = Buffer.from(zxyTile.data);
436
+ } else {
437
+ zxyTile = undefined;
438
+ }
439
+
440
+ return { data: zxyTile, header: tileType.header };
441
+ } catch (error) {
442
+ if (
443
+ error.message &&
444
+ error.message.includes('429') &&
445
+ attempt < maxRetries - 1
446
+ ) {
447
+ const delay = Math.pow(2, attempt) * 1000;
448
+ console.warn(
449
+ `Rate limited for tile ${z}/${x}/${y}, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`,
450
+ );
451
+ await new Promise((resolve) => setTimeout(resolve, delay));
452
+ continue;
453
+ }
454
+
455
+ if (error.message && error.message.includes('Bad response code:')) {
456
+ console.error(`HTTP error for tile ${z}/${x}/${y}: ${error.message}`);
457
+ return { data: undefined, header: tileType.header };
458
+ }
459
+
460
+ throw error;
461
+ }
114
462
  }
115
- return { data: zxyTile, header: tileType.header };
463
+
464
+ console.error(
465
+ `Failed to fetch tile ${z}/${x}/${y} after ${maxRetries} attempts`,
466
+ );
467
+ return { data: undefined, header: tileType.header };
116
468
  }
117
469
 
118
470
  /**
119
- *
120
- * @param typenum
471
+ * Maps PMTiles tile type number to tile format string and Content-Type header.
472
+ * @param {number} typenum - The PMTiles tile type number (0=Unknown, 1=MVT/PBF, 2=PNG, 3=JPEG, 4=WebP, 5=AVIF).
473
+ * @returns {object} - An object containing type (string) and header (object with Content-Type).
121
474
  */
122
475
  function getPmtilesTileType(typenum) {
123
476
  let head = {};
package/src/render.js CHANGED
@@ -7,8 +7,9 @@ const mercator = new SphericalMercator();
7
7
 
8
8
  /**
9
9
  * Transforms coordinates to pixels.
10
- * @param {List[Number]} ll Longitude/Latitude coordinate pair.
11
- * @param {number} zoom Map zoom level.
10
+ * @param {Array<number>} ll - Longitude/Latitude coordinate pair.
11
+ * @param {number} zoom - Map zoom level.
12
+ * @returns {Array<number>} Pixel coordinates as [x, y].
12
13
  */
13
14
  const precisePx = (ll, zoom) => {
14
15
  const px = mercator.px(ll, 20);
@@ -18,9 +19,10 @@ const precisePx = (ll, zoom) => {
18
19
 
19
20
  /**
20
21
  * Draws a marker in canvas context.
21
- * @param {object} ctx Canvas context object.
22
- * @param {object} marker Marker object parsed by extractMarkersFromQuery.
23
- * @param {number} z Map zoom level.
22
+ * @param {CanvasRenderingContext2D} ctx - Canvas context object.
23
+ * @param {object} marker - Marker object parsed by extractMarkersFromQuery.
24
+ * @param {number} z - Map zoom level.
25
+ * @returns {Promise<void>} A promise that resolves when the marker is drawn.
24
26
  */
25
27
  const drawMarker = (ctx, marker, z) => {
26
28
  return new Promise((resolve) => {
@@ -89,9 +91,10 @@ const drawMarker = (ctx, marker, z) => {
89
91
  * Wraps drawing of markers into list of promises and awaits them.
90
92
  * It's required because images are expected to load asynchronous in canvas js
91
93
  * even when provided from a local disk.
92
- * @param {object} ctx Canvas context object.
93
- * @param {List[Object]} markers Marker objects parsed by extractMarkersFromQuery.
94
- * @param {number} z Map zoom level.
94
+ * @param {CanvasRenderingContext2D} ctx - Canvas context object.
95
+ * @param {Array<object>} markers - Marker objects parsed by extractMarkersFromQuery.
96
+ * @param {number} z - Map zoom level.
97
+ * @returns {Promise<void>} A promise that resolves when all markers are drawn.
95
98
  */
96
99
  const drawMarkers = async (ctx, markers, z) => {
97
100
  const markerPromises = [];
@@ -107,11 +110,12 @@ const drawMarkers = async (ctx, markers, z) => {
107
110
 
108
111
  /**
109
112
  * Draws a list of coordinates onto a canvas and styles the resulting path.
110
- * @param {object} ctx Canvas context object.
111
- * @param {List[Number]} path List of coordinates.
112
- * @param {object} query Request query parameters.
113
- * @param {string} pathQuery Path query parameter.
114
- * @param {number} z Map zoom level.
113
+ * @param {CanvasRenderingContext2D} ctx - Canvas context object.
114
+ * @param {Array<Array<number>>} path - List of coordinate pairs.
115
+ * @param {object} query - Request query parameters.
116
+ * @param {string} pathQuery - Path query parameter string.
117
+ * @param {number} z - Map zoom level.
118
+ * @returns {void}
115
119
  */
116
120
  const drawPath = (ctx, path, query, pathQuery, z) => {
117
121
  const splitPaths = pathQuery.split('|');
@@ -210,6 +214,21 @@ const drawPath = (ctx, path, query, pathQuery, z) => {
210
214
  ctx.stroke();
211
215
  };
212
216
 
217
+ /**
218
+ * Renders an overlay with paths and markers on a map tile.
219
+ * @param {number} z - Map zoom level.
220
+ * @param {number} x - Longitude of center point.
221
+ * @param {number} y - Latitude of center point.
222
+ * @param {number} bearing - Map bearing in degrees.
223
+ * @param {number} pitch - Map pitch in degrees.
224
+ * @param {number} w - Width of the canvas.
225
+ * @param {number} h - Height of the canvas.
226
+ * @param {number} scale - Scale factor for rendering.
227
+ * @param {Array<Array<Array<number>>>} paths - Array of path coordinate arrays.
228
+ * @param {Array<object>} markers - Array of marker objects.
229
+ * @param {object} query - Request query parameters.
230
+ * @returns {Promise<Buffer|null>} A promise that resolves with the canvas buffer or null if no overlay is needed.
231
+ */
213
232
  export const renderOverlay = async (
214
233
  z,
215
234
  x,
@@ -262,6 +281,14 @@ export const renderOverlay = async (
262
281
  return canvas.toBuffer();
263
282
  };
264
283
 
284
+ /**
285
+ * Renders a watermark on a canvas.
286
+ * @param {number} width - Width of the canvas.
287
+ * @param {number} height - Height of the canvas.
288
+ * @param {number} scale - Scale factor for rendering.
289
+ * @param {string} text - Watermark text to render.
290
+ * @returns {object} The canvas with the rendered attribution.
291
+ */
265
292
  export const renderWatermark = (width, height, scale, text) => {
266
293
  const canvas = createCanvas(scale * width, scale * height);
267
294
  const ctx = canvas.getContext('2d');
@@ -277,6 +304,14 @@ export const renderWatermark = (width, height, scale, text) => {
277
304
  return canvas;
278
305
  };
279
306
 
307
+ /**
308
+ * Renders an attribution box on a canvas.
309
+ * @param {number} width - Width of the canvas.
310
+ * @param {number} height - Height of the canvas.
311
+ * @param {number} scale - Scale factor for rendering.
312
+ * @param {string} text - Attribution text to render.
313
+ * @returns {object} The canvas with the rendered attribution.
314
+ */
280
315
  export const renderAttribution = (width, height, scale, text) => {
281
316
  const canvas = createCanvas(scale * width, scale * height);
282
317
  const ctx = canvas.getContext('2d');