tileserver-gl-light 5.5.0-pre.1 → 5.5.0-pre.3

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 (35) hide show
  1. package/CHANGELOG.md +6 -2
  2. package/docs/config.rst +41 -1
  3. package/docs/usage.rst +5 -0
  4. package/package.json +11 -11
  5. package/public/resources/elevation-control.js +1 -1
  6. package/public/resources/maplibre-gl-inspect.js +2823 -2770
  7. package/public/resources/maplibre-gl-inspect.js.map +1 -1
  8. package/public/resources/maplibre-gl.css +1 -1
  9. package/public/resources/maplibre-gl.js +4 -4
  10. package/public/resources/maplibre-gl.js.map +1 -1
  11. package/src/main.js +16 -17
  12. package/src/pmtiles_adapter.js +77 -42
  13. package/src/promises.js +1 -1
  14. package/src/render.js +270 -93
  15. package/src/serve_data.js +8 -12
  16. package/src/serve_light.js +0 -1
  17. package/src/serve_rendered.js +375 -205
  18. package/src/server.js +30 -27
  19. package/src/utils.js +18 -19
  20. package/test/fixtures/visual/encoded-path-auto.png +0 -0
  21. package/test/fixtures/visual/linecap-linejoin-bevel-square.png +0 -0
  22. package/test/fixtures/visual/linecap-linejoin-round-round.png +0 -0
  23. package/test/fixtures/visual/path-auto.png +0 -0
  24. package/test/fixtures/visual/static-bbox.png +0 -0
  25. package/test/fixtures/visual/static-bearing-pitch.png +0 -0
  26. package/test/fixtures/visual/static-bearing.png +0 -0
  27. package/test/fixtures/visual/static-border-global.png +0 -0
  28. package/test/fixtures/visual/static-lat-lng.png +0 -0
  29. package/test/fixtures/visual/static-markers.png +0 -0
  30. package/test/fixtures/visual/static-multiple-paths.png +0 -0
  31. package/test/fixtures/visual/static-path-border-isolated.png +0 -0
  32. package/test/fixtures/visual/static-path-border-stroke.png +0 -0
  33. package/test/fixtures/visual/static-path-latlng.png +0 -0
  34. package/test/fixtures/visual/static-pixel-ratio-2x.png +0 -0
  35. 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
  }
