tileserver-gl-light 5.5.0-pre.4 → 5.5.0-pre.6
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 +6 -6
- package/public/resources/maplibre-gl-inspect.js +12 -8
- package/public/resources/maplibre-gl-inspect.js.map +1 -1
- package/public/resources/maplibre-gl.js +4 -4
- package/public/resources/maplibre-gl.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 +132 -76
- 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/serve_rendered.js
CHANGED
|
@@ -531,30 +531,43 @@ async function respondImage(
|
|
|
531
531
|
pool = item.map.renderersStatic[scale];
|
|
532
532
|
}
|
|
533
533
|
|
|
534
|
+
if (!pool) {
|
|
535
|
+
console.error(`Pool not found for scale ${scale}, mode ${mode}`);
|
|
536
|
+
return res.status(500).send('Renderer pool not configured');
|
|
537
|
+
}
|
|
538
|
+
|
|
534
539
|
pool.acquire(async (err, renderer) => {
|
|
535
540
|
// Check if pool.acquire failed or returned null/invalid renderer
|
|
536
541
|
if (err) {
|
|
537
542
|
console.error('Failed to acquire renderer from pool:', err);
|
|
538
|
-
|
|
543
|
+
if (!res.headersSent) {
|
|
544
|
+
return res.status(503).send('Renderer pool error');
|
|
545
|
+
}
|
|
546
|
+
return;
|
|
539
547
|
}
|
|
540
548
|
|
|
541
549
|
if (!renderer) {
|
|
542
550
|
console.error(
|
|
543
551
|
'Renderer is null - likely crashed or failed to initialize',
|
|
544
552
|
);
|
|
545
|
-
|
|
553
|
+
if (!res.headersSent) {
|
|
554
|
+
return res.status(503).send('Renderer unavailable');
|
|
555
|
+
}
|
|
556
|
+
return;
|
|
546
557
|
}
|
|
547
558
|
|
|
548
559
|
// Validate renderer has required methods (basic health check)
|
|
549
560
|
if (typeof renderer.render !== 'function') {
|
|
550
561
|
console.error('Renderer is invalid - missing render method');
|
|
551
|
-
// Destroy the bad renderer and remove from pool
|
|
552
562
|
try {
|
|
553
|
-
pool.
|
|
563
|
+
pool.removeBadObject(renderer);
|
|
554
564
|
} catch (e) {
|
|
555
|
-
console.error('Error
|
|
565
|
+
console.error('Error removing bad renderer:', e);
|
|
566
|
+
}
|
|
567
|
+
if (!res.headersSent) {
|
|
568
|
+
return res.status(503).send('Renderer invalid');
|
|
556
569
|
}
|
|
557
|
-
return
|
|
570
|
+
return;
|
|
558
571
|
}
|
|
559
572
|
|
|
560
573
|
// For 512px tiles, use the actual maplibre-native zoom. For 256px tiles, use zoom - 1
|
|
@@ -588,13 +601,14 @@ async function respondImage(
|
|
|
588
601
|
|
|
589
602
|
// Set a timeout for the render operation to detect hung renderers
|
|
590
603
|
const renderTimeout = setTimeout(() => {
|
|
591
|
-
console.error('Renderer timeout - destroying
|
|
604
|
+
console.error('Renderer timeout - destroying hung renderer');
|
|
605
|
+
|
|
592
606
|
try {
|
|
593
|
-
|
|
594
|
-
pool.destroy(renderer);
|
|
607
|
+
pool.removeBadObject(renderer);
|
|
595
608
|
} catch (e) {
|
|
596
|
-
console.error('Error
|
|
609
|
+
console.error('Error removing timed-out renderer:', e);
|
|
597
610
|
}
|
|
611
|
+
|
|
598
612
|
if (!res.headersSent) {
|
|
599
613
|
res.status(503).send('Renderer timeout');
|
|
600
614
|
}
|
|
@@ -604,13 +618,17 @@ async function respondImage(
|
|
|
604
618
|
renderer.render(params, (err, data) => {
|
|
605
619
|
clearTimeout(renderTimeout);
|
|
606
620
|
|
|
621
|
+
if (res.headersSent) {
|
|
622
|
+
// Timeout already fired and sent response, don't process
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
607
626
|
if (err) {
|
|
608
627
|
console.error('Render error:', err);
|
|
609
|
-
// Destroy renderer instead of releasing it back to pool since it may be corrupted
|
|
610
628
|
try {
|
|
611
|
-
pool.
|
|
629
|
+
pool.removeBadObject(renderer);
|
|
612
630
|
} catch (e) {
|
|
613
|
-
console.error('Error
|
|
631
|
+
console.error('Error removing failed renderer:', e);
|
|
614
632
|
}
|
|
615
633
|
if (!res.headersSent) {
|
|
616
634
|
return res
|
|
@@ -646,12 +664,11 @@ async function respondImage(
|
|
|
646
664
|
height: height * scale,
|
|
647
665
|
});
|
|
648
666
|
}
|
|
649
|
-
// HACK(Part 2) 256px tiles are a zoom level lower than maplibre-native default tiles. this hack allows tileserver-gl to support zoom 0 256px tiles, which would actually be zoom -1 in maplibre-native. Since zoom -1 isn't supported, a double sized zoom 0 tile is requested and resized here.
|
|
650
667
|
|
|
668
|
+
// HACK(Part 2) 256px tiles are a zoom level lower than maplibre-native default tiles. this hack allows tileserver-gl to support zoom 0 256px tiles, which would actually be zoom -1 in maplibre-native. Since zoom -1 isn't supported, a double sized zoom 0 tile is requested and resized here.
|
|
651
669
|
if (z === 0 && width === 256) {
|
|
652
670
|
image.resize(width * scale, height * scale);
|
|
653
671
|
}
|
|
654
|
-
// END HACK(Part 2)
|
|
655
672
|
|
|
656
673
|
const composites = [];
|
|
657
674
|
if (overlay) {
|
|
@@ -659,7 +676,6 @@ async function respondImage(
|
|
|
659
676
|
}
|
|
660
677
|
if (item.watermark) {
|
|
661
678
|
const canvas = renderWatermark(width, height, scale, item.watermark);
|
|
662
|
-
|
|
663
679
|
composites.push({ input: canvas.toBuffer() });
|
|
664
680
|
}
|
|
665
681
|
|
|
@@ -670,7 +686,6 @@ async function respondImage(
|
|
|
670
686
|
scale,
|
|
671
687
|
item.staticAttributionText,
|
|
672
688
|
);
|
|
673
|
-
|
|
674
689
|
composites.push({ input: canvas.toBuffer() });
|
|
675
690
|
}
|
|
676
691
|
|
|
@@ -687,7 +702,6 @@ async function respondImage(
|
|
|
687
702
|
}
|
|
688
703
|
// eslint-disable-next-line security/detect-object-injection -- format is validated above
|
|
689
704
|
const formatQuality = formatQualities[format];
|
|
690
|
-
|
|
691
705
|
// eslint-disable-next-line security/detect-object-injection -- format is validated above
|
|
692
706
|
const formatOptions = (options.formatOptions || {})[format] || {};
|
|
693
707
|
|
|
@@ -710,25 +724,32 @@ async function respondImage(
|
|
|
710
724
|
} else if (format === 'webp') {
|
|
711
725
|
image.webp({ quality: formatOptions.quality || formatQuality || 90 });
|
|
712
726
|
}
|
|
727
|
+
|
|
713
728
|
image.toBuffer((err, buffer, info) => {
|
|
714
|
-
if (!buffer) {
|
|
715
|
-
|
|
729
|
+
if (err || !buffer) {
|
|
730
|
+
console.error('Sharp error:', err);
|
|
731
|
+
if (!res.headersSent) {
|
|
732
|
+
return res.status(500).send('Image processing failed');
|
|
733
|
+
}
|
|
734
|
+
return;
|
|
716
735
|
}
|
|
717
736
|
|
|
718
|
-
res.
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
737
|
+
if (!res.headersSent) {
|
|
738
|
+
res.set({
|
|
739
|
+
'Last-Modified': item.lastModified,
|
|
740
|
+
'Content-Type': `image/${format}`,
|
|
741
|
+
});
|
|
742
|
+
return res.status(200).send(buffer);
|
|
743
|
+
}
|
|
723
744
|
});
|
|
724
745
|
});
|
|
725
746
|
} catch (error) {
|
|
726
747
|
clearTimeout(renderTimeout);
|
|
727
748
|
console.error('Unexpected error during render:', error);
|
|
728
749
|
try {
|
|
729
|
-
pool.
|
|
750
|
+
pool.removeBadObject(renderer);
|
|
730
751
|
} catch (e) {
|
|
731
|
-
console.error('Error
|
|
752
|
+
console.error('Error removing renderer after error:', e);
|
|
732
753
|
}
|
|
733
754
|
if (!res.headersSent) {
|
|
734
755
|
return res.status(500).send('Render failed');
|
|
@@ -1073,7 +1094,7 @@ export const serve_rendered = {
|
|
|
1073
1094
|
(!p1 && p2 === 'static') || (p1 === 'static' && p2 === 'raw')
|
|
1074
1095
|
? 'static'
|
|
1075
1096
|
: 'tile';
|
|
1076
|
-
if (verbose) {
|
|
1097
|
+
if (verbose >= 3) {
|
|
1077
1098
|
console.log(
|
|
1078
1099
|
`Handling rendered %s request for: /styles/%s%s/%s/%s/%s%s.%s`,
|
|
1079
1100
|
requestType,
|
|
@@ -1133,7 +1154,7 @@ export const serve_rendered = {
|
|
|
1133
1154
|
return res.sendStatus(404);
|
|
1134
1155
|
}
|
|
1135
1156
|
const tileSize = parseInt(req.params.tileSize, 10) || undefined;
|
|
1136
|
-
if (verbose) {
|
|
1157
|
+
if (verbose >= 3) {
|
|
1137
1158
|
console.log(
|
|
1138
1159
|
`Handling rendered tilejson request for: /styles/%s%s.json`,
|
|
1139
1160
|
req.params.tileSize
|
|
@@ -1184,11 +1205,16 @@ export const serve_rendered = {
|
|
|
1184
1205
|
renderersStatic: [],
|
|
1185
1206
|
sources: {},
|
|
1186
1207
|
sourceTypes: {},
|
|
1208
|
+
sparseFlags: {},
|
|
1187
1209
|
};
|
|
1188
1210
|
|
|
1189
|
-
const { publicUrl, verbose } = programOpts;
|
|
1211
|
+
const { publicUrl, verbose, fetchTimeout } = programOpts;
|
|
1190
1212
|
|
|
1191
1213
|
const styleJSON = clone(style);
|
|
1214
|
+
|
|
1215
|
+
// Global sparse flag for HTTP/remote sources (from config options)
|
|
1216
|
+
const globalSparse = options.sparse ?? true;
|
|
1217
|
+
|
|
1192
1218
|
/**
|
|
1193
1219
|
* Creates a pool of renderers.
|
|
1194
1220
|
* @param {number} ratio Pixel ratio
|
|
@@ -1210,7 +1236,7 @@ export const serve_rendered = {
|
|
|
1210
1236
|
ratio,
|
|
1211
1237
|
request: async (req, callback) => {
|
|
1212
1238
|
const protocol = req.url.split(':')[0];
|
|
1213
|
-
if (verbose
|
|
1239
|
+
if (verbose >= 3) {
|
|
1214
1240
|
console.log('Handling request:', req);
|
|
1215
1241
|
}
|
|
1216
1242
|
if (protocol === 'sprites') {
|
|
@@ -1266,22 +1292,18 @@ export const serve_rendered = {
|
|
|
1266
1292
|
x,
|
|
1267
1293
|
y,
|
|
1268
1294
|
);
|
|
1269
|
-
if (fetchTile == null
|
|
1270
|
-
if (verbose) {
|
|
1271
|
-
console.log(
|
|
1272
|
-
'fetchTile warning on %s, sparse response',
|
|
1273
|
-
req.url,
|
|
1274
|
-
);
|
|
1295
|
+
if (fetchTile == null) {
|
|
1296
|
+
if (verbose >= 2) {
|
|
1297
|
+
console.log('fetchTile null on %s', req.url);
|
|
1275
1298
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
if (
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
req.url,
|
|
1283
|
-
);
|
|
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;
|
|
1284
1305
|
}
|
|
1306
|
+
// sparse=false -> 204 (empty tile, no overzoom) - create blank response
|
|
1285
1307
|
createEmptyResponse(
|
|
1286
1308
|
sourceInfo.format,
|
|
1287
1309
|
sourceInfo.color,
|
|
@@ -1320,40 +1342,58 @@ export const serve_rendered = {
|
|
|
1320
1342
|
|
|
1321
1343
|
callback(null, response);
|
|
1322
1344
|
} else if (protocol === 'http' || protocol === 'https') {
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
|
1345
|
+
const controller = new AbortController();
|
|
1346
|
+
const timeoutMs = (fetchTimeout && Number(fetchTimeout)) || 15000;
|
|
1347
|
+
let timeoutId;
|
|
1327
1348
|
|
|
1349
|
+
try {
|
|
1350
|
+
timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1328
1351
|
const response = await fetch(req.url, {
|
|
1329
1352
|
signal: controller.signal,
|
|
1330
1353
|
});
|
|
1331
|
-
|
|
1332
1354
|
clearTimeout(timeoutId);
|
|
1333
1355
|
|
|
1334
|
-
//
|
|
1335
|
-
if (response.status ===
|
|
1336
|
-
|
|
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) {
|
|
1337
1368
|
console.log(
|
|
1338
|
-
'fetchTile
|
|
1369
|
+
'fetchTile HTTP %d on %s, %s',
|
|
1370
|
+
response.status,
|
|
1339
1371
|
req.url,
|
|
1372
|
+
globalSparse
|
|
1373
|
+
? 'allowing overzoom'
|
|
1374
|
+
: 'creating empty tile',
|
|
1340
1375
|
);
|
|
1341
1376
|
}
|
|
1342
|
-
callback();
|
|
1343
|
-
return;
|
|
1344
|
-
}
|
|
1345
1377
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
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;
|
|
1351
1391
|
}
|
|
1352
1392
|
|
|
1353
1393
|
const responseHeaders = response.headers;
|
|
1354
1394
|
const responseData = await response.arrayBuffer();
|
|
1355
|
-
|
|
1356
1395
|
const parsedResponse = {};
|
|
1396
|
+
|
|
1357
1397
|
if (responseHeaders.get('last-modified')) {
|
|
1358
1398
|
parsedResponse.modified = new Date(
|
|
1359
1399
|
responseHeaders.get('last-modified'),
|
|
@@ -1371,8 +1411,7 @@ export const serve_rendered = {
|
|
|
1371
1411
|
parsedResponse.data = Buffer.from(responseData);
|
|
1372
1412
|
callback(null, parsedResponse);
|
|
1373
1413
|
} catch (error) {
|
|
1374
|
-
// Log DNS failures
|
|
1375
|
-
// Native fetch wraps DNS errors in error.cause
|
|
1414
|
+
// Log DNS failures
|
|
1376
1415
|
if (error.cause?.code === 'ENOTFOUND') {
|
|
1377
1416
|
console.error(
|
|
1378
1417
|
`DNS RESOLUTION FAILED for ${req.url}. ` +
|
|
@@ -1381,20 +1420,27 @@ export const serve_rendered = {
|
|
|
1381
1420
|
);
|
|
1382
1421
|
}
|
|
1383
1422
|
|
|
1384
|
-
//
|
|
1423
|
+
// Log timeout
|
|
1385
1424
|
if (error.name === 'AbortError') {
|
|
1386
1425
|
console.error(
|
|
1387
1426
|
`FETCH TIMEOUT for ${req.url}. ` +
|
|
1388
|
-
`The request took longer than
|
|
1427
|
+
`The request took longer than ${timeoutMs} ms to complete.`,
|
|
1389
1428
|
);
|
|
1390
1429
|
}
|
|
1391
1430
|
|
|
1392
|
-
//
|
|
1431
|
+
// Log all other errors
|
|
1393
1432
|
console.error(
|
|
1394
1433
|
`Error fetching remote URL ${req.url}:`,
|
|
1395
1434
|
error.message || error,
|
|
1396
1435
|
);
|
|
1397
1436
|
|
|
1437
|
+
if (globalSparse) {
|
|
1438
|
+
// sparse=true -> allow overzoom
|
|
1439
|
+
callback();
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// sparse=false (default) -> create empty tile
|
|
1398
1444
|
const parts = url.parse(req.url);
|
|
1399
1445
|
const extension = path.extname(parts.pathname).toLowerCase();
|
|
1400
1446
|
// eslint-disable-next-line security/detect-object-injection -- extension is from path.extname, limited set
|
|
@@ -1476,7 +1522,7 @@ export const serve_rendered = {
|
|
|
1476
1522
|
|
|
1477
1523
|
// Remove (flatten) 3D buildings
|
|
1478
1524
|
if (layer.paint['fill-extrusion-height']) {
|
|
1479
|
-
if (verbose) {
|
|
1525
|
+
if (verbose >= 1) {
|
|
1480
1526
|
console.warn(
|
|
1481
1527
|
`Warning: Layer '${layerIdForWarning}' in style '${id}' has property 'fill-extrusion-height'. ` +
|
|
1482
1528
|
`3D extrusion may appear distorted or misleading when rendered as a static image due to camera angle limitations. ` +
|
|
@@ -1487,7 +1533,7 @@ export const serve_rendered = {
|
|
|
1487
1533
|
layer.paint['fill-extrusion-height'] = 0;
|
|
1488
1534
|
}
|
|
1489
1535
|
if (layer.paint['fill-extrusion-base']) {
|
|
1490
|
-
if (verbose) {
|
|
1536
|
+
if (verbose >= 1) {
|
|
1491
1537
|
console.warn(
|
|
1492
1538
|
`Warning: Layer '${layerIdForWarning}' in style '${id}' has property 'fill-extrusion-base'. ` +
|
|
1493
1539
|
`3D extrusion may appear distorted or misleading when rendered as a static image due to camera angle limitations. ` +
|
|
@@ -1507,7 +1553,7 @@ export const serve_rendered = {
|
|
|
1507
1553
|
|
|
1508
1554
|
for (const prop of hillshadePropertiesToRemove) {
|
|
1509
1555
|
if (prop in layer.paint) {
|
|
1510
|
-
if (verbose) {
|
|
1556
|
+
if (verbose >= 1) {
|
|
1511
1557
|
console.warn(
|
|
1512
1558
|
`Warning: Layer '${layerIdForWarning}' in style '${id}' has property '${prop}'. ` +
|
|
1513
1559
|
`This property is not supported by MapLibre Native. ` +
|
|
@@ -1522,7 +1568,7 @@ export const serve_rendered = {
|
|
|
1522
1568
|
|
|
1523
1569
|
// --- Remove 'hillshade-shadow-color' if it is an array. It can only be a string in MapLibre Native ---
|
|
1524
1570
|
if (Array.isArray(layer.paint['hillshade-shadow-color'])) {
|
|
1525
|
-
if (verbose) {
|
|
1571
|
+
if (verbose >= 1) {
|
|
1526
1572
|
console.warn(
|
|
1527
1573
|
`Warning: Layer '${layerIdForWarning}' in style '${id}' has property 'hillshade-shadow-color'. ` +
|
|
1528
1574
|
`An array value is not supported by MapLibre Native for this property (expected string/color). ` +
|
|
@@ -1568,7 +1614,6 @@ export const serve_rendered = {
|
|
|
1568
1614
|
|
|
1569
1615
|
for (const name of Object.keys(styleJSON.sources)) {
|
|
1570
1616
|
let sourceType;
|
|
1571
|
-
let sparse;
|
|
1572
1617
|
// eslint-disable-next-line security/detect-object-injection -- name is from style sources object keys
|
|
1573
1618
|
let source = styleJSON.sources[name];
|
|
1574
1619
|
let url = source.url;
|
|
@@ -1599,7 +1644,6 @@ export const serve_rendered = {
|
|
|
1599
1644
|
if (dataInfo.inputFile) {
|
|
1600
1645
|
inputFile = dataInfo.inputFile;
|
|
1601
1646
|
sourceType = dataInfo.fileType;
|
|
1602
|
-
sparse = dataInfo.sparse;
|
|
1603
1647
|
s3Profile = dataInfo.s3Profile;
|
|
1604
1648
|
requestPayer = dataInfo.requestPayer;
|
|
1605
1649
|
s3Region = dataInfo.s3Region;
|
|
@@ -1643,7 +1687,6 @@ export const serve_rendered = {
|
|
|
1643
1687
|
const type = source.type;
|
|
1644
1688
|
Object.assign(source, metadata);
|
|
1645
1689
|
source.type = type;
|
|
1646
|
-
source.sparse = sparse;
|
|
1647
1690
|
source.tiles = [
|
|
1648
1691
|
// meta url which will be detected when requested
|
|
1649
1692
|
`pmtiles://${name}/{z}/{x}/{y}.${metadata.format || 'pbf'}`,
|
|
@@ -1662,6 +1705,13 @@ export const serve_rendered = {
|
|
|
1662
1705
|
tileJSON.attribution += source.attribution;
|
|
1663
1706
|
}
|
|
1664
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;
|
|
1665
1715
|
} else {
|
|
1666
1716
|
// MBTiles does not support remote URLs
|
|
1667
1717
|
|
|
@@ -1687,7 +1737,6 @@ export const serve_rendered = {
|
|
|
1687
1737
|
const type = source.type;
|
|
1688
1738
|
Object.assign(source, info);
|
|
1689
1739
|
source.type = type;
|
|
1690
|
-
source.sparse = sparse;
|
|
1691
1740
|
source.tiles = [
|
|
1692
1741
|
// meta url which will be detected when requested
|
|
1693
1742
|
`mbtiles://${name}/{z}/{x}/{y}.${info.format || 'pbf'}`,
|
|
@@ -1710,6 +1759,13 @@ export const serve_rendered = {
|
|
|
1710
1759
|
tileJSON.attribution += source.attribution;
|
|
1711
1760
|
}
|
|
1712
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;
|
|
1713
1769
|
}
|
|
1714
1770
|
}
|
|
1715
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) {
|