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/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) {
|
package/src/server.js
CHANGED
|
@@ -31,9 +31,9 @@ const packageJson = JSON.parse(
|
|
|
31
31
|
);
|
|
32
32
|
const isLight = packageJson.name.slice(-6) === '-light';
|
|
33
33
|
|
|
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
|
/**
|
|
39
39
|
* Starts the server.
|
|
@@ -60,8 +60,7 @@ async function start(opts) {
|
|
|
60
60
|
app.use(
|
|
61
61
|
morgan(logFormat, {
|
|
62
62
|
stream: opts.logFile
|
|
63
|
-
?
|
|
64
|
-
fs.createWriteStream(opts.logFile, { flags: 'a' })
|
|
63
|
+
? fs.createWriteStream(opts.logFile, { flags: 'a' })
|
|
65
64
|
: process.stdout,
|
|
66
65
|
skip: (req, res) =>
|
|
67
66
|
opts.silent && (res.statusCode === 200 || res.statusCode === 304),
|
|
@@ -74,9 +73,8 @@ async function start(opts) {
|
|
|
74
73
|
if (opts.configPath) {
|
|
75
74
|
configPath = path.resolve(opts.configPath);
|
|
76
75
|
try {
|
|
77
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- configPath is from CLI argument, expected behavior
|
|
78
76
|
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
79
|
-
} catch
|
|
77
|
+
} catch {
|
|
80
78
|
console.log('ERROR: Config file not found or invalid!');
|
|
81
79
|
console.log(' See README.md for instructions and sample data.');
|
|
82
80
|
process.exit(1);
|
|
@@ -109,8 +107,7 @@ async function start(opts) {
|
|
|
109
107
|
const startupPromises = [];
|
|
110
108
|
|
|
111
109
|
for (const type of Object.keys(paths)) {
|
|
112
|
-
// eslint-disable-next-line security/detect-object-injection -- type
|
|
113
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename, security/detect-object-injection -- paths[type] constructed from validated config paths
|
|
110
|
+
// eslint-disable-next-line security/detect-object-injection -- paths[type] constructed from validated config paths
|
|
114
111
|
if (!fs.existsSync(paths[type])) {
|
|
115
112
|
console.error(
|
|
116
113
|
// eslint-disable-next-line security/detect-object-injection -- type is from Object.keys of paths config
|
|
@@ -128,7 +125,7 @@ async function start(opts) {
|
|
|
128
125
|
*/
|
|
129
126
|
async function getFiles(directory) {
|
|
130
127
|
// Fetch all entries of the directory and attach type information
|
|
131
|
-
|
|
128
|
+
|
|
132
129
|
const dirEntries = await fs.promises.readdir(directory, {
|
|
133
130
|
withFileTypes: true,
|
|
134
131
|
});
|
|
@@ -157,12 +154,13 @@ async function start(opts) {
|
|
|
157
154
|
|
|
158
155
|
if (options.dataDecorator) {
|
|
159
156
|
try {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
157
|
+
const dataDecoratorPath = path.resolve(paths.root, options.dataDecorator);
|
|
158
|
+
|
|
159
|
+
const module = await import(dataDecoratorPath);
|
|
160
|
+
options.dataDecoratorFunc = module.default;
|
|
164
161
|
} catch (e) {
|
|
165
|
-
|
|
162
|
+
console.error(`Error loading data decorator: ${e}`);
|
|
163
|
+
// Intentionally don't set options.dataDecoratorFunc - let it remain undefined
|
|
166
164
|
}
|
|
167
165
|
}
|
|
168
166
|
|
|
@@ -204,12 +202,13 @@ async function start(opts) {
|
|
|
204
202
|
styleJSON = await res.json();
|
|
205
203
|
} else {
|
|
206
204
|
const styleFile = path.resolve(options.paths.styles, item.style);
|
|
207
|
-
|
|
205
|
+
|
|
208
206
|
const styleFileData = await fs.promises.readFile(styleFile);
|
|
209
207
|
styleJSON = JSON.parse(styleFileData);
|
|
210
208
|
}
|
|
211
|
-
} catch (
|
|
209
|
+
} catch (err) {
|
|
212
210
|
console.log(`Error getting style file "${item.style}"`);
|
|
211
|
+
console.error(err && err.stack ? err.stack : err);
|
|
213
212
|
return false;
|
|
214
213
|
}
|
|
215
214
|
|
|
@@ -224,17 +223,20 @@ async function start(opts) {
|
|
|
224
223
|
(styleSourceId, protocol) => {
|
|
225
224
|
let dataItemId;
|
|
226
225
|
for (const id of Object.keys(data)) {
|
|
227
|
-
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of data config
|
|
228
226
|
if (id === styleSourceId) {
|
|
229
227
|
// Style id was found in data ids, return that id
|
|
230
228
|
dataItemId = id;
|
|
229
|
+
break;
|
|
231
230
|
} else {
|
|
232
231
|
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of data config
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
|
|
232
|
+
const sourceData = data[id];
|
|
233
|
+
|
|
234
|
+
if (
|
|
235
|
+
(sourceData.pmtiles && sourceData.pmtiles === styleSourceId) ||
|
|
236
|
+
(sourceData.mbtiles && sourceData.mbtiles === styleSourceId)
|
|
237
|
+
) {
|
|
237
238
|
dataItemId = id;
|
|
239
|
+
break;
|
|
238
240
|
}
|
|
239
241
|
}
|
|
240
242
|
}
|
|
@@ -289,10 +291,21 @@ async function start(opts) {
|
|
|
289
291
|
function dataResolver(styleSourceId) {
|
|
290
292
|
let resolvedFileType;
|
|
291
293
|
let resolvedInputFile;
|
|
292
|
-
let resolvedSparse = false;
|
|
293
294
|
let resolvedS3Profile;
|
|
294
295
|
let resolvedRequestPayer;
|
|
295
296
|
let resolvedS3Region;
|
|
297
|
+
let resolvedS3UrlFormat;
|
|
298
|
+
let resolvedSparse;
|
|
299
|
+
|
|
300
|
+
// Debug logging to see what we're trying to match
|
|
301
|
+
if (opts.verbose >= 3) {
|
|
302
|
+
console.log(
|
|
303
|
+
`[dataResolver] Looking for styleSourceId: ${styleSourceId}`,
|
|
304
|
+
);
|
|
305
|
+
console.log(
|
|
306
|
+
`[dataResolver] Available data keys: ${Object.keys(data).join(', ')}`,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
296
309
|
|
|
297
310
|
for (const id of Object.keys(data)) {
|
|
298
311
|
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of data config
|
|
@@ -301,45 +314,64 @@ async function start(opts) {
|
|
|
301
314
|
let currentInputFileValue;
|
|
302
315
|
|
|
303
316
|
// Check for recognized file type keys
|
|
304
|
-
if (
|
|
317
|
+
if (Object.hasOwn(sourceData, 'pmtiles')) {
|
|
305
318
|
currentFileType = 'pmtiles';
|
|
306
319
|
currentInputFileValue = sourceData.pmtiles;
|
|
307
|
-
} else if (
|
|
320
|
+
} else if (Object.hasOwn(sourceData, 'mbtiles')) {
|
|
308
321
|
currentFileType = 'mbtiles';
|
|
309
322
|
currentInputFileValue = sourceData.mbtiles;
|
|
310
323
|
}
|
|
311
324
|
|
|
312
325
|
if (currentFileType && currentInputFileValue) {
|
|
326
|
+
// Debug logging
|
|
327
|
+
if (opts.verbose >= 3) {
|
|
328
|
+
console.log(
|
|
329
|
+
`[dataResolver] Checking id="${id}", file="${currentInputFileValue}"`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
313
333
|
// Check if this source matches the styleSourceId
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
334
|
+
// Match by ID, by file path, or by base filename
|
|
335
|
+
const matchById = styleSourceId === id;
|
|
336
|
+
const matchByFile = styleSourceId === currentInputFileValue;
|
|
337
|
+
const matchByBasename =
|
|
338
|
+
styleSourceId.includes(currentInputFileValue) ||
|
|
339
|
+
currentInputFileValue.includes(styleSourceId);
|
|
340
|
+
|
|
341
|
+
if (matchById || matchByFile || matchByBasename) {
|
|
342
|
+
if (opts.verbose >= 2) {
|
|
343
|
+
console.log(
|
|
344
|
+
`[dataResolver] Match found for styleSourceId: ${styleSourceId}. (byId=${matchById}, byFile=${matchByFile}, byBasename=${matchByBasename})`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
318
348
|
resolvedFileType = currentFileType;
|
|
319
349
|
resolvedInputFile = currentInputFileValue;
|
|
320
350
|
|
|
321
|
-
// Get sparse if present
|
|
322
|
-
if (sourceData.hasOwnProperty('sparse')) {
|
|
323
|
-
resolvedSparse = !!sourceData.sparse;
|
|
324
|
-
} else {
|
|
325
|
-
resolvedSparse = false;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
351
|
// Get s3Profile if present
|
|
329
|
-
if (
|
|
352
|
+
if (Object.hasOwn(sourceData, 's3Profile')) {
|
|
330
353
|
resolvedS3Profile = sourceData.s3Profile;
|
|
331
354
|
}
|
|
332
355
|
|
|
356
|
+
// Get s3UrlFormat if present
|
|
357
|
+
if (Object.hasOwn(sourceData, 's3UrlFormat')) {
|
|
358
|
+
resolvedS3UrlFormat = sourceData.s3UrlFormat;
|
|
359
|
+
}
|
|
360
|
+
|
|
333
361
|
// Get requestPayer if present
|
|
334
|
-
if (
|
|
362
|
+
if (Object.hasOwn(sourceData, 'requestPayer')) {
|
|
335
363
|
resolvedRequestPayer = !!sourceData.requestPayer;
|
|
336
364
|
}
|
|
337
365
|
|
|
338
366
|
// Get s3Region if present
|
|
339
|
-
if (
|
|
367
|
+
if (Object.hasOwn(sourceData, 's3Region')) {
|
|
340
368
|
resolvedS3Region = sourceData.s3Region;
|
|
341
369
|
}
|
|
342
370
|
|
|
371
|
+
// Get sparse: per-source overrides global, default to true
|
|
372
|
+
resolvedSparse =
|
|
373
|
+
sourceData.sparse ?? options.sparse ?? true;
|
|
374
|
+
|
|
343
375
|
break; // Found our match, exit the outer loop
|
|
344
376
|
}
|
|
345
377
|
}
|
|
@@ -350,13 +382,23 @@ async function start(opts) {
|
|
|
350
382
|
console.warn(
|
|
351
383
|
`Data source not found for styleSourceId: ${styleSourceId}`,
|
|
352
384
|
);
|
|
385
|
+
console.warn(
|
|
386
|
+
`Available data sources: ${Object.keys(data)
|
|
387
|
+
.map((id) => {
|
|
388
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
389
|
+
const src = data[id];
|
|
390
|
+
return `${id} -> ${src.pmtiles || src.mbtiles || 'unknown'}`;
|
|
391
|
+
})
|
|
392
|
+
.join(', ')}`,
|
|
393
|
+
);
|
|
353
394
|
return {
|
|
354
395
|
inputFile: undefined,
|
|
355
396
|
fileType: undefined,
|
|
356
|
-
sparse: false,
|
|
357
397
|
s3Profile: undefined,
|
|
358
398
|
requestPayer: false,
|
|
359
399
|
s3Region: undefined,
|
|
400
|
+
s3UrlFormat: undefined,
|
|
401
|
+
sparse: true,
|
|
360
402
|
};
|
|
361
403
|
}
|
|
362
404
|
|
|
@@ -384,10 +426,11 @@ async function start(opts) {
|
|
|
384
426
|
return {
|
|
385
427
|
inputFile: resolvedInputFile,
|
|
386
428
|
fileType: resolvedFileType,
|
|
387
|
-
sparse: resolvedSparse,
|
|
388
429
|
s3Profile: resolvedS3Profile,
|
|
389
430
|
requestPayer: resolvedRequestPayer,
|
|
390
431
|
s3Region: resolvedS3Region,
|
|
432
|
+
s3UrlFormat: resolvedS3UrlFormat,
|
|
433
|
+
sparse: resolvedSparse,
|
|
391
434
|
};
|
|
392
435
|
},
|
|
393
436
|
),
|
|
@@ -399,6 +442,8 @@ async function start(opts) {
|
|
|
399
442
|
return success;
|
|
400
443
|
}
|
|
401
444
|
|
|
445
|
+
// Collect style loading promises separately
|
|
446
|
+
const stylePromises = [];
|
|
402
447
|
for (const id of Object.keys(config.styles || {})) {
|
|
403
448
|
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of config.styles
|
|
404
449
|
const item = config.styles[id];
|
|
@@ -406,28 +451,39 @@ async function start(opts) {
|
|
|
406
451
|
console.log(`Missing "style" property for ${id}`);
|
|
407
452
|
continue;
|
|
408
453
|
}
|
|
409
|
-
|
|
454
|
+
stylePromises.push(addStyle(id, item, true, true));
|
|
410
455
|
}
|
|
456
|
+
|
|
457
|
+
// Wait for styles to finish loading, then load data sources
|
|
458
|
+
// This ensures data sources added by styles are included
|
|
459
|
+
startupPromises.push(
|
|
460
|
+
Promise.all(stylePromises).then(() => {
|
|
461
|
+
const dataLoadPromises = [];
|
|
462
|
+
for (const id of Object.keys(data)) {
|
|
463
|
+
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of data config
|
|
464
|
+
const item = data[id];
|
|
465
|
+
|
|
466
|
+
if (!item.pmtiles && !item.mbtiles) {
|
|
467
|
+
console.log(
|
|
468
|
+
`Missing "pmtiles" or "mbtiles" property for ${id} data source`,
|
|
469
|
+
);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
dataLoadPromises.push(
|
|
474
|
+
serve_data.add(options, serving.data, item, id, opts),
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
return Promise.all(dataLoadPromises);
|
|
478
|
+
}),
|
|
479
|
+
);
|
|
480
|
+
|
|
411
481
|
startupPromises.push(
|
|
412
482
|
serve_font(options, serving.fonts, opts).then((sub) => {
|
|
413
483
|
app.use('/', sub);
|
|
414
484
|
}),
|
|
415
485
|
);
|
|
416
|
-
for (const id of Object.keys(data)) {
|
|
417
|
-
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of data config
|
|
418
|
-
const item = data[id];
|
|
419
|
-
// eslint-disable-next-line security/detect-object-injection -- id is from Object.keys of data config
|
|
420
|
-
const fileType = Object.keys(data[id])[0];
|
|
421
|
-
if (!fileType || !(fileType === 'pmtiles' || fileType === 'mbtiles')) {
|
|
422
|
-
console.log(
|
|
423
|
-
`Missing "pmtiles" or "mbtiles" property for ${id} data source`,
|
|
424
|
-
);
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
startupPromises.push(serve_data.add(options, serving.data, item, id, opts));
|
|
428
|
-
}
|
|
429
486
|
if (options.serveAllStyles) {
|
|
430
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- options.paths.styles is from validated config
|
|
431
487
|
fs.readdir(options.paths.styles, { withFileTypes: true }, (err, files) => {
|
|
432
488
|
if (err) {
|
|
433
489
|
return;
|
|
@@ -599,11 +655,10 @@ async function start(opts) {
|
|
|
599
655
|
}
|
|
600
656
|
}
|
|
601
657
|
try {
|
|
602
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- templateFile is from internal templates or config-specified path
|
|
603
658
|
const content = fs.readFileSync(templateFile, 'utf-8');
|
|
604
659
|
const compiled = handlebars.compile(content.toString());
|
|
605
660
|
app.get(urlPath, (req, res, next) => {
|
|
606
|
-
if (opts.verbose) {
|
|
661
|
+
if (opts.verbose >= 1) {
|
|
607
662
|
console.log(`Serving template at path: ${urlPath}`);
|
|
608
663
|
}
|
|
609
664
|
let data = {};
|
|
@@ -623,7 +678,7 @@ async function start(opts) {
|
|
|
623
678
|
if (template === 'wmts') res.set('Content-Type', 'text/xml');
|
|
624
679
|
return res.status(200).send(compiled(data));
|
|
625
680
|
} else {
|
|
626
|
-
if (opts.verbose) {
|
|
681
|
+
if (opts.verbose >= 1) {
|
|
627
682
|
console.log(`Forwarding request for: ${urlPath} to next route`);
|
|
628
683
|
}
|
|
629
684
|
next('route');
|
|
@@ -793,7 +848,7 @@ async function start(opts) {
|
|
|
793
848
|
return null;
|
|
794
849
|
}
|
|
795
850
|
|
|
796
|
-
if (
|
|
851
|
+
if (Object.hasOwn(wmts, 'serve_rendered') && !wmts.serve_rendered) {
|
|
797
852
|
return null;
|
|
798
853
|
}
|
|
799
854
|
|
package/src/utils.js
CHANGED
|
@@ -40,7 +40,6 @@ export function allowedScales(scale, maxScale = 9) {
|
|
|
40
40
|
return 1;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// eslint-disable-next-line security/detect-non-literal-regexp -- maxScale is a number parameter, not user input
|
|
44
43
|
const regex = new RegExp(`^[2-${maxScale}]x$`);
|
|
45
44
|
if (!regex.test(scale)) {
|
|
46
45
|
return null;
|
|
@@ -161,14 +160,13 @@ export function getTileUrls(
|
|
|
161
160
|
const hostParts = urlObject.host.split('.');
|
|
162
161
|
const relativeSubdomainsUsable =
|
|
163
162
|
hostParts.length > 1 &&
|
|
164
|
-
|
|
165
|
-
!/^([0-9]{1,3}\.){3}[0-9]{1,3}(\:[0-9]+)?$/.test(urlObject.host);
|
|
163
|
+
!/^([0-9]{1,3}\.){3}[0-9]{1,3}(:[0-9]+)?$/.test(urlObject.host);
|
|
166
164
|
const newDomains = [];
|
|
167
165
|
for (const domain of domains) {
|
|
168
166
|
if (domain.indexOf('*') !== -1) {
|
|
169
167
|
if (relativeSubdomainsUsable) {
|
|
170
168
|
const newParts = hostParts.slice(1);
|
|
171
|
-
newParts.unshift(domain.replace(
|
|
169
|
+
newParts.unshift(domain.replace(/\*/g, hostParts[0]));
|
|
172
170
|
newDomains.push(newParts.join('.'));
|
|
173
171
|
}
|
|
174
172
|
} else {
|
|
@@ -253,7 +251,7 @@ export function fixTileJSONCenter(tileJSON) {
|
|
|
253
251
|
export function readFile(filename) {
|
|
254
252
|
return new Promise((resolve, reject) => {
|
|
255
253
|
const sanitizedFilename = path.normalize(filename); // Normalize path, remove ..
|
|
256
|
-
|
|
254
|
+
|
|
257
255
|
fs.readFile(String(sanitizedFilename), (err, data) => {
|
|
258
256
|
if (err) {
|
|
259
257
|
reject(err);
|
|
@@ -276,7 +274,7 @@ export function readFile(filename) {
|
|
|
276
274
|
async function getFontPbf(allowedFonts, fontPath, name, range, fallbacks) {
|
|
277
275
|
// eslint-disable-next-line security/detect-object-injection -- name is validated font name from sanitizedName check
|
|
278
276
|
if (!allowedFonts || (allowedFonts[name] && fallbacks)) {
|
|
279
|
-
const fontMatch = name?.match(/^[\p{L}\p{N}
|
|
277
|
+
const fontMatch = name?.match(/^[\p{L}\p{N} \-.~!*'()@&=+,#$[\]]+$/u);
|
|
280
278
|
const sanitizedName = fontMatch?.[0] || 'invalid';
|
|
281
279
|
if (!name || typeof name !== 'string' || name.trim() === '' || !fontMatch) {
|
|
282
280
|
console.error(
|
|
@@ -390,16 +388,13 @@ export async function getFontsPbf(
|
|
|
390
388
|
export async function listFonts(fontPath) {
|
|
391
389
|
const existingFonts = {};
|
|
392
390
|
|
|
393
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- fontPath is from validated config
|
|
394
391
|
const files = await fsPromises.readdir(fontPath);
|
|
395
392
|
for (const file of files) {
|
|
396
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- file is from readdir of validated fontPath
|
|
397
393
|
const stats = await fsPromises.stat(path.join(fontPath, file));
|
|
398
394
|
if (
|
|
399
395
|
stats.isDirectory() &&
|
|
400
396
|
(await existsP(path.join(fontPath, file, '0-255.pbf')))
|
|
401
397
|
) {
|
|
402
|
-
// eslint-disable-next-line security/detect-object-injection -- file is from readdir, used as font name key
|
|
403
398
|
existingFonts[path.basename(file)] = true;
|
|
404
399
|
}
|
|
405
400
|
}
|
|
@@ -415,7 +410,7 @@ export async function listFonts(fontPath) {
|
|
|
415
410
|
export function isValidHttpUrl(string) {
|
|
416
411
|
try {
|
|
417
412
|
return httpTester.test(string);
|
|
418
|
-
} catch
|
|
413
|
+
} catch {
|
|
419
414
|
return false;
|
|
420
415
|
}
|
|
421
416
|
}
|
|
@@ -428,7 +423,7 @@ export function isValidHttpUrl(string) {
|
|
|
428
423
|
export function isS3Url(string) {
|
|
429
424
|
try {
|
|
430
425
|
return s3Tester.test(string) || s3HttpTester.test(string);
|
|
431
|
-
} catch
|
|
426
|
+
} catch {
|
|
432
427
|
return false;
|
|
433
428
|
}
|
|
434
429
|
}
|
|
@@ -445,7 +440,7 @@ export function isValidRemoteUrl(string) {
|
|
|
445
440
|
s3Tester.test(string) ||
|
|
446
441
|
s3HttpTester.test(string)
|
|
447
442
|
);
|
|
448
|
-
} catch
|
|
443
|
+
} catch {
|
|
449
444
|
return false;
|
|
450
445
|
}
|
|
451
446
|
}
|
|
@@ -458,7 +453,7 @@ export function isValidRemoteUrl(string) {
|
|
|
458
453
|
export function isPMTilesProtocol(string) {
|
|
459
454
|
try {
|
|
460
455
|
return pmtilesTester.test(string);
|
|
461
|
-
} catch
|
|
456
|
+
} catch {
|
|
462
457
|
return false;
|
|
463
458
|
}
|
|
464
459
|
}
|
|
@@ -471,11 +466,40 @@ export function isPMTilesProtocol(string) {
|
|
|
471
466
|
export function isMBTilesProtocol(string) {
|
|
472
467
|
try {
|
|
473
468
|
return mbtilesTester.test(string);
|
|
474
|
-
} catch
|
|
469
|
+
} catch {
|
|
475
470
|
return false;
|
|
476
471
|
}
|
|
477
472
|
}
|
|
478
473
|
|
|
474
|
+
/**
|
|
475
|
+
* Converts a longitude/latitude point to tile and pixel coordinates at a given zoom level.
|
|
476
|
+
* @param {number} lon - Longitude in degrees.
|
|
477
|
+
* @param {number} lat - Latitude in degrees.
|
|
478
|
+
* @param {number} zoom - Zoom level.
|
|
479
|
+
* @param {number} tileSize - Size of the tile in pixels (e.g., 256 or 512).
|
|
480
|
+
* @returns {{tileX: number, tileY: number, pixelX: number, pixelY: number}} - Tile and pixel coordinates.
|
|
481
|
+
*/
|
|
482
|
+
export function lonLatToTilePixel(lon, lat, zoom, tileSize) {
|
|
483
|
+
let siny = Math.sin((lat * Math.PI) / 180);
|
|
484
|
+
// Truncating to 0.9999 effectively limits latitude to 89.189. This is
|
|
485
|
+
// about a third of a tile past the edge of the world tile.
|
|
486
|
+
siny = Math.min(Math.max(siny, -0.9999), 0.9999);
|
|
487
|
+
|
|
488
|
+
const xWorld = tileSize * (0.5 + lon / 360);
|
|
489
|
+
const yWorld =
|
|
490
|
+
tileSize * (0.5 - Math.log((1 + siny) / (1 - siny)) / (4 * Math.PI));
|
|
491
|
+
|
|
492
|
+
const scale = 1 << zoom;
|
|
493
|
+
|
|
494
|
+
const tileX = Math.floor((xWorld * scale) / tileSize);
|
|
495
|
+
const tileY = Math.floor((yWorld * scale) / tileSize);
|
|
496
|
+
|
|
497
|
+
const pixelX = Math.floor(xWorld * scale) - tileX * tileSize;
|
|
498
|
+
const pixelY = Math.floor(yWorld * scale) - tileY * tileSize;
|
|
499
|
+
|
|
500
|
+
return { tileX, tileY, pixelX, pixelY };
|
|
501
|
+
}
|
|
502
|
+
|
|
479
503
|
/**
|
|
480
504
|
* Fetches tile data from either PMTiles or MBTiles source.
|
|
481
505
|
* @param {object} source - The source object, which may contain a mbtiles object, or pmtiles object.
|
|
@@ -487,15 +511,18 @@ export function isMBTilesProtocol(string) {
|
|
|
487
511
|
*/
|
|
488
512
|
export async function fetchTileData(source, sourceType, z, x, y) {
|
|
489
513
|
if (sourceType === 'pmtiles') {
|
|
490
|
-
|
|
514
|
+
try {
|
|
491
515
|
const tileinfo = await getPMtilesTile(source, z, x, y);
|
|
492
|
-
if (!tileinfo?.data) return
|
|
493
|
-
|
|
494
|
-
})
|
|
516
|
+
if (!tileinfo?.data) return null;
|
|
517
|
+
return { data: tileinfo.data, headers: tileinfo.header };
|
|
518
|
+
} catch (error) {
|
|
519
|
+
console.error('Error fetching PMTiles tile:', error);
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
495
522
|
} else if (sourceType === 'mbtiles') {
|
|
496
|
-
return
|
|
523
|
+
return new Promise((resolve) => {
|
|
497
524
|
source.getTile(z, x, y, (err, tileData, tileHeader) => {
|
|
498
|
-
if (err) {
|
|
525
|
+
if (err || tileData == null) {
|
|
499
526
|
return resolve(null);
|
|
500
527
|
}
|
|
501
528
|
resolve({ data: tileData, headers: tileHeader });
|