tileserver-gl-light 5.5.0-pre.0 → 5.5.0-pre.11
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.
- package/CHANGELOG.md +51 -33
- package/docs/config.rst +52 -11
- package/docs/endpoints.rst +12 -2
- package/docs/installation.rst +6 -6
- package/docs/usage.rst +35 -1
- package/package.json +15 -15
- package/public/resources/elevation-control.js +92 -21
- package/public/resources/maplibre-gl-inspect.js +2827 -2770
- package/public/resources/maplibre-gl-inspect.js.map +1 -1
- package/public/resources/maplibre-gl.css +1 -1
- package/public/resources/maplibre-gl.js +4 -4
- package/public/resources/maplibre-gl.js.map +1 -1
- package/src/main.js +31 -20
- package/src/pmtiles_adapter.js +104 -45
- package/src/promises.js +1 -1
- package/src/render.js +270 -93
- package/src/serve_data.js +266 -90
- package/src/serve_font.js +2 -2
- package/src/serve_light.js +2 -4
- package/src/serve_rendered.js +445 -236
- package/src/serve_style.js +29 -8
- package/src/server.js +115 -60
- package/src/utils.js +47 -20
- package/test/elevation.js +513 -0
- package/test/fixtures/visual/encoded-path-auto.png +0 -0
- package/test/fixtures/visual/linecap-linejoin-bevel-square.png +0 -0
- package/test/fixtures/visual/linecap-linejoin-round-round.png +0 -0
- package/test/fixtures/visual/path-auto.png +0 -0
- package/test/fixtures/visual/static-bbox.png +0 -0
- package/test/fixtures/visual/static-bearing-pitch.png +0 -0
- package/test/fixtures/visual/static-bearing.png +0 -0
- package/test/fixtures/visual/static-border-global.png +0 -0
- package/test/fixtures/visual/static-lat-lng.png +0 -0
- package/test/fixtures/visual/static-markers.png +0 -0
- package/test/fixtures/visual/static-multiple-paths.png +0 -0
- package/test/fixtures/visual/static-path-border-isolated.png +0 -0
- package/test/fixtures/visual/static-path-border-stroke.png +0 -0
- package/test/fixtures/visual/static-path-latlng.png +0 -0
- package/test/fixtures/visual/static-pixel-ratio-2x.png +0 -0
- package/test/static_images.js +241 -0
- package/test/tiles_data.js +1 -1
- package/test/utils/create_terrain_mbtiles.js +124 -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
|
|
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';
|
|
@@ -81,7 +82,10 @@ program
|
|
|
81
82
|
)
|
|
82
83
|
.option(
|
|
83
84
|
'-V, --verbose [level]',
|
|
84
|
-
'More verbose output (
|
|
85
|
+
'More verbose output (level 1-3)\n' +
|
|
86
|
+
'\t-V, --verbose, -V 1, or --verbose 1: Important operations\n' +
|
|
87
|
+
'\t-V 2 or --verbose 2: Detailed operations\n' +
|
|
88
|
+
'\t-V 3 or --verbose 3: All requests and debug info',
|
|
85
89
|
(value) => {
|
|
86
90
|
// If no value provided, return 1 (boolean true case)
|
|
87
91
|
if (value === undefined || value === true) return 1;
|
|
@@ -91,6 +95,14 @@ program
|
|
|
91
95
|
return isNaN(level) ? 1 : Math.min(Math.max(level, 1), 3);
|
|
92
96
|
},
|
|
93
97
|
)
|
|
98
|
+
.option(
|
|
99
|
+
'--fetch-timeout <ms>',
|
|
100
|
+
'External fetch timeout in milliseconds for renderer HTTP requests (default 15000)',
|
|
101
|
+
(value) => {
|
|
102
|
+
const v = parseInt(value, 10);
|
|
103
|
+
return isNaN(v) ? 15000 : v;
|
|
104
|
+
},
|
|
105
|
+
)
|
|
94
106
|
.option('-s, --silent', 'Less verbose output')
|
|
95
107
|
.option('-l|--log_file <file>', 'output log file (defaults to standard out)')
|
|
96
108
|
.option(
|
|
@@ -118,6 +130,7 @@ const startServer = (configPath, config) => {
|
|
|
118
130
|
silent: opts.silent,
|
|
119
131
|
logFile: opts.log_file,
|
|
120
132
|
logFormat: opts.log_format,
|
|
133
|
+
fetchTimeout: opts.fetchTimeout,
|
|
121
134
|
publicUrl,
|
|
122
135
|
});
|
|
123
136
|
};
|
|
@@ -163,7 +176,6 @@ const startWithInputFile = async (inputFile) => {
|
|
|
163
176
|
inputFile = path.resolve(process.cwd(), inputFile);
|
|
164
177
|
inputFilePath = path.dirname(inputFile);
|
|
165
178
|
|
|
166
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Validating local file from CLI argument
|
|
167
179
|
const inputFileStats = await fsp.stat(inputFile);
|
|
168
180
|
if (!inputFileStats.isFile() || inputFileStats.size === 0) {
|
|
169
181
|
console.log(`ERROR: Not a valid input file: ${inputFile}`);
|
|
@@ -246,7 +258,7 @@ const startWithInputFile = async (inputFile) => {
|
|
|
246
258
|
}
|
|
247
259
|
}
|
|
248
260
|
|
|
249
|
-
if (opts.verbose) {
|
|
261
|
+
if (opts.verbose >= 1) {
|
|
250
262
|
console.log(JSON.stringify(config, undefined, 2));
|
|
251
263
|
} else {
|
|
252
264
|
console.log('Run with --verbose to see the config file here.');
|
|
@@ -304,7 +316,7 @@ const startWithInputFile = async (inputFile) => {
|
|
|
304
316
|
};
|
|
305
317
|
}
|
|
306
318
|
|
|
307
|
-
if (opts.verbose) {
|
|
319
|
+
if (opts.verbose >= 1) {
|
|
308
320
|
console.log(JSON.stringify(config, undefined, 2));
|
|
309
321
|
} else {
|
|
310
322
|
console.log('Run with --verbose to see the config file here.');
|
|
@@ -314,7 +326,6 @@ const startWithInputFile = async (inputFile) => {
|
|
|
314
326
|
}
|
|
315
327
|
};
|
|
316
328
|
|
|
317
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Config path from CLI argument is expected behavior
|
|
318
329
|
fs.stat(path.resolve(opts.config), async (err, stats) => {
|
|
319
330
|
if (err || !stats.isFile() || stats.size === 0) {
|
|
320
331
|
let inputFile;
|
|
@@ -331,7 +342,6 @@ fs.stat(path.resolve(opts.config), async (err, stats) => {
|
|
|
331
342
|
const files = await fsp.readdir(process.cwd());
|
|
332
343
|
for (const filename of files) {
|
|
333
344
|
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
345
|
const inputFilesStats = await fsp.stat(filename);
|
|
336
346
|
if (inputFilesStats.isFile() && inputFilesStats.size > 0) {
|
|
337
347
|
inputFile = filename;
|
|
@@ -346,25 +356,26 @@ fs.stat(path.resolve(opts.config), async (err, stats) => {
|
|
|
346
356
|
const url =
|
|
347
357
|
'https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/zurich_switzerland.mbtiles';
|
|
348
358
|
const filename = 'zurich_switzerland.mbtiles';
|
|
349
|
-
|
|
359
|
+
|
|
350
360
|
const writer = fs.createWriteStream(filename);
|
|
351
361
|
console.log(`No input file found`);
|
|
352
362
|
console.log(`[DEMO] Downloading sample data (${filename}) from ${url}`);
|
|
353
363
|
|
|
354
364
|
try {
|
|
355
|
-
const response = await
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
);
|
|
365
|
+
const response = await fetch(url);
|
|
366
|
+
|
|
367
|
+
if (!response.ok) {
|
|
368
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Convert web ReadableStream to Node.js Readable stream and pipe to file
|
|
372
|
+
const nodeStream = Readable.fromWeb(response.body);
|
|
373
|
+
await pipeline(nodeStream, writer);
|
|
374
|
+
|
|
375
|
+
console.log('Download complete');
|
|
376
|
+
startWithInputFile(filename);
|
|
366
377
|
} catch (error) {
|
|
367
|
-
console.error(`Error downloading file: ${error}`);
|
|
378
|
+
console.error(`Error downloading file: ${error.message || error}`);
|
|
368
379
|
}
|
|
369
380
|
}
|
|
370
381
|
}
|
package/src/pmtiles_adapter.js
CHANGED
|
@@ -17,31 +17,45 @@ 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 {
|
|
20
|
+
* @param {string} [s3UrlFormat] - Optional S3 URL format from config: 'aws' or 'custom'.
|
|
21
|
+
* @param {number} [verbose] - Verbosity level (1-3). 1=important, 2=detailed, 3=debug/all requests.
|
|
21
22
|
*/
|
|
22
23
|
constructor(
|
|
23
24
|
s3Url,
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
88
|
+
// Parse URL parameters
|
|
89
|
+
const [cleanUrl, queryString] = url.split('?');
|
|
67
90
|
if (queryString) {
|
|
68
91
|
const params = new URLSearchParams(queryString);
|
|
69
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
79
|
-
const
|
|
80
|
-
|
|
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
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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(
|
|
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
|
/**
|
|
@@ -115,7 +147,7 @@ class S3Source {
|
|
|
115
147
|
* @param {string|null} endpoint - The custom endpoint URL, or null for default AWS S3.
|
|
116
148
|
* @param {string} region - The AWS region.
|
|
117
149
|
* @param {string} [profile] - Optional AWS credential profile name.
|
|
118
|
-
* @param {
|
|
150
|
+
* @param {number} [verbose] - Verbosity level (1-3). 1=important, 2=detailed, 3=debug/all requests.
|
|
119
151
|
* @returns {S3Client} - Configured S3Client instance.
|
|
120
152
|
*/
|
|
121
153
|
createS3Client(endpoint, region, profile, verbose) {
|
|
@@ -276,13 +308,18 @@ async function readFileBytes(fd, buffer, offset) {
|
|
|
276
308
|
});
|
|
277
309
|
}
|
|
278
310
|
|
|
311
|
+
// Cache for PMTiles objects to avoid creating multiple instances for the same URL
|
|
312
|
+
const pmtilesCache = new Map();
|
|
313
|
+
|
|
279
314
|
/**
|
|
280
315
|
* Opens a PMTiles file from local filesystem, HTTP URL, or S3 URL.
|
|
316
|
+
* Uses caching to avoid creating multiple PMTiles instances for the same file.
|
|
281
317
|
* @param {string} filePath - The path to the PMTiles file.
|
|
282
318
|
* @param {string} [s3Profile] - Optional AWS credential profile name.
|
|
283
319
|
* @param {boolean} [requestPayer] - Optional flag for requester pays buckets.
|
|
284
320
|
* @param {string} [s3Region] - Optional AWS region.
|
|
285
|
-
* @param {
|
|
321
|
+
* @param {string} [s3UrlFormat] - Optional S3 URL format: 'aws' or 'custom'.
|
|
322
|
+
* @param {number} [verbose] - Verbosity level (1-3). 1=important, 2=detailed, 3=debug/all requests.
|
|
286
323
|
* @returns {PMTiles} - A PMTiles instance.
|
|
287
324
|
*/
|
|
288
325
|
export function openPMtiles(
|
|
@@ -290,8 +327,26 @@ export function openPMtiles(
|
|
|
290
327
|
s3Profile,
|
|
291
328
|
requestPayer,
|
|
292
329
|
s3Region,
|
|
330
|
+
s3UrlFormat,
|
|
293
331
|
verbose = 0,
|
|
294
332
|
) {
|
|
333
|
+
// Create a cache key that includes all parameters that affect the source
|
|
334
|
+
const cacheKey = JSON.stringify({
|
|
335
|
+
filePath,
|
|
336
|
+
s3Profile,
|
|
337
|
+
requestPayer,
|
|
338
|
+
s3Region,
|
|
339
|
+
s3UrlFormat,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Check if we already have a PMTiles object for this configuration
|
|
343
|
+
if (pmtilesCache.has(cacheKey)) {
|
|
344
|
+
if (verbose >= 2) {
|
|
345
|
+
console.log(`Using cached PMTiles instance for: ${filePath}`);
|
|
346
|
+
}
|
|
347
|
+
return pmtilesCache.get(cacheKey);
|
|
348
|
+
}
|
|
349
|
+
|
|
295
350
|
let pmtiles = undefined;
|
|
296
351
|
|
|
297
352
|
if (isS3Url(filePath)) {
|
|
@@ -303,6 +358,7 @@ export function openPMtiles(
|
|
|
303
358
|
s3Profile,
|
|
304
359
|
requestPayer,
|
|
305
360
|
s3Region,
|
|
361
|
+
s3UrlFormat,
|
|
306
362
|
verbose,
|
|
307
363
|
);
|
|
308
364
|
pmtiles = new PMTiles(source);
|
|
@@ -316,12 +372,15 @@ export function openPMtiles(
|
|
|
316
372
|
if (verbose >= 2) {
|
|
317
373
|
console.log(`Opening PMTiles from local file: ${filePath}`);
|
|
318
374
|
}
|
|
319
|
-
|
|
375
|
+
|
|
320
376
|
const fd = fs.openSync(filePath, 'r');
|
|
321
377
|
const source = new PMTilesFileSource(fd);
|
|
322
378
|
pmtiles = new PMTiles(source);
|
|
323
379
|
}
|
|
324
380
|
|
|
381
|
+
// Cache the PMTiles object
|
|
382
|
+
pmtilesCache.set(cacheKey, pmtiles);
|
|
383
|
+
|
|
325
384
|
return pmtiles;
|
|
326
385
|
}
|
|
327
386
|
|