@@ -17,6 +17,7 @@ class S3Source {
17
17
  * @param {string} [s3Profile] - Optional AWS credential profile name from config.
18
18
  * @param {boolean} [configRequestPayer] - Optional flag from config for requester pays buckets.
19
19
  * @param {string} [configRegion] - Optional AWS region from config.
20
+ * @param {string} [s3UrlFormat] - Optional S3 URL format from config: 'aws' or 'custom'.
20
21
  * @param {boolean} [verbose] - Whether to show verbose logging.
21
22
  */
22
23
  constructor(
@@ -24,24 +25,37 @@ class S3Source {
24
25
  s3Profile,
25
26
  configRequestPayer,
26
27
  configRegion,
28
+ s3UrlFormat,
27
29
  verbose = false,
28
30
  ) {
29
- const parsed = this.parseS3Url(s3Url);
31
+ const parsed = this.parseS3Url(s3Url, s3UrlFormat);
30
32
  this.bucket = parsed.bucket;
31
33
  this.key = parsed.key;
32
34
  this.endpoint = parsed.endpoint;
33
35
  this.url = s3Url;
34
36
  this.verbose = verbose;
35
37
 
36
- // Determine the final profile: Config takes precedence over URL
38
+ // Apply configuration precedence: Config > URL > Default
39
+ // Using || for strings (empty string = not set)
40
+ // Using ?? for booleans (false is valid value)
37
41
  const profile = s3Profile || parsed.profile;
38
-
39
- // Determine requestPayer: Config takes precedence over URL
40
42
  this.requestPayer = configRequestPayer ?? parsed.requestPayer;
41
-
42
- // Determine region: Config takes precedence over URL
43
43
  this.region = configRegion || parsed.region;
44
44
 
45
+ // Log precedence decisions for debugging
46
+ if (verbose >= 3) {
47
+ console.log(`S3 config precedence for ${s3Url}:`);
48
+ console.log(
49
+ ` Profile: ${s3Profile ? 'config' : parsed.profile ? 'url' : 'default'} = ${profile || 'none'}`,
50
+ );
51
+ console.log(
52
+ ` Region: ${configRegion ? 'config' : parsed.region !== (process.env.AWS_REGION || 'us-east-1') ? 'url' : 'env/default'} = ${this.region}`,
53
+ );
54
+ console.log(
55
+ ` RequestPayer: ${configRequestPayer !== undefined ? 'config' : parsed.requestPayer ? 'url' : 'default'} = ${this.requestPayer}`,
56
+ );
57
+ }
58
+
45
59
  // Create S3 client
46
60
  this.s3Client = this.createS3Client(
47
61
  parsed.endpoint,
@@ -54,60 +68,78 @@ class S3Source {
54
68
  /**
55
69
  * Parses various S3 URL formats into bucket, key, endpoint, region, and profile.
56
70
  * @param {string} url - The S3 URL to parse.
71
+ * @param {string} [s3UrlFormat] - Optional format override: 'aws' or 'custom'.
57
72
  * @returns {object} - An object containing bucket, key, endpoint, region, and profile.
58
73
  * @throws {Error} - Throws an error if the URL format is invalid.
59
74
  */
60
- parseS3Url(url) {
61
- // Initialize with defaults/environment
75
+ parseS3Url(url, s3UrlFormat) {
76
+ // Validate s3UrlFormat if provided
77
+ if (s3UrlFormat && s3UrlFormat !== 'aws' && s3UrlFormat !== 'custom') {
78
+ console.warn(
79
+ `Invalid s3UrlFormat: "${s3UrlFormat}". Must be "aws" or "custom". Using auto-detection.`,
80
+ );
81
+ s3UrlFormat = undefined;
82
+ }
83
+
62
84
  let region = process.env.AWS_REGION || 'us-east-1';
63
85
  let profile = null;
64
86
  let requestPayer = false;
65
87
 
66
- const queryString = url.split('?')[1];
88
+ // Parse URL parameters
89
+ const [cleanUrl, queryString] = url.split('?');
67
90
  if (queryString) {
68
91
  const params = new URLSearchParams(queryString);
69
- // Update profile if provided in url parameters
92
+ // URL parameters override defaults
70
93
  profile = params.get('profile') ?? profile;
71
- // Update region if provided in url parameters
72
94
  region = params.get('region') ?? region;
73
- // Update requestPayer if provided in url parameters
95
+ s3UrlFormat = s3UrlFormat ?? params.get('s3UrlFormat'); // Config overrides URL
96
+
74
97
  const payerVal = params.get('requestPayer');
75
98
  requestPayer = payerVal === 'true' || payerVal === '1';
76
99
  }
77
100
 
78
- // Clean URL for format detection (remove trailing slashes)
79
- const baseUrl = url.split('?')[0];
80
- const cleanUrl = baseUrl.replace(/\/+$/, '');
101
+ // Helper to build result object
102
+ const buildResult = (endpoint, bucket, key) => ({
103
+ endpoint: endpoint ? `https://${endpoint}` : null,
104
+ bucket,
105
+ key,
106
+ region,
107
+ profile,
108
+ requestPayer,
109
+ });
81
110
 
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
- }
111
+ // Define patterns based on format
112
+ const patterns = {
113
+ customWithDot: /^s3:\/\/([^/]*\.[^/]+)\/([^/]+)\/(.+)$/, // Auto-detect: requires dot
114
+ customForced: /^s3:\/\/([^/]+)\/([^/]+)\/(.+)$/, // Explicit: no dot required
115
+ aws: /^s3:\/\/([^/]+)\/(.+)$/,
116
+ };
95
117
 
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
- };
118
+ // Match based on s3UrlFormat or auto-detect
119
+ let match;
120
+
121
+ if (s3UrlFormat === 'custom') {
122
+ match = cleanUrl.match(patterns.customForced);
123
+ if (match) return buildResult(match[1], match[2], match[3]);
124
+ } else if (s3UrlFormat === 'aws') {
125
+ match = cleanUrl.match(patterns.aws);
126
+ if (match) return buildResult(null, match[1], match[2]);
127
+ } else {
128
+ // Auto-detection: try custom (with dot) first, then AWS
129
+ match = cleanUrl.match(patterns.customWithDot);
130
+ if (match) return buildResult(match[1], match[2], match[3]);
131
+
132
+ match = cleanUrl.match(patterns.aws);
133
+ if (match) return buildResult(null, match[1], match[2]);
108
134
  }
109
135
 
110
- throw new Error(`Invalid S3 URL format: ${url}`);
136
+ throw new Error(
137
+ `Invalid S3 URL format: ${url}\n` +
138
+ `Expected formats:\n` +
139
+ ` AWS S3: s3://bucket-name/path/to/file.pmtiles\n` +
140
+ ` Custom endpoint: s3://endpoint.com/bucket/path/to/file.pmtiles\n` +
141
+ `Use s3UrlFormat parameter to override auto-detection if needed.`,
142
+ );
111
143
  }
112
144
 
113
145
  /**
@@ -282,6 +314,7 @@ async function readFileBytes(fd, buffer, offset) {
282
314
  * @param {string} [s3Profile] - Optional AWS credential profile name.
283
315
  * @param {boolean} [requestPayer] - Optional flag for requester pays buckets.
284
316
  * @param {string} [s3Region] - Optional AWS region.
317
+ * @param {string} [s3UrlFormat] - Optional S3 URL format: 'aws' or 'custom'.
285
318
  * @param {boolean} [verbose] - Whether to show verbose logging.
286
319
  * @returns {PMTiles} - A PMTiles instance.
287
320
  */
@@ -290,6 +323,7 @@ export function openPMtiles(
290
323
  s3Profile,
291
324
  requestPayer,
292
325
  s3Region,
326
+ s3UrlFormat,
293
327
  verbose = 0,
294
328
  ) {
295
329
  let pmtiles = undefined;
@@ -303,6 +337,7 @@ export function openPMtiles(
303
337
  s3Profile,
304
338
  requestPayer,
305
339
  s3Region,
340
+ s3UrlFormat,
306
341
  verbose,
307
342
  );
308
343
  pmtiles = new PMTiles(source);
@@ -316,7 +351,7 @@ export function openPMtiles(
316
351
  if (verbose >= 2) {
317
352
  console.log(`Opening PMTiles from local file: ${filePath}`);
318
353
  }
319
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- Opening local PMTiles file specified in config or CLI
354
+
320
355
  const fd = fs.openSync(filePath, 'r');
321
356
  const source = new PMTilesFileSource(fd);
322
357
  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
  };