tileserver-gl-light 5.5.0-pre.5 → 5.5.0-pre.7
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 +4 -1
- package/docs/config.rst +11 -10
- package/docs/usage.rst +21 -0
- package/package.json +5 -5
- package/public/resources/maplibre-gl-inspect.js +12 -8
- package/public/resources/maplibre-gl-inspect.js.map +1 -1
- package/src/main.js +15 -3
- package/src/pmtiles_adapter.js +27 -3
- package/src/serve_data.js +22 -11
- package/src/serve_font.js +2 -2
- package/src/serve_rendered.js +84 -49
- package/src/serve_style.js +29 -8
- package/src/server.js +87 -35
- package/src/utils.js +1 -2
- package/test/tiles_data.js +1 -1
package/src/main.js
CHANGED
|
@@ -82,7 +82,10 @@ program
|
|
|
82
82
|
)
|
|
83
83
|
.option(
|
|
84
84
|
'-V, --verbose [level]',
|
|
85
|
-
'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',
|
|
86
89
|
(value) => {
|
|
87
90
|
// If no value provided, return 1 (boolean true case)
|
|
88
91
|
if (value === undefined || value === true) return 1;
|
|
@@ -92,6 +95,14 @@ program
|
|
|
92
95
|
return isNaN(level) ? 1 : Math.min(Math.max(level, 1), 3);
|
|
93
96
|
},
|
|
94
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
|
+
)
|
|
95
106
|
.option('-s, --silent', 'Less verbose output')
|
|
96
107
|
.option('-l|--log_file <file>', 'output log file (defaults to standard out)')
|
|
97
108
|
.option(
|
|
@@ -119,6 +130,7 @@ const startServer = (configPath, config) => {
|
|
|
119
130
|
silent: opts.silent,
|
|
120
131
|
logFile: opts.log_file,
|
|
121
132
|
logFormat: opts.log_format,
|
|
133
|
+
fetchTimeout: opts.fetchTimeout,
|
|
122
134
|
publicUrl,
|
|
123
135
|
});
|
|
124
136
|
};
|
|
@@ -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.');
|
package/src/pmtiles_adapter.js
CHANGED
|
@@ -18,7 +18,7 @@ class S3Source {
|
|
|
18
18
|
* @param {boolean} [configRequestPayer] - Optional flag from config for requester pays buckets.
|
|
19
19
|
* @param {string} [configRegion] - Optional AWS region from config.
|
|
20
20
|
* @param {string} [s3UrlFormat] - Optional S3 URL format from config: 'aws' or 'custom'.
|
|
21
|
-
* @param {
|
|
21
|
+
* @param {number} [verbose] - Verbosity level (1-3). 1=important, 2=detailed, 3=debug/all requests.
|
|
22
22
|
*/
|
|
23
23
|
constructor(
|
|
24
24
|
s3Url,
|
|
@@ -147,7 +147,7 @@ class S3Source {
|
|
|
147
147
|
* @param {string|null} endpoint - The custom endpoint URL, or null for default AWS S3.
|
|
148
148
|
* @param {string} region - The AWS region.
|
|
149
149
|
* @param {string} [profile] - Optional AWS credential profile name.
|
|
150
|
-
* @param {
|
|
150
|
+
* @param {number} [verbose] - Verbosity level (1-3). 1=important, 2=detailed, 3=debug/all requests.
|
|
151
151
|
* @returns {S3Client} - Configured S3Client instance.
|
|
152
152
|
*/
|
|
153
153
|
createS3Client(endpoint, region, profile, verbose) {
|
|
@@ -308,14 +308,18 @@ async function readFileBytes(fd, buffer, offset) {
|
|
|
308
308
|
});
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
// Cache for PMTiles objects to avoid creating multiple instances for the same URL
|
|
312
|
+
const pmtilesCache = new Map();
|
|
313
|
+
|
|
311
314
|
/**
|
|
312
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.
|
|
313
317
|
* @param {string} filePath - The path to the PMTiles file.
|
|
314
318
|
* @param {string} [s3Profile] - Optional AWS credential profile name.
|
|
315
319
|
* @param {boolean} [requestPayer] - Optional flag for requester pays buckets.
|
|
316
320
|
* @param {string} [s3Region] - Optional AWS region.
|
|
317
321
|
* @param {string} [s3UrlFormat] - Optional S3 URL format: 'aws' or 'custom'.
|
|
318
|
-
* @param {
|
|
322
|
+
* @param {number} [verbose] - Verbosity level (1-3). 1=important, 2=detailed, 3=debug/all requests.
|
|
319
323
|
* @returns {PMTiles} - A PMTiles instance.
|
|
320
324
|
*/
|
|
321
325
|
export function openPMtiles(
|
|
@@ -326,6 +330,23 @@ export function openPMtiles(
|
|
|
326
330
|
s3UrlFormat,
|
|
327
331
|
verbose = 0,
|
|
328
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
|
+
|
|
329
350
|
let pmtiles = undefined;
|
|
330
351
|
|
|
331
352
|
if (isS3Url(filePath)) {
|
|
@@ -357,6 +378,9 @@ export function openPMtiles(
|
|
|
357
378
|
pmtiles = new PMTiles(source);
|
|
358
379
|
}
|
|
359
380
|
|
|
381
|
+
// Cache the PMTiles object
|
|
382
|
+
pmtilesCache.set(cacheKey, pmtiles);
|
|
383
|
+
|
|
360
384
|
return pmtiles;
|
|
361
385
|
}
|
|
362
386
|
|
package/src/serve_data.js
CHANGED
|
@@ -58,7 +58,7 @@ export const serve_data = {
|
|
|
58
58
|
* @returns {Promise<void>}
|
|
59
59
|
*/
|
|
60
60
|
app.get('/:id/:z/:x/:y.:format', async (req, res) => {
|
|
61
|
-
if (verbose) {
|
|
61
|
+
if (verbose >= 1) {
|
|
62
62
|
console.log(
|
|
63
63
|
`Handling tile request for: /data/%s/%s/%s/%s.%s`,
|
|
64
64
|
String(req.params.id).replace(/\n|\r/g, ''),
|
|
@@ -109,10 +109,10 @@ export const serve_data = {
|
|
|
109
109
|
x,
|
|
110
110
|
y,
|
|
111
111
|
);
|
|
112
|
-
if (fetchTile == null
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return res.status(204).send();
|
|
112
|
+
if (fetchTile == null) {
|
|
113
|
+
// sparse=true (default) -> 404 (allows overzoom)
|
|
114
|
+
// sparse=false -> 204 (empty tile, no overzoom)
|
|
115
|
+
return res.status(item.sparse ? 404 : 204).send();
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
let data = fetchTile.data;
|
|
@@ -180,7 +180,7 @@ export const serve_data = {
|
|
|
180
180
|
*/
|
|
181
181
|
app.get('/:id/elevation/:z/:x/:y', async (req, res, next) => {
|
|
182
182
|
try {
|
|
183
|
-
if (verbose) {
|
|
183
|
+
if (verbose >= 1) {
|
|
184
184
|
console.log(
|
|
185
185
|
`Handling elevation request for: /data/%s/elevation/%s/%s/%s`,
|
|
186
186
|
String(req.params.id).replace(/\n|\r/g, ''),
|
|
@@ -261,7 +261,11 @@ export const serve_data = {
|
|
|
261
261
|
xy[0],
|
|
262
262
|
xy[1],
|
|
263
263
|
);
|
|
264
|
-
if (fetchTile == null)
|
|
264
|
+
if (fetchTile == null) {
|
|
265
|
+
// sparse=true (default) -> 404 (allows overzoom)
|
|
266
|
+
// sparse=false -> 204 (empty tile, no overzoom)
|
|
267
|
+
return res.status(item.sparse ? 404 : 204).send();
|
|
268
|
+
}
|
|
265
269
|
|
|
266
270
|
let data = fetchTile.data;
|
|
267
271
|
var param = {
|
|
@@ -294,7 +298,7 @@ export const serve_data = {
|
|
|
294
298
|
* @returns {Promise<void>}
|
|
295
299
|
*/
|
|
296
300
|
app.get('/:id.json', (req, res) => {
|
|
297
|
-
if (verbose) {
|
|
301
|
+
if (verbose >= 1) {
|
|
298
302
|
console.log(
|
|
299
303
|
`Handling tilejson request for: /data/%s.json`,
|
|
300
304
|
String(req.params.id).replace(/\n|\r/g, ''),
|
|
@@ -331,7 +335,7 @@ export const serve_data = {
|
|
|
331
335
|
* @param {string} id ID of the data source.
|
|
332
336
|
* @param {object} programOpts - An object containing the program options
|
|
333
337
|
* @param {string} programOpts.publicUrl Public URL for the data.
|
|
334
|
-
* @param {
|
|
338
|
+
* @param {number} programOpts.verbose Verbosity level (1-3). 1=important, 2=detailed, 3=debug/all requests.
|
|
335
339
|
* @returns {Promise<void>}
|
|
336
340
|
*/
|
|
337
341
|
add: async function (options, repo, params, id, programOpts) {
|
|
@@ -359,7 +363,7 @@ export const serve_data = {
|
|
|
359
363
|
}
|
|
360
364
|
}
|
|
361
365
|
|
|
362
|
-
if (verbose
|
|
366
|
+
if (verbose >= 1) {
|
|
363
367
|
console.log(`[INFO] Loading data source '${id}' from: ${inputFile}`);
|
|
364
368
|
}
|
|
365
369
|
|
|
@@ -381,7 +385,6 @@ export const serve_data = {
|
|
|
381
385
|
tileJSON['format'] = 'pbf';
|
|
382
386
|
tileJSON['encoding'] = params['encoding'];
|
|
383
387
|
tileJSON['tileSize'] = params['tileSize'];
|
|
384
|
-
tileJSON['sparse'] = params['sparse'];
|
|
385
388
|
|
|
386
389
|
if (inputType === 'pmtiles') {
|
|
387
390
|
source = openPMtiles(
|
|
@@ -415,12 +418,20 @@ export const serve_data = {
|
|
|
415
418
|
tileJSON = options.dataDecoratorFunc(id, 'tilejson', tileJSON);
|
|
416
419
|
}
|
|
417
420
|
|
|
421
|
+
// Determine sparse: per-source overrides global, then format-based default
|
|
422
|
+
// sparse=true -> 404 (allows overzoom)
|
|
423
|
+
// sparse=false -> 204 (empty tile, no overzoom)
|
|
424
|
+
// Default: vector tiles (pbf) -> false, raster tiles -> true
|
|
425
|
+
const isVector = tileJSON.format === 'pbf';
|
|
426
|
+
const sparse = params.sparse ?? options.sparse ?? !isVector;
|
|
427
|
+
|
|
418
428
|
// eslint-disable-next-line security/detect-object-injection -- id is from config file data source names
|
|
419
429
|
repo[id] = {
|
|
420
430
|
tileJSON,
|
|
421
431
|
publicUrl,
|
|
422
432
|
source,
|
|
423
433
|
sourceType,
|
|
434
|
+
sparse,
|
|
424
435
|
};
|
|
425
436
|
},
|
|
426
437
|
};
|
package/src/serve_font.js
CHANGED
|
@@ -36,7 +36,7 @@ export async function serve_font(options, allowedFonts, programOpts) {
|
|
|
36
36
|
'',
|
|
37
37
|
);
|
|
38
38
|
|
|
39
|
-
if (verbose) {
|
|
39
|
+
if (verbose >= 1) {
|
|
40
40
|
console.log(
|
|
41
41
|
`Handling font request for: /fonts/%s/%s.pbf`,
|
|
42
42
|
sFontStack,
|
|
@@ -86,7 +86,7 @@ export async function serve_font(options, allowedFonts, programOpts) {
|
|
|
86
86
|
* @returns {void}
|
|
87
87
|
*/
|
|
88
88
|
app.get('/fonts.json', (req, res) => {
|
|
89
|
-
if (verbose) {
|
|
89
|
+
if (verbose >= 1) {
|
|
90
90
|
console.log('Handling list font request for /fonts.json');
|
|
91
91
|
}
|
|
92
92
|
res.header('Content-type', 'application/json');
|
package/src/serve_rendered.js
CHANGED
|
@@ -1094,7 +1094,7 @@ export const serve_rendered = {
|
|
|
1094
1094
|
(!p1 && p2 === 'static') || (p1 === 'static' && p2 === 'raw')
|
|
1095
1095
|
? 'static'
|
|
1096
1096
|
: 'tile';
|
|
1097
|
-
if (verbose) {
|
|
1097
|
+
if (verbose >= 3) {
|
|
1098
1098
|
console.log(
|
|
1099
1099
|
`Handling rendered %s request for: /styles/%s%s/%s/%s/%s%s.%s`,
|
|
1100
1100
|
requestType,
|
|
@@ -1154,7 +1154,7 @@ export const serve_rendered = {
|
|
|
1154
1154
|
return res.sendStatus(404);
|
|
1155
1155
|
}
|
|
1156
1156
|
const tileSize = parseInt(req.params.tileSize, 10) || undefined;
|
|
1157
|
-
if (verbose) {
|
|
1157
|
+
if (verbose >= 3) {
|
|
1158
1158
|
console.log(
|
|
1159
1159
|
`Handling rendered tilejson request for: /styles/%s%s.json`,
|
|
1160
1160
|
req.params.tileSize
|
|
@@ -1205,11 +1205,16 @@ export const serve_rendered = {
|
|
|
1205
1205
|
renderersStatic: [],
|
|
1206
1206
|
sources: {},
|
|
1207
1207
|
sourceTypes: {},
|
|
1208
|
+
sparseFlags: {},
|
|
1208
1209
|
};
|
|
1209
1210
|
|
|
1210
|
-
const { publicUrl, verbose } = programOpts;
|
|
1211
|
+
const { publicUrl, verbose, fetchTimeout } = programOpts;
|
|
1211
1212
|
|
|
1212
1213
|
const styleJSON = clone(style);
|
|
1214
|
+
|
|
1215
|
+
// Global sparse flag for HTTP/remote sources (from config options)
|
|
1216
|
+
const globalSparse = options.sparse ?? true;
|
|
1217
|
+
|
|
1213
1218
|
/**
|
|
1214
1219
|
* Creates a pool of renderers.
|
|
1215
1220
|
* @param {number} ratio Pixel ratio
|
|
@@ -1231,7 +1236,7 @@ export const serve_rendered = {
|
|
|
1231
1236
|
ratio,
|
|
1232
1237
|
request: async (req, callback) => {
|
|
1233
1238
|
const protocol = req.url.split(':')[0];
|
|
1234
|
-
if (verbose
|
|
1239
|
+
if (verbose >= 3) {
|
|
1235
1240
|
console.log('Handling request:', req);
|
|
1236
1241
|
}
|
|
1237
1242
|
if (protocol === 'sprites') {
|
|
@@ -1287,22 +1292,18 @@ export const serve_rendered = {
|
|
|
1287
1292
|
x,
|
|
1288
1293
|
y,
|
|
1289
1294
|
);
|
|
1290
|
-
if (fetchTile == null
|
|
1291
|
-
if (verbose) {
|
|
1292
|
-
console.log(
|
|
1293
|
-
'fetchTile warning on %s, sparse response',
|
|
1294
|
-
req.url,
|
|
1295
|
-
);
|
|
1295
|
+
if (fetchTile == null) {
|
|
1296
|
+
if (verbose >= 2) {
|
|
1297
|
+
console.log('fetchTile null on %s', req.url);
|
|
1296
1298
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
if (
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
req.url,
|
|
1304
|
-
);
|
|
1299
|
+
// eslint-disable-next-line security/detect-object-injection -- sourceId from internal style source names
|
|
1300
|
+
const sparse = map.sparseFlags[sourceId] ?? true;
|
|
1301
|
+
// sparse=true (default) -> return empty callback so MapLibre can overzoom
|
|
1302
|
+
if (sparse) {
|
|
1303
|
+
callback();
|
|
1304
|
+
return;
|
|
1305
1305
|
}
|
|
1306
|
+
// sparse=false -> 204 (empty tile, no overzoom) - create blank response
|
|
1306
1307
|
createEmptyResponse(
|
|
1307
1308
|
sourceInfo.format,
|
|
1308
1309
|
sourceInfo.color,
|
|
@@ -1341,40 +1342,58 @@ export const serve_rendered = {
|
|
|
1341
1342
|
|
|
1342
1343
|
callback(null, response);
|
|
1343
1344
|
} else if (protocol === 'http' || protocol === 'https') {
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
|
1345
|
+
const controller = new AbortController();
|
|
1346
|
+
const timeoutMs = (fetchTimeout && Number(fetchTimeout)) || 15000;
|
|
1347
|
+
let timeoutId;
|
|
1348
1348
|
|
|
1349
|
+
try {
|
|
1350
|
+
timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1349
1351
|
const response = await fetch(req.url, {
|
|
1350
1352
|
signal: controller.signal,
|
|
1351
1353
|
});
|
|
1352
|
-
|
|
1353
1354
|
clearTimeout(timeoutId);
|
|
1354
1355
|
|
|
1355
|
-
//
|
|
1356
|
-
if (response.status ===
|
|
1357
|
-
|
|
1356
|
+
// HTTP 204 No Content means "empty tile" - generate a blank tile
|
|
1357
|
+
if (response.status === 204) {
|
|
1358
|
+
const parts = url.parse(req.url);
|
|
1359
|
+
const extension = path.extname(parts.pathname).toLowerCase();
|
|
1360
|
+
// eslint-disable-next-line security/detect-object-injection -- extension is from path.extname, limited set
|
|
1361
|
+
const format = extensionToFormat[extension] || '';
|
|
1362
|
+
createEmptyResponse(format, '', callback);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
if (!response.ok) {
|
|
1367
|
+
if (verbose >= 2) {
|
|
1358
1368
|
console.log(
|
|
1359
|
-
'fetchTile
|
|
1369
|
+
'fetchTile HTTP %d on %s, %s',
|
|
1370
|
+
response.status,
|
|
1360
1371
|
req.url,
|
|
1372
|
+
globalSparse
|
|
1373
|
+
? 'allowing overzoom'
|
|
1374
|
+
: 'creating empty tile',
|
|
1361
1375
|
);
|
|
1362
1376
|
}
|
|
1363
|
-
callback();
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
1377
|
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1378
|
+
if (globalSparse) {
|
|
1379
|
+
// sparse=true -> allow overzoom
|
|
1380
|
+
callback();
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// sparse=false (default) -> create empty tile
|
|
1385
|
+
const parts = url.parse(req.url);
|
|
1386
|
+
const extension = path.extname(parts.pathname).toLowerCase();
|
|
1387
|
+
// eslint-disable-next-line security/detect-object-injection -- extension is from path.extname, limited set
|
|
1388
|
+
const format = extensionToFormat[extension] || '';
|
|
1389
|
+
createEmptyResponse(format, '', callback);
|
|
1390
|
+
return;
|
|
1372
1391
|
}
|
|
1373
1392
|
|
|
1374
1393
|
const responseHeaders = response.headers;
|
|
1375
1394
|
const responseData = await response.arrayBuffer();
|
|
1376
|
-
|
|
1377
1395
|
const parsedResponse = {};
|
|
1396
|
+
|
|
1378
1397
|
if (responseHeaders.get('last-modified')) {
|
|
1379
1398
|
parsedResponse.modified = new Date(
|
|
1380
1399
|
responseHeaders.get('last-modified'),
|
|
@@ -1392,8 +1411,7 @@ export const serve_rendered = {
|
|
|
1392
1411
|
parsedResponse.data = Buffer.from(responseData);
|
|
1393
1412
|
callback(null, parsedResponse);
|
|
1394
1413
|
} catch (error) {
|
|
1395
|
-
// Log DNS failures
|
|
1396
|
-
// Native fetch wraps DNS errors in error.cause
|
|
1414
|
+
// Log DNS failures
|
|
1397
1415
|
if (error.cause?.code === 'ENOTFOUND') {
|
|
1398
1416
|
console.error(
|
|
1399
1417
|
`DNS RESOLUTION FAILED for ${req.url}. ` +
|
|
@@ -1402,20 +1420,27 @@ export const serve_rendered = {
|
|
|
1402
1420
|
);
|
|
1403
1421
|
}
|
|
1404
1422
|
|
|
1405
|
-
//
|
|
1423
|
+
// Log timeout
|
|
1406
1424
|
if (error.name === 'AbortError') {
|
|
1407
1425
|
console.error(
|
|
1408
1426
|
`FETCH TIMEOUT for ${req.url}. ` +
|
|
1409
|
-
`The request took longer than
|
|
1427
|
+
`The request took longer than ${timeoutMs} ms to complete.`,
|
|
1410
1428
|
);
|
|
1411
1429
|
}
|
|
1412
1430
|
|
|
1413
|
-
//
|
|
1431
|
+
// Log all other errors
|
|
1414
1432
|
console.error(
|
|
1415
1433
|
`Error fetching remote URL ${req.url}:`,
|
|
1416
1434
|
error.message || error,
|
|
1417
1435
|
);
|
|
1418
1436
|
|
|
1437
|
+
if (globalSparse) {
|
|
1438
|
+
// sparse=true -> allow overzoom
|
|
1439
|
+
callback();
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// sparse=false (default) -> create empty tile
|
|
1419
1444
|
const parts = url.parse(req.url);
|
|
1420
1445
|
const extension = path.extname(parts.pathname).toLowerCase();
|
|
1421
1446
|
// eslint-disable-next-line security/detect-object-injection -- extension is from path.extname, limited set
|
|
@@ -1497,7 +1522,7 @@ export const serve_rendered = {
|
|
|
1497
1522
|
|
|
1498
1523
|
// Remove (flatten) 3D buildings
|
|
1499
1524
|
if (layer.paint['fill-extrusion-height']) {
|
|
1500
|
-
if (verbose) {
|
|
1525
|
+
if (verbose >= 1) {
|
|
1501
1526
|
console.warn(
|
|
1502
1527
|
`Warning: Layer '${layerIdForWarning}' in style '${id}' has property 'fill-extrusion-height'. ` +
|
|
1503
1528
|
`3D extrusion may appear distorted or misleading when rendered as a static image due to camera angle limitations. ` +
|
|
@@ -1508,7 +1533,7 @@ export const serve_rendered = {
|
|
|
1508
1533
|
layer.paint['fill-extrusion-height'] = 0;
|
|
1509
1534
|
}
|
|
1510
1535
|
if (layer.paint['fill-extrusion-base']) {
|
|
1511
|
-
if (verbose) {
|
|
1536
|
+
if (verbose >= 1) {
|
|
1512
1537
|
console.warn(
|
|
1513
1538
|
`Warning: Layer '${layerIdForWarning}' in style '${id}' has property 'fill-extrusion-base'. ` +
|
|
1514
1539
|
`3D extrusion may appear distorted or misleading when rendered as a static image due to camera angle limitations. ` +
|
|
@@ -1528,7 +1553,7 @@ export const serve_rendered = {
|
|
|
1528
1553
|
|
|
1529
1554
|
for (const prop of hillshadePropertiesToRemove) {
|
|
1530
1555
|
if (prop in layer.paint) {
|
|
1531
|
-
if (verbose) {
|
|
1556
|
+
if (verbose >= 1) {
|
|
1532
1557
|
console.warn(
|
|
1533
1558
|
`Warning: Layer '${layerIdForWarning}' in style '${id}' has property '${prop}'. ` +
|
|
1534
1559
|
`This property is not supported by MapLibre Native. ` +
|
|
@@ -1543,7 +1568,7 @@ export const serve_rendered = {
|
|
|
1543
1568
|
|
|
1544
1569
|
// --- Remove 'hillshade-shadow-color' if it is an array. It can only be a string in MapLibre Native ---
|
|
1545
1570
|
if (Array.isArray(layer.paint['hillshade-shadow-color'])) {
|
|
1546
|
-
if (verbose) {
|
|
1571
|
+
if (verbose >= 1) {
|
|
1547
1572
|
console.warn(
|
|
1548
1573
|
`Warning: Layer '${layerIdForWarning}' in style '${id}' has property 'hillshade-shadow-color'. ` +
|
|
1549
1574
|
`An array value is not supported by MapLibre Native for this property (expected string/color). ` +
|
|
@@ -1589,7 +1614,6 @@ export const serve_rendered = {
|
|
|
1589
1614
|
|
|
1590
1615
|
for (const name of Object.keys(styleJSON.sources)) {
|
|
1591
1616
|
let sourceType;
|
|
1592
|
-
let sparse;
|
|
1593
1617
|
// eslint-disable-next-line security/detect-object-injection -- name is from style sources object keys
|
|
1594
1618
|
let source = styleJSON.sources[name];
|
|
1595
1619
|
let url = source.url;
|
|
@@ -1620,7 +1644,6 @@ export const serve_rendered = {
|
|
|
1620
1644
|
if (dataInfo.inputFile) {
|
|
1621
1645
|
inputFile = dataInfo.inputFile;
|
|
1622
1646
|
sourceType = dataInfo.fileType;
|
|
1623
|
-
sparse = dataInfo.sparse;
|
|
1624
1647
|
s3Profile = dataInfo.s3Profile;
|
|
1625
1648
|
requestPayer = dataInfo.requestPayer;
|
|
1626
1649
|
s3Region = dataInfo.s3Region;
|
|
@@ -1664,7 +1687,6 @@ export const serve_rendered = {
|
|
|
1664
1687
|
const type = source.type;
|
|
1665
1688
|
Object.assign(source, metadata);
|
|
1666
1689
|
source.type = type;
|
|
1667
|
-
source.sparse = sparse;
|
|
1668
1690
|
source.tiles = [
|
|
1669
1691
|
// meta url which will be detected when requested
|
|
1670
1692
|
`pmtiles://${name}/{z}/{x}/{y}.${metadata.format || 'pbf'}`,
|
|
@@ -1683,6 +1705,13 @@ export const serve_rendered = {
|
|
|
1683
1705
|
tileJSON.attribution += source.attribution;
|
|
1684
1706
|
}
|
|
1685
1707
|
}
|
|
1708
|
+
|
|
1709
|
+
// Set sparse flag: user config overrides format-based default
|
|
1710
|
+
// Vector tiles (pbf) default to false (204), raster tiles default to true (404)
|
|
1711
|
+
const isVector = metadata.format === 'pbf';
|
|
1712
|
+
// eslint-disable-next-line security/detect-object-injection -- name is from style sources object keys
|
|
1713
|
+
map.sparseFlags[name] =
|
|
1714
|
+
dataInfo.sparse ?? options.sparse ?? !isVector;
|
|
1686
1715
|
} else {
|
|
1687
1716
|
// MBTiles does not support remote URLs
|
|
1688
1717
|
|
|
@@ -1708,7 +1737,6 @@ export const serve_rendered = {
|
|
|
1708
1737
|
const type = source.type;
|
|
1709
1738
|
Object.assign(source, info);
|
|
1710
1739
|
source.type = type;
|
|
1711
|
-
source.sparse = sparse;
|
|
1712
1740
|
source.tiles = [
|
|
1713
1741
|
// meta url which will be detected when requested
|
|
1714
1742
|
`mbtiles://${name}/{z}/{x}/{y}.${info.format || 'pbf'}`,
|
|
@@ -1731,6 +1759,13 @@ export const serve_rendered = {
|
|
|
1731
1759
|
tileJSON.attribution += source.attribution;
|
|
1732
1760
|
}
|
|
1733
1761
|
}
|
|
1762
|
+
|
|
1763
|
+
// Set sparse flag: user config overrides format-based default
|
|
1764
|
+
// Vector tiles (pbf) default to false (204), raster tiles default to true (404)
|
|
1765
|
+
const isVector = info.format === 'pbf';
|
|
1766
|
+
// eslint-disable-next-line security/detect-object-injection -- name is from style sources object keys
|
|
1767
|
+
map.sparseFlags[name] =
|
|
1768
|
+
dataInfo.sparse ?? options.sparse ?? !isVector;
|
|
1734
1769
|
}
|
|
1735
1770
|
}
|
|
1736
1771
|
}
|
package/src/serve_style.js
CHANGED
|
@@ -35,7 +35,7 @@ export const serve_style = {
|
|
|
35
35
|
*/
|
|
36
36
|
app.get('/:id/style.json', (req, res, next) => {
|
|
37
37
|
const { id } = req.params;
|
|
38
|
-
if (verbose) {
|
|
38
|
+
if (verbose >= 1) {
|
|
39
39
|
console.log(
|
|
40
40
|
'Handling style request for: /styles/%s/style.json',
|
|
41
41
|
String(id).replace(/\n|\r/g, ''),
|
|
@@ -95,7 +95,7 @@ export const serve_style = {
|
|
|
95
95
|
const sanitizedFormat = format
|
|
96
96
|
? '.' + String(format).replace(/\n|\r/g, '')
|
|
97
97
|
: '';
|
|
98
|
-
if (verbose) {
|
|
98
|
+
if (verbose >= 1) {
|
|
99
99
|
console.log(
|
|
100
100
|
`Handling sprite request for: /styles/%s/sprite/%s%s%s`,
|
|
101
101
|
sanitizedId,
|
|
@@ -108,7 +108,7 @@ export const serve_style = {
|
|
|
108
108
|
const item = repo[id];
|
|
109
109
|
const validatedFormat = allowedSpriteFormats(format);
|
|
110
110
|
if (!item || !validatedFormat) {
|
|
111
|
-
if (verbose)
|
|
111
|
+
if (verbose >= 1)
|
|
112
112
|
console.error(
|
|
113
113
|
`Sprite item or format not found for: /styles/%s/sprite/%s%s%s`,
|
|
114
114
|
sanitizedId,
|
|
@@ -123,7 +123,7 @@ export const serve_style = {
|
|
|
123
123
|
);
|
|
124
124
|
const spriteScale = allowedSpriteScales(scale);
|
|
125
125
|
if (!sprite || spriteScale === null) {
|
|
126
|
-
if (verbose)
|
|
126
|
+
if (verbose >= 1)
|
|
127
127
|
console.error(
|
|
128
128
|
`Bad Sprite ID or Scale for: /styles/%s/sprite/%s%s%s`,
|
|
129
129
|
sanitizedId,
|
|
@@ -147,7 +147,7 @@ export const serve_style = {
|
|
|
147
147
|
|
|
148
148
|
const sanitizedSpritePath = sprite.path.replace(/^(\.\.\/)+/, '');
|
|
149
149
|
const filename = `${sanitizedSpritePath}${spriteScale}.${validatedFormat}`;
|
|
150
|
-
if (verbose) console.log(`Loading sprite from: %s`, filename);
|
|
150
|
+
if (verbose >= 1) console.log(`Loading sprite from: %s`, filename);
|
|
151
151
|
try {
|
|
152
152
|
const data = await readFile(filename);
|
|
153
153
|
|
|
@@ -156,7 +156,7 @@ export const serve_style = {
|
|
|
156
156
|
} else if (validatedFormat === 'png') {
|
|
157
157
|
res.header('Content-type', 'image/png');
|
|
158
158
|
}
|
|
159
|
-
if (verbose)
|
|
159
|
+
if (verbose >= 1)
|
|
160
160
|
console.log(
|
|
161
161
|
`Responding with sprite data for /styles/%s/sprite/%s%s%s`,
|
|
162
162
|
sanitizedId,
|
|
@@ -167,7 +167,7 @@ export const serve_style = {
|
|
|
167
167
|
res.set({ 'Last-Modified': item.lastModified });
|
|
168
168
|
return res.send(data);
|
|
169
169
|
} catch (err) {
|
|
170
|
-
if (verbose) {
|
|
170
|
+
if (verbose >= 1) {
|
|
171
171
|
console.error(
|
|
172
172
|
'Sprite load error: %s, Error: %s',
|
|
173
173
|
filename,
|
|
@@ -217,7 +217,28 @@ export const serve_style = {
|
|
|
217
217
|
const styleFile = path.resolve(options.paths.styles, params.style);
|
|
218
218
|
const styleJSON = clone(style);
|
|
219
219
|
|
|
220
|
-
|
|
220
|
+
// Sanitize style for validation: remove non-spec properties (e.g., 'sparse')
|
|
221
|
+
// so that validateStyleMin doesn't reject valid styles containing our custom flags.
|
|
222
|
+
const styleForValidation = clone(styleJSON);
|
|
223
|
+
if (styleForValidation.sources) {
|
|
224
|
+
for (const name of Object.keys(styleForValidation.sources)) {
|
|
225
|
+
if (
|
|
226
|
+
// eslint-disable-next-line security/detect-object-injection -- name is from Object.keys of styleForValidation.sources
|
|
227
|
+
styleForValidation.sources[name] &&
|
|
228
|
+
// eslint-disable-next-line security/detect-object-injection -- name is from Object.keys of styleForValidation.sources
|
|
229
|
+
'sparse' in styleForValidation.sources[name]
|
|
230
|
+
) {
|
|
231
|
+
try {
|
|
232
|
+
// eslint-disable-next-line security/detect-object-injection -- name is from Object.keys of styleForValidation.sources
|
|
233
|
+
delete styleForValidation.sources[name].sparse;
|
|
234
|
+
} catch (_err) {
|
|
235
|
+
// ignore any deletion errors and continue validation
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const validationErrors = validateStyleMin(styleForValidation);
|
|
221
242
|
if (validationErrors.length > 0) {
|
|
222
243
|
console.log(`The file "${params.style}" is not a valid style file:`);
|
|
223
244
|
for (const err of validationErrors) {
|