tileserver-gl-light 5.5.0-pre.1 → 5.5.0-pre.12
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 -34
- package/docs/config.rst +52 -11
- package/docs/endpoints.rst +12 -2
- package/docs/installation.rst +6 -6
- package/docs/usage.rst +26 -0
- package/package.json +14 -14
- 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/serve_data.js
CHANGED
|
@@ -12,9 +12,9 @@ import { SphericalMercator } from '@mapbox/sphericalmercator';
|
|
|
12
12
|
import {
|
|
13
13
|
fixTileJSONCenter,
|
|
14
14
|
getTileUrls,
|
|
15
|
-
isS3Url,
|
|
16
15
|
isValidRemoteUrl,
|
|
17
16
|
fetchTileData,
|
|
17
|
+
lonLatToTilePixel,
|
|
18
18
|
} from './utils.js';
|
|
19
19
|
import { getPMtilesInfo, openPMtiles } from './pmtiles_adapter.js';
|
|
20
20
|
import { gunzipP, gzipP } from './promises.js';
|
|
@@ -31,9 +31,9 @@ const packageJson = JSON.parse(
|
|
|
31
31
|
);
|
|
32
32
|
|
|
33
33
|
const isLight = packageJson.name.slice(-6) === '-light';
|
|
34
|
-
const serve_rendered = (
|
|
35
|
-
|
|
36
|
-
)
|
|
34
|
+
const { serve_rendered } = await import(
|
|
35
|
+
`${!isLight ? `./serve_rendered.js` : `./serve_light.js`}`
|
|
36
|
+
);
|
|
37
37
|
|
|
38
38
|
export const serve_data = {
|
|
39
39
|
/**
|
|
@@ -46,6 +46,7 @@ export const serve_data = {
|
|
|
46
46
|
init: function (options, repo, programOpts) {
|
|
47
47
|
const { verbose } = programOpts;
|
|
48
48
|
const app = express().disable('x-powered-by');
|
|
49
|
+
app.use(express.json());
|
|
49
50
|
|
|
50
51
|
/**
|
|
51
52
|
* Handles requests for tile data, responding with the tile image.
|
|
@@ -59,7 +60,7 @@ export const serve_data = {
|
|
|
59
60
|
* @returns {Promise<void>}
|
|
60
61
|
*/
|
|
61
62
|
app.get('/:id/:z/:x/:y.:format', async (req, res) => {
|
|
62
|
-
if (verbose) {
|
|
63
|
+
if (verbose >= 1) {
|
|
63
64
|
console.log(
|
|
64
65
|
`Handling tile request for: /data/%s/%s/%s/%s.%s`,
|
|
65
66
|
String(req.params.id).replace(/\n|\r/g, ''),
|
|
@@ -69,7 +70,7 @@ export const serve_data = {
|
|
|
69
70
|
String(req.params.format).replace(/\n|\r/g, ''),
|
|
70
71
|
);
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
+
|
|
73
74
|
const item = repo[req.params.id];
|
|
74
75
|
if (!item) {
|
|
75
76
|
return res.sendStatus(404);
|
|
@@ -110,10 +111,10 @@ export const serve_data = {
|
|
|
110
111
|
x,
|
|
111
112
|
y,
|
|
112
113
|
);
|
|
113
|
-
if (fetchTile == null
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return res.status(204).send();
|
|
114
|
+
if (fetchTile == null) {
|
|
115
|
+
// sparse=true (default) -> 404 (allows overzoom)
|
|
116
|
+
// sparse=false -> 204 (empty tile, no overzoom)
|
|
117
|
+
return res.status(item.sparse ? 404 : 204).send();
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
let data = fetchTile.data;
|
|
@@ -122,7 +123,6 @@ export const serve_data = {
|
|
|
122
123
|
|
|
123
124
|
if (isGzipped) {
|
|
124
125
|
data = await gunzipP(data);
|
|
125
|
-
isGzipped = false;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
if (tileJSONFormat === 'pbf') {
|
|
@@ -165,13 +165,169 @@ export const serve_data = {
|
|
|
165
165
|
headers['Content-Encoding'] = 'gzip';
|
|
166
166
|
res.set(headers);
|
|
167
167
|
|
|
168
|
-
|
|
169
|
-
data = await gzipP(data);
|
|
170
|
-
}
|
|
168
|
+
data = await gzipP(data);
|
|
171
169
|
|
|
172
170
|
return res.status(200).send(data);
|
|
173
171
|
});
|
|
174
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Validates elevation data source and returns source info or sends error response.
|
|
175
|
+
* @param {string} id - ID of the data source.
|
|
176
|
+
* @param {object} res - Express response object.
|
|
177
|
+
* @returns {object|null} Source info object or null if validation failed.
|
|
178
|
+
*/
|
|
179
|
+
const validateElevationSource = (id, res) => {
|
|
180
|
+
// eslint-disable-next-line security/detect-object-injection -- id is route parameter for data source lookup
|
|
181
|
+
const item = repo?.[id];
|
|
182
|
+
if (!item) {
|
|
183
|
+
res.sendStatus(404);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
if (!item.source) {
|
|
187
|
+
res.status(404).send('Missing source');
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
if (!item.tileJSON) {
|
|
191
|
+
res.status(404).send('Missing tileJSON');
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
if (!item.sourceType) {
|
|
195
|
+
res.status(404).send('Missing sourceType');
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const { source, tileJSON, sourceType } = item;
|
|
199
|
+
if (sourceType !== 'pmtiles' && sourceType !== 'mbtiles') {
|
|
200
|
+
res.status(400).send('Invalid sourceType. Must be pmtiles or mbtiles.');
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
const encoding = tileJSON?.encoding;
|
|
204
|
+
if (encoding == null) {
|
|
205
|
+
res.status(400).send('Missing tileJSON.encoding');
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
if (encoding !== 'terrarium' && encoding !== 'mapbox') {
|
|
209
|
+
res.status(400).send('Invalid encoding. Must be terrarium or mapbox.');
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const format = tileJSON?.format;
|
|
213
|
+
if (format == null) {
|
|
214
|
+
res.status(400).send('Missing tileJSON.format');
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
if (format !== 'webp' && format !== 'png') {
|
|
218
|
+
res.status(400).send('Invalid format. Must be webp or png.');
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
if (tileJSON.minzoom == null || tileJSON.maxzoom == null) {
|
|
222
|
+
res.status(400).send('Missing tileJSON zoom bounds');
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
source,
|
|
227
|
+
sourceType,
|
|
228
|
+
encoding,
|
|
229
|
+
format,
|
|
230
|
+
tileSize: tileJSON.tileSize || 512,
|
|
231
|
+
minzoom: tileJSON.minzoom,
|
|
232
|
+
maxzoom: tileJSON.maxzoom,
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Validates that a point has valid lon, lat, and z properties.
|
|
238
|
+
* @param {object} point - Point to validate.
|
|
239
|
+
* @param {number} index - Index of the point in the array.
|
|
240
|
+
* @returns {string|null} Error message if invalid, null if valid.
|
|
241
|
+
*/
|
|
242
|
+
const validatePoint = (point, index) => {
|
|
243
|
+
if (point == null || typeof point !== 'object') {
|
|
244
|
+
return `Invalid point at index ${index}: point must be an object`;
|
|
245
|
+
}
|
|
246
|
+
if (typeof point.lon !== 'number' || !isFinite(point.lon)) {
|
|
247
|
+
return `Invalid point at index ${index}: lon must be a finite number`;
|
|
248
|
+
}
|
|
249
|
+
if (typeof point.lat !== 'number' || !isFinite(point.lat)) {
|
|
250
|
+
return `Invalid point at index ${index}: lat must be a finite number`;
|
|
251
|
+
}
|
|
252
|
+
if (typeof point.z !== 'number' || !isFinite(point.z)) {
|
|
253
|
+
return `Invalid point at index ${index}: z must be a finite number`;
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Gets batch elevations for an array of points.
|
|
260
|
+
* @param {object} sourceInfo - Validated source info from validateElevationSource.
|
|
261
|
+
* @param {Array<{lon: number, lat: number, z: number}>} points - Array of validated points.
|
|
262
|
+
* @returns {Promise<Array<number|null>>} Array of elevations in same order as input.
|
|
263
|
+
*/
|
|
264
|
+
const getBatchElevations = async (sourceInfo, points) => {
|
|
265
|
+
const {
|
|
266
|
+
source,
|
|
267
|
+
sourceType,
|
|
268
|
+
encoding,
|
|
269
|
+
format,
|
|
270
|
+
tileSize,
|
|
271
|
+
minzoom,
|
|
272
|
+
maxzoom,
|
|
273
|
+
} = sourceInfo;
|
|
274
|
+
|
|
275
|
+
// Group points by tile (including zoom level in the key)
|
|
276
|
+
const tileGroups = new Map();
|
|
277
|
+
for (let i = 0; i < points.length; i++) {
|
|
278
|
+
// eslint-disable-next-line security/detect-object-injection -- i is loop counter
|
|
279
|
+
const point = points[i];
|
|
280
|
+
let zoom = point.z;
|
|
281
|
+
if (zoom < minzoom) {
|
|
282
|
+
zoom = minzoom;
|
|
283
|
+
}
|
|
284
|
+
if (zoom > maxzoom) {
|
|
285
|
+
zoom = maxzoom;
|
|
286
|
+
}
|
|
287
|
+
const { tileX, tileY, pixelX, pixelY } = lonLatToTilePixel(
|
|
288
|
+
point.lon,
|
|
289
|
+
point.lat,
|
|
290
|
+
zoom,
|
|
291
|
+
tileSize,
|
|
292
|
+
);
|
|
293
|
+
const tileKey = `${zoom},${tileX},${tileY}`;
|
|
294
|
+
if (!tileGroups.has(tileKey)) {
|
|
295
|
+
tileGroups.set(tileKey, { zoom, tileX, tileY, pixels: [] });
|
|
296
|
+
}
|
|
297
|
+
tileGroups.get(tileKey).pixels.push({ pixelX, pixelY, index: i });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Initialize results array with nulls
|
|
301
|
+
const results = new Array(points.length).fill(null);
|
|
302
|
+
|
|
303
|
+
// Process each tile and extract elevations
|
|
304
|
+
for (const [, tileData] of tileGroups) {
|
|
305
|
+
const { zoom, tileX, tileY, pixels } = tileData;
|
|
306
|
+
const fetchTile = await fetchTileData(
|
|
307
|
+
source,
|
|
308
|
+
sourceType,
|
|
309
|
+
zoom,
|
|
310
|
+
tileX,
|
|
311
|
+
tileY,
|
|
312
|
+
);
|
|
313
|
+
if (fetchTile == null) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const elevations = await serve_rendered.getBatchElevationsFromTile(
|
|
318
|
+
fetchTile.data,
|
|
319
|
+
{ encoding, format, tile_size: tileSize },
|
|
320
|
+
pixels,
|
|
321
|
+
);
|
|
322
|
+
for (const { index, elevation } of elevations) {
|
|
323
|
+
// eslint-disable-next-line security/detect-object-injection -- index is from internal elevation processing
|
|
324
|
+
results[index] = elevation;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return results;
|
|
329
|
+
};
|
|
330
|
+
|
|
175
331
|
/**
|
|
176
332
|
* Handles requests for elevation data.
|
|
177
333
|
* @param {object} req - Express request object.
|
|
@@ -184,7 +340,7 @@ export const serve_data = {
|
|
|
184
340
|
*/
|
|
185
341
|
app.get('/:id/elevation/:z/:x/:y', async (req, res, next) => {
|
|
186
342
|
try {
|
|
187
|
-
if (verbose) {
|
|
343
|
+
if (verbose >= 1) {
|
|
188
344
|
console.log(
|
|
189
345
|
`Handling elevation request for: /data/%s/elevation/%s/%s/%s`,
|
|
190
346
|
String(req.params.id).replace(/\n|\r/g, ''),
|
|
@@ -193,49 +349,24 @@ export const serve_data = {
|
|
|
193
349
|
String(req.params.y).replace(/\n|\r/g, ''),
|
|
194
350
|
);
|
|
195
351
|
}
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
if (!
|
|
199
|
-
|
|
200
|
-
if (!item.tileJSON) return res.status(404).send('Missing tileJSON');
|
|
201
|
-
if (!item.sourceType) return res.status(404).send('Missing sourceType');
|
|
202
|
-
const { source, tileJSON, sourceType } = item;
|
|
203
|
-
if (sourceType !== 'pmtiles' && sourceType !== 'mbtiles') {
|
|
204
|
-
return res
|
|
205
|
-
.status(400)
|
|
206
|
-
.send('Invalid sourceType. Must be pmtiles or mbtiles.');
|
|
207
|
-
}
|
|
208
|
-
const encoding = tileJSON?.encoding;
|
|
209
|
-
if (encoding == null) {
|
|
210
|
-
return res.status(400).send('Missing tileJSON.encoding');
|
|
211
|
-
} else if (encoding !== 'terrarium' && encoding !== 'mapbox') {
|
|
212
|
-
return res
|
|
213
|
-
.status(400)
|
|
214
|
-
.send('Invalid encoding. Must be terrarium or mapbox.');
|
|
215
|
-
}
|
|
216
|
-
const format = tileJSON?.format;
|
|
217
|
-
if (format == null) {
|
|
218
|
-
return res.status(400).send('Missing tileJSON.format');
|
|
219
|
-
} else if (format !== 'webp' && format !== 'png') {
|
|
220
|
-
return res.status(400).send('Invalid format. Must be webp or png.');
|
|
221
|
-
}
|
|
352
|
+
|
|
353
|
+
const sourceInfo = validateElevationSource(req.params.id, res);
|
|
354
|
+
if (!sourceInfo) return;
|
|
355
|
+
|
|
222
356
|
const z = parseInt(req.params.z, 10);
|
|
223
357
|
const x = parseFloat(req.params.x);
|
|
224
358
|
const y = parseFloat(req.params.y);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const TILE_SIZE = tileJSON.tileSize || 512;
|
|
229
|
-
let bbox;
|
|
230
|
-
let xy;
|
|
231
|
-
var zoom = z;
|
|
359
|
+
|
|
360
|
+
let lon, lat;
|
|
361
|
+
let zoom = z;
|
|
232
362
|
|
|
233
363
|
if (Number.isInteger(x) && Number.isInteger(y)) {
|
|
364
|
+
// Tile coordinates mode - strict bounds checking
|
|
234
365
|
const intX = parseInt(req.params.x, 10);
|
|
235
366
|
const intY = parseInt(req.params.y, 10);
|
|
236
367
|
if (
|
|
237
|
-
zoom <
|
|
238
|
-
zoom >
|
|
368
|
+
zoom < sourceInfo.minzoom ||
|
|
369
|
+
zoom > sourceInfo.maxzoom ||
|
|
239
370
|
intX < 0 ||
|
|
240
371
|
intY < 0 ||
|
|
241
372
|
intX >= Math.pow(2, zoom) ||
|
|
@@ -243,45 +374,83 @@ export const serve_data = {
|
|
|
243
374
|
) {
|
|
244
375
|
return res.status(404).send('Out of bounds');
|
|
245
376
|
}
|
|
246
|
-
|
|
247
|
-
|
|
377
|
+
const bbox = new SphericalMercator().bbox(intX, intY, zoom);
|
|
378
|
+
lon = (bbox[0] + bbox[2]) / 2;
|
|
379
|
+
lat = (bbox[1] + bbox[3]) / 2;
|
|
248
380
|
} else {
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
if (zoom > tileJSON.maxzoom) {
|
|
254
|
-
zoom = tileJSON.maxzoom;
|
|
255
|
-
}
|
|
256
|
-
bbox = [x, y, x + 0.1, y + 0.1];
|
|
257
|
-
const { minX, minY } = new SphericalMercator().xyz(bbox, zoom);
|
|
258
|
-
xy = [minX, minY];
|
|
381
|
+
// Coordinate mode
|
|
382
|
+
lon = x;
|
|
383
|
+
lat = y;
|
|
259
384
|
}
|
|
260
385
|
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
386
|
+
const results = await getBatchElevations(sourceInfo, [
|
|
387
|
+
{ lon, lat, z: zoom },
|
|
388
|
+
]);
|
|
389
|
+
|
|
390
|
+
if (results[0] == null) {
|
|
391
|
+
return res.status(204).send();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Build response matching original format
|
|
395
|
+
const clampedZoom = Math.min(
|
|
396
|
+
Math.max(zoom, sourceInfo.minzoom),
|
|
397
|
+
sourceInfo.maxzoom,
|
|
398
|
+
);
|
|
399
|
+
const { tileX, tileY, pixelX, pixelY } = lonLatToTilePixel(
|
|
400
|
+
lon,
|
|
401
|
+
lat,
|
|
402
|
+
clampedZoom,
|
|
403
|
+
sourceInfo.tileSize,
|
|
267
404
|
);
|
|
268
|
-
if (fetchTile == null) return res.status(204).send();
|
|
269
|
-
|
|
270
|
-
let data = fetchTile.data;
|
|
271
|
-
var param = {
|
|
272
|
-
long: bbox[0].toFixed(7),
|
|
273
|
-
lat: bbox[1].toFixed(7),
|
|
274
|
-
encoding,
|
|
275
|
-
format,
|
|
276
|
-
tile_size: TILE_SIZE,
|
|
277
|
-
z: zoom,
|
|
278
|
-
x: xy[0],
|
|
279
|
-
y: xy[1],
|
|
280
|
-
};
|
|
281
405
|
|
|
282
|
-
res
|
|
283
|
-
|
|
284
|
-
|
|
406
|
+
res.status(200).json({
|
|
407
|
+
long: lon,
|
|
408
|
+
lat: lat,
|
|
409
|
+
elevation: results[0],
|
|
410
|
+
z: clampedZoom,
|
|
411
|
+
x: tileX,
|
|
412
|
+
y: tileY,
|
|
413
|
+
pixelX,
|
|
414
|
+
pixelY,
|
|
415
|
+
});
|
|
416
|
+
} catch (err) {
|
|
417
|
+
return res
|
|
418
|
+
.status(500)
|
|
419
|
+
.header('Content-Type', 'text/plain')
|
|
420
|
+
.send(err.message);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Handles batch elevation requests.
|
|
426
|
+
* Accepts a POST request with JSON body containing:
|
|
427
|
+
* - points: Array of {lon, lat, z} coordinates with zoom level
|
|
428
|
+
* Returns an array of elevations (or null for points with no data) in the same order as input.
|
|
429
|
+
* @param {object} req - Express request object.
|
|
430
|
+
* @param {object} res - Express response object.
|
|
431
|
+
* @param {string} req.params.id - ID of the data source.
|
|
432
|
+
* @returns {Promise<void>}
|
|
433
|
+
*/
|
|
434
|
+
app.post('/:id/elevation', async (req, res, next) => {
|
|
435
|
+
try {
|
|
436
|
+
const sourceInfo = validateElevationSource(req.params.id, res);
|
|
437
|
+
if (!sourceInfo) return;
|
|
438
|
+
|
|
439
|
+
const { points } = req.body;
|
|
440
|
+
if (!Array.isArray(points) || points.length === 0) {
|
|
441
|
+
return res.status(400).send('Missing or empty points array');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
for (let i = 0; i < points.length; i++) {
|
|
445
|
+
// eslint-disable-next-line security/detect-object-injection -- i is loop counter
|
|
446
|
+
const error = validatePoint(points[i], i);
|
|
447
|
+
if (error) {
|
|
448
|
+
return res.status(400).send(error);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const results = await getBatchElevations(sourceInfo, points);
|
|
453
|
+
res.status(200).json(results);
|
|
285
454
|
} catch (err) {
|
|
286
455
|
return res
|
|
287
456
|
.status(500)
|
|
@@ -298,13 +467,13 @@ export const serve_data = {
|
|
|
298
467
|
* @returns {Promise<void>}
|
|
299
468
|
*/
|
|
300
469
|
app.get('/:id.json', (req, res) => {
|
|
301
|
-
if (verbose) {
|
|
470
|
+
if (verbose >= 1) {
|
|
302
471
|
console.log(
|
|
303
472
|
`Handling tilejson request for: /data/%s.json`,
|
|
304
473
|
String(req.params.id).replace(/\n|\r/g, ''),
|
|
305
474
|
);
|
|
306
475
|
}
|
|
307
|
-
|
|
476
|
+
|
|
308
477
|
const item = repo[req.params.id];
|
|
309
478
|
if (!item) {
|
|
310
479
|
return res.sendStatus(404);
|
|
@@ -335,7 +504,7 @@ export const serve_data = {
|
|
|
335
504
|
* @param {string} id ID of the data source.
|
|
336
505
|
* @param {object} programOpts - An object containing the program options
|
|
337
506
|
* @param {string} programOpts.publicUrl Public URL for the data.
|
|
338
|
-
* @param {
|
|
507
|
+
* @param {number} programOpts.verbose Verbosity level (1-3). 1=important, 2=detailed, 3=debug/all requests.
|
|
339
508
|
* @returns {Promise<void>}
|
|
340
509
|
*/
|
|
341
510
|
add: async function (options, repo, params, id, programOpts) {
|
|
@@ -363,7 +532,7 @@ export const serve_data = {
|
|
|
363
532
|
}
|
|
364
533
|
}
|
|
365
534
|
|
|
366
|
-
if (verbose
|
|
535
|
+
if (verbose >= 1) {
|
|
367
536
|
console.log(`[INFO] Loading data source '${id}' from: ${inputFile}`);
|
|
368
537
|
}
|
|
369
538
|
|
|
@@ -373,7 +542,6 @@ export const serve_data = {
|
|
|
373
542
|
|
|
374
543
|
// Only check file stats for local files, not remote URLs
|
|
375
544
|
if (!isValidRemoteUrl(inputFile)) {
|
|
376
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- inputFile is from config file, validated above
|
|
377
545
|
const inputFileStats = await fsp.stat(inputFile);
|
|
378
546
|
if (!inputFileStats.isFile() || inputFileStats.size === 0) {
|
|
379
547
|
throw Error(`Not valid input file: "${inputFile}"`);
|
|
@@ -386,7 +554,6 @@ export const serve_data = {
|
|
|
386
554
|
tileJSON['format'] = 'pbf';
|
|
387
555
|
tileJSON['encoding'] = params['encoding'];
|
|
388
556
|
tileJSON['tileSize'] = params['tileSize'];
|
|
389
|
-
tileJSON['sparse'] = params['sparse'];
|
|
390
557
|
|
|
391
558
|
if (inputType === 'pmtiles') {
|
|
392
559
|
source = openPMtiles(
|
|
@@ -394,6 +561,7 @@ export const serve_data = {
|
|
|
394
561
|
params.s3Profile,
|
|
395
562
|
params.requestPayer,
|
|
396
563
|
params.s3Region,
|
|
564
|
+
params.s3UrlFormat,
|
|
397
565
|
verbose,
|
|
398
566
|
);
|
|
399
567
|
sourceType = 'pmtiles';
|
|
@@ -419,12 +587,20 @@ export const serve_data = {
|
|
|
419
587
|
tileJSON = options.dataDecoratorFunc(id, 'tilejson', tileJSON);
|
|
420
588
|
}
|
|
421
589
|
|
|
590
|
+
// Determine sparse: per-source overrides global, then format-based default
|
|
591
|
+
// sparse=true -> 404 (allows overzoom)
|
|
592
|
+
// sparse=false -> 204 (empty tile, no overzoom)
|
|
593
|
+
// Default: vector tiles (pbf) -> false, raster tiles -> true
|
|
594
|
+
const isVector = tileJSON.format === 'pbf';
|
|
595
|
+
const sparse = params.sparse ?? options.sparse ?? !isVector;
|
|
596
|
+
|
|
422
597
|
// eslint-disable-next-line security/detect-object-injection -- id is from config file data source names
|
|
423
598
|
repo[id] = {
|
|
424
599
|
tileJSON,
|
|
425
600
|
publicUrl,
|
|
426
601
|
source,
|
|
427
602
|
sourceType,
|
|
603
|
+
sparse,
|
|
428
604
|
};
|
|
429
605
|
},
|
|
430
606
|
};
|
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_light.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-empty-function */
|
|
2
1
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
3
2
|
'use strict';
|
|
4
3
|
|
|
@@ -7,8 +6,7 @@ export const serve_rendered = {
|
|
|
7
6
|
add: (options, repo, params, id, programOpts, dataResolver) => {},
|
|
8
7
|
remove: (repo, id) => {},
|
|
9
8
|
clear: (repo) => {},
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return param;
|
|
9
|
+
getBatchElevationsFromTile: (data, param, pixels) => {
|
|
10
|
+
return pixels.map(({ index }) => ({ index, elevation: null }));
|
|
13
11
|
},
|
|
14
12
|
};
|