tileserver-gl-light 5.5.0-pre.1 → 5.5.0-pre.2
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 -2
- package/package.json +9 -9
- package/public/resources/maplibre-gl-inspect.js +2823 -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 +16 -17
- package/src/pmtiles_adapter.js +3 -3
- package/src/promises.js +1 -1
- package/src/render.js +270 -93
- package/src/serve_data.js +6 -8
- package/src/serve_light.js +0 -1
- package/src/serve_rendered.js +372 -205
- package/src/server.js +22 -27
- package/src/utils.js +17 -18
- 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/src/main.js
CHANGED
|
@@ -12,7 +12,8 @@ import fs from 'node:fs';
|
|
|
12
12
|
import fsp from 'node:fs/promises';
|
|
13
13
|
import path from 'path';
|
|
14
14
|
import { fileURLToPath } from 'url';
|
|
15
|
-
import
|
|
15
|
+
import { Readable } from 'node:stream';
|
|
16
|
+
import { pipeline } from 'node:stream/promises';
|
|
16
17
|
import { server } from './server.js';
|
|
17
18
|
import { isValidRemoteUrl } from './utils.js';
|
|
18
19
|
import { openPMtiles, getPMtilesInfo } from './pmtiles_adapter.js';
|
|
@@ -163,7 +164,6 @@ const startWithInputFile = async (inputFile) => {
|
|
|
163
164
|
inputFile = path.resolve(process.cwd(), inputFile);
|
|
164
165
|
inputFilePath = path.dirname(inputFile);
|
|
165
166
|
|
|
166
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Validating local file from CLI argument
|
|
167
167
|
const inputFileStats = await fsp.stat(inputFile);
|
|
168
168
|
if (!inputFileStats.isFile() || inputFileStats.size === 0) {
|
|
169
169
|
console.log(`ERROR: Not a valid input file: ${inputFile}`);
|
|
@@ -314,7 +314,6 @@ const startWithInputFile = async (inputFile) => {
|
|
|
314
314
|
}
|
|
315
315
|
};
|
|
316
316
|
|
|
317
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Config path from CLI argument is expected behavior
|
|
318
317
|
fs.stat(path.resolve(opts.config), async (err, stats) => {
|
|
319
318
|
if (err || !stats.isFile() || stats.size === 0) {
|
|
320
319
|
let inputFile;
|
|
@@ -331,7 +330,6 @@ fs.stat(path.resolve(opts.config), async (err, stats) => {
|
|
|
331
330
|
const files = await fsp.readdir(process.cwd());
|
|
332
331
|
for (const filename of files) {
|
|
333
332
|
if (filename.endsWith('.mbtiles') || filename.endsWith('.pmtiles')) {
|
|
334
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Scanning current directory for tile files
|
|
335
333
|
const inputFilesStats = await fsp.stat(filename);
|
|
336
334
|
if (inputFilesStats.isFile() && inputFilesStats.size > 0) {
|
|
337
335
|
inputFile = filename;
|
|
@@ -346,25 +344,26 @@ fs.stat(path.resolve(opts.config), async (err, stats) => {
|
|
|
346
344
|
const url =
|
|
347
345
|
'https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/zurich_switzerland.mbtiles';
|
|
348
346
|
const filename = 'zurich_switzerland.mbtiles';
|
|
349
|
-
|
|
347
|
+
|
|
350
348
|
const writer = fs.createWriteStream(filename);
|
|
351
349
|
console.log(`No input file found`);
|
|
352
350
|
console.log(`[DEMO] Downloading sample data (${filename}) from ${url}`);
|
|
353
351
|
|
|
354
352
|
try {
|
|
355
|
-
const response = await
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
);
|
|
353
|
+
const response = await fetch(url);
|
|
354
|
+
|
|
355
|
+
if (!response.ok) {
|
|
356
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Convert web ReadableStream to Node.js Readable stream and pipe to file
|
|
360
|
+
const nodeStream = Readable.fromWeb(response.body);
|
|
361
|
+
await pipeline(nodeStream, writer);
|
|
362
|
+
|
|
363
|
+
console.log('Download complete');
|
|
364
|
+
startWithInputFile(filename);
|
|
366
365
|
} catch (error) {
|
|
367
|
-
console.error(`Error downloading file: ${error}`);
|
|
366
|
+
console.error(`Error downloading file: ${error.message || error}`);
|
|
368
367
|
}
|
|
369
368
|
}
|
|
370
369
|
}
|
package/src/pmtiles_adapter.js
CHANGED
|
@@ -81,7 +81,7 @@ class S3Source {
|
|
|
81
81
|
|
|
82
82
|
// Format 1: s3://endpoint/bucket/key (S3-compatible storage)
|
|
83
83
|
// Example: s3://storage.example.com/mybucket/path/to/tiles.pmtile
|
|
84
|
-
const endpointMatch = cleanUrl.match(/^s3:\/\/([
|
|
84
|
+
const endpointMatch = cleanUrl.match(/^s3:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
85
85
|
if (endpointMatch) {
|
|
86
86
|
return {
|
|
87
87
|
endpoint: `https://${endpointMatch[1]}`,
|
|
@@ -95,7 +95,7 @@ class S3Source {
|
|
|
95
95
|
|
|
96
96
|
// Format 2: s3://bucket/key (AWS S3 default)
|
|
97
97
|
// Example: s3://my-bucket/path/to/tiles.pmtiles
|
|
98
|
-
const awsMatch = cleanUrl.match(/^s3:\/\/([
|
|
98
|
+
const awsMatch = cleanUrl.match(/^s3:\/\/([^/]+)\/(.+)$/);
|
|
99
99
|
if (awsMatch) {
|
|
100
100
|
return {
|
|
101
101
|
endpoint: null, // Use default AWS endpoint
|
|
@@ -316,7 +316,7 @@ export function openPMtiles(
|
|
|
316
316
|
if (verbose >= 2) {
|
|
317
317
|
console.log(`Opening PMTiles from local file: ${filePath}`);
|
|
318
318
|
}
|
|
319
|
-
|
|
319
|
+
|
|
320
320
|
const fd = fs.openSync(filePath, 'r');
|
|
321
321
|
const source = new PMTilesFileSource(fd);
|
|
322
322
|
pmtiles = new PMTiles(source);
|
package/src/promises.js
CHANGED
package/src/render.js
CHANGED
|
@@ -5,6 +5,17 @@ import { SphericalMercator } from '@mapbox/sphericalmercator';
|
|
|
5
5
|
|
|
6
6
|
const mercator = new SphericalMercator();
|
|
7
7
|
|
|
8
|
+
// Constants
|
|
9
|
+
const CONSTANTS = {
|
|
10
|
+
DEFAULT_LINE_WIDTH: 1,
|
|
11
|
+
DEFAULT_BORDER_WIDTH_RATIO: 0.1, // 10% of line width
|
|
12
|
+
DEFAULT_FILL_COLOR: 'rgba(255,255,255,0.4)',
|
|
13
|
+
DEFAULT_STROKE_COLOR: 'rgba(0,64,255,0.7)',
|
|
14
|
+
MAX_LINE_WIDTH: 500,
|
|
15
|
+
MAX_BORDER_WIDTH: 250,
|
|
16
|
+
MARKER_LOAD_TIMEOUT: 5000,
|
|
17
|
+
};
|
|
18
|
+
|
|
8
19
|
/**
|
|
9
20
|
* Transforms coordinates to pixels.
|
|
10
21
|
* @param {Array<number>} ll - Longitude/Latitude coordinate pair.
|
|
@@ -17,6 +28,73 @@ const precisePx = (ll, zoom) => {
|
|
|
17
28
|
return [px[0] * scale, px[1] * scale];
|
|
18
29
|
};
|
|
19
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Validates if a string is a valid color value.
|
|
33
|
+
* @param {string} color - Color string to validate.
|
|
34
|
+
* @returns {boolean} True if valid color.
|
|
35
|
+
*/
|
|
36
|
+
const isValidColor = (color) => {
|
|
37
|
+
if (!color || typeof color !== 'string') {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Allow 'none' and 'transparent' keywords
|
|
42
|
+
if (color === 'none' || color === 'transparent') {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Basic validation for common formats
|
|
47
|
+
const hexPattern = /^#([0-9A-Fa-f]{3}){1,2}$/; // 3 or 6 digits
|
|
48
|
+
const hexAlphaPattern = /^#([0-9A-Fa-f]{8})$/; // 8 digits with alpha
|
|
49
|
+
const rgbPattern = /^rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)$/;
|
|
50
|
+
const rgbaPattern = /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)$/;
|
|
51
|
+
const namedColors = [
|
|
52
|
+
'red',
|
|
53
|
+
'blue',
|
|
54
|
+
'green',
|
|
55
|
+
'yellow',
|
|
56
|
+
'black',
|
|
57
|
+
'white',
|
|
58
|
+
'gray',
|
|
59
|
+
'grey',
|
|
60
|
+
'orange',
|
|
61
|
+
'purple',
|
|
62
|
+
'pink',
|
|
63
|
+
'brown',
|
|
64
|
+
'cyan',
|
|
65
|
+
'magenta',
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
hexPattern.test(color) ||
|
|
70
|
+
hexAlphaPattern.test(color) ||
|
|
71
|
+
rgbPattern.test(color) ||
|
|
72
|
+
rgbaPattern.test(color) ||
|
|
73
|
+
namedColors.includes(color.toLowerCase())
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Safely parses a numeric value with bounds checking.
|
|
79
|
+
* @param {string|number} value - Value to parse.
|
|
80
|
+
* @param {number} defaultValue - Default value if parsing fails.
|
|
81
|
+
* @param {number} min - Minimum allowed value.
|
|
82
|
+
* @param {number} max - Maximum allowed value.
|
|
83
|
+
* @returns {number} Parsed and bounded value.
|
|
84
|
+
*/
|
|
85
|
+
const safeParseNumber = (
|
|
86
|
+
value,
|
|
87
|
+
defaultValue,
|
|
88
|
+
min = -Infinity,
|
|
89
|
+
max = Infinity,
|
|
90
|
+
) => {
|
|
91
|
+
const parsed = Number(value);
|
|
92
|
+
if (isNaN(parsed)) {
|
|
93
|
+
return defaultValue;
|
|
94
|
+
}
|
|
95
|
+
return Math.max(min, Math.min(max, parsed));
|
|
96
|
+
};
|
|
97
|
+
|
|
20
98
|
/**
|
|
21
99
|
* Draws a marker in canvas context.
|
|
22
100
|
* @param {CanvasRenderingContext2D} ctx - Canvas context object.
|
|
@@ -25,22 +103,28 @@ const precisePx = (ll, zoom) => {
|
|
|
25
103
|
* @returns {Promise<void>} A promise that resolves when the marker is drawn.
|
|
26
104
|
*/
|
|
27
105
|
const drawMarker = (ctx, marker, z) => {
|
|
28
|
-
return new Promise((resolve) => {
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
29
107
|
const img = new Image();
|
|
30
108
|
const pixelCoords = precisePx(marker.location, z);
|
|
31
109
|
|
|
110
|
+
// Add timeout to prevent hanging on slow/failed image loads
|
|
111
|
+
const timeout = setTimeout(() => {
|
|
112
|
+
reject(new Error(`Marker image load timeout: ${marker.icon}`));
|
|
113
|
+
}, CONSTANTS.MARKER_LOAD_TIMEOUT);
|
|
114
|
+
|
|
32
115
|
const getMarkerCoordinates = (imageWidth, imageHeight, scale) => {
|
|
33
116
|
// Images are placed with their top-left corner at the provided location
|
|
34
117
|
// within the canvas but we expect icons to be centered and above it.
|
|
35
118
|
|
|
36
|
-
//
|
|
119
|
+
// Subtract half of the image's width from the x-coordinate to center
|
|
37
120
|
// the image in relation to the provided location
|
|
38
121
|
let xCoordinate = pixelCoords[0] - imageWidth / 2;
|
|
39
|
-
|
|
122
|
+
|
|
123
|
+
// Subtract the image's height from the y-coordinate to place it above
|
|
40
124
|
// the provided location
|
|
41
125
|
let yCoordinate = pixelCoords[1] - imageHeight;
|
|
42
126
|
|
|
43
|
-
// Since image placement is dependent on the size offsets have to be
|
|
127
|
+
// Since image placement is dependent on the size, offsets have to be
|
|
44
128
|
// scaled as well. Additionally offsets are provided as either positive or
|
|
45
129
|
// negative values so we always add them
|
|
46
130
|
if (marker.offsetX) {
|
|
@@ -57,30 +141,38 @@ const drawMarker = (ctx, marker, z) => {
|
|
|
57
141
|
};
|
|
58
142
|
|
|
59
143
|
const drawOnCanvas = () => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
144
|
+
clearTimeout(timeout);
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
// Check if the image should be resized before being drawn
|
|
148
|
+
const defaultScale = 1;
|
|
149
|
+
const scale = marker.scale ? marker.scale : defaultScale;
|
|
150
|
+
|
|
151
|
+
// Calculate scaled image sizes
|
|
152
|
+
const imageWidth = img.width * scale;
|
|
153
|
+
const imageHeight = img.height * scale;
|
|
154
|
+
|
|
155
|
+
// Pass the desired sizes to get correlating coordinates
|
|
156
|
+
const coords = getMarkerCoordinates(imageWidth, imageHeight, scale);
|
|
157
|
+
|
|
158
|
+
// Draw the image on canvas
|
|
159
|
+
if (scale !== defaultScale) {
|
|
160
|
+
ctx.drawImage(img, coords.x, coords.y, imageWidth, imageHeight);
|
|
161
|
+
} else {
|
|
162
|
+
ctx.drawImage(img, coords.x, coords.y);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Resolve the promise when image has been drawn
|
|
166
|
+
resolve();
|
|
167
|
+
} catch (error) {
|
|
168
|
+
reject(new Error(`Failed to draw marker: ${error.message}`));
|
|
76
169
|
}
|
|
77
|
-
// Resolve the promise when image has been drawn
|
|
78
|
-
resolve();
|
|
79
170
|
};
|
|
80
171
|
|
|
81
172
|
img.onload = drawOnCanvas;
|
|
82
|
-
img.onerror = (
|
|
83
|
-
|
|
173
|
+
img.onerror = () => {
|
|
174
|
+
clearTimeout(timeout);
|
|
175
|
+
reject(new Error(`Failed to load marker image: ${marker.icon}`));
|
|
84
176
|
};
|
|
85
177
|
img.src = marker.icon;
|
|
86
178
|
});
|
|
@@ -89,7 +181,7 @@ const drawMarker = (ctx, marker, z) => {
|
|
|
89
181
|
/**
|
|
90
182
|
* Draws a list of markers onto a canvas.
|
|
91
183
|
* Wraps drawing of markers into list of promises and awaits them.
|
|
92
|
-
* It's required because images are expected to load
|
|
184
|
+
* It's required because images are expected to load asynchronously in canvas js
|
|
93
185
|
* even when provided from a local disk.
|
|
94
186
|
* @param {CanvasRenderingContext2D} ctx - Canvas context object.
|
|
95
187
|
* @param {Array<object>} markers - Marker objects parsed by extractMarkersFromQuery.
|
|
@@ -105,7 +197,26 @@ const drawMarkers = async (ctx, markers, z) => {
|
|
|
105
197
|
}
|
|
106
198
|
|
|
107
199
|
// Await marker drawings before continuing
|
|
108
|
-
|
|
200
|
+
// Use Promise.allSettled to continue even if some markers fail
|
|
201
|
+
const results = await Promise.allSettled(markerPromises);
|
|
202
|
+
|
|
203
|
+
// Log any failures
|
|
204
|
+
results.forEach((result, index) => {
|
|
205
|
+
if (result.status === 'rejected') {
|
|
206
|
+
console.warn(`Marker ${index} failed to render:`, result.reason);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Extracts an option value from a path query string.
|
|
213
|
+
* @param {Array<string>} splitPaths - Path string split by pipe character.
|
|
214
|
+
* @param {string} optionName - Name of the option to extract.
|
|
215
|
+
* @returns {string|undefined} Option value or undefined if not found.
|
|
216
|
+
*/
|
|
217
|
+
const getInlineOption = (splitPaths, optionName) => {
|
|
218
|
+
const found = splitPaths.find((x) => x.startsWith(`${optionName}:`));
|
|
219
|
+
return found ? found.replace(`${optionName}:`, '') : undefined;
|
|
109
220
|
};
|
|
110
221
|
|
|
111
222
|
/**
|
|
@@ -118,21 +229,25 @@ const drawMarkers = async (ctx, markers, z) => {
|
|
|
118
229
|
* @returns {void}
|
|
119
230
|
*/
|
|
120
231
|
const drawPath = (ctx, path, query, pathQuery, z) => {
|
|
121
|
-
const splitPaths = pathQuery.split('|');
|
|
122
|
-
|
|
123
232
|
if (!path || path.length < 2) {
|
|
124
|
-
return
|
|
233
|
+
return;
|
|
125
234
|
}
|
|
126
235
|
|
|
236
|
+
const splitPaths = pathQuery.split('|');
|
|
237
|
+
|
|
238
|
+
// Start the path - transform coordinates to pixels on canvas and draw lines between points
|
|
127
239
|
ctx.beginPath();
|
|
128
240
|
|
|
129
|
-
|
|
130
|
-
for (const pair of path) {
|
|
241
|
+
for (const [i, pair] of path.entries()) {
|
|
131
242
|
const px = precisePx(pair, z);
|
|
132
|
-
|
|
243
|
+
if (i === 0) {
|
|
244
|
+
ctx.moveTo(px[0], px[1]);
|
|
245
|
+
} else {
|
|
246
|
+
ctx.lineTo(px[0], px[1]);
|
|
247
|
+
}
|
|
133
248
|
}
|
|
134
249
|
|
|
135
|
-
// Check if first coordinate matches last coordinate
|
|
250
|
+
// Check if first coordinate matches last coordinate (closed path)
|
|
136
251
|
if (
|
|
137
252
|
path[0][0] === path[path.length - 1][0] &&
|
|
138
253
|
path[0][1] === path[path.length - 1][1]
|
|
@@ -140,77 +255,130 @@ const drawPath = (ctx, path, query, pathQuery, z) => {
|
|
|
140
255
|
ctx.closePath();
|
|
141
256
|
}
|
|
142
257
|
|
|
143
|
-
//
|
|
144
|
-
const
|
|
258
|
+
// --- FILL Logic ---
|
|
259
|
+
const inlineFill = getInlineOption(splitPaths, 'fill');
|
|
260
|
+
const pathHasFill = inlineFill !== undefined;
|
|
261
|
+
|
|
145
262
|
if (query.fill !== undefined || pathHasFill) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
263
|
+
let fillColor;
|
|
264
|
+
|
|
149
265
|
if (pathHasFill) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
266
|
+
fillColor = inlineFill;
|
|
267
|
+
} else if ('fill' in query) {
|
|
268
|
+
fillColor = query.fill || CONSTANTS.DEFAULT_FILL_COLOR;
|
|
269
|
+
} else {
|
|
270
|
+
fillColor = CONSTANTS.DEFAULT_FILL_COLOR;
|
|
153
271
|
}
|
|
154
|
-
ctx.fill();
|
|
155
|
-
}
|
|
156
272
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
273
|
+
// Validate color before using
|
|
274
|
+
if (isValidColor(fillColor)) {
|
|
275
|
+
ctx.fillStyle = fillColor;
|
|
276
|
+
ctx.fill();
|
|
277
|
+
} else {
|
|
278
|
+
console.warn(`Invalid fill color: ${fillColor}, using default`);
|
|
279
|
+
ctx.fillStyle = CONSTANTS.DEFAULT_FILL_COLOR;
|
|
280
|
+
ctx.fill();
|
|
165
281
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// --- WIDTH & BORDER Logic ---
|
|
285
|
+
const inlineWidth = getInlineOption(splitPaths, 'width');
|
|
286
|
+
const pathHasWidth = inlineWidth !== undefined;
|
|
287
|
+
const inlineBorder = getInlineOption(splitPaths, 'border');
|
|
288
|
+
const inlineBorderWidth = getInlineOption(splitPaths, 'borderwidth');
|
|
289
|
+
const pathHasBorder = inlineBorder !== undefined;
|
|
290
|
+
|
|
291
|
+
// Parse line width with validation
|
|
292
|
+
let lineWidth = CONSTANTS.DEFAULT_LINE_WIDTH;
|
|
293
|
+
if (pathHasWidth) {
|
|
294
|
+
lineWidth = safeParseNumber(
|
|
295
|
+
inlineWidth,
|
|
296
|
+
CONSTANTS.DEFAULT_LINE_WIDTH,
|
|
297
|
+
0,
|
|
298
|
+
CONSTANTS.MAX_LINE_WIDTH,
|
|
299
|
+
);
|
|
300
|
+
} else if ('width' in query) {
|
|
301
|
+
lineWidth = safeParseNumber(
|
|
302
|
+
query.width,
|
|
303
|
+
CONSTANTS.DEFAULT_LINE_WIDTH,
|
|
304
|
+
0,
|
|
305
|
+
CONSTANTS.MAX_LINE_WIDTH,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Get border width with validation
|
|
310
|
+
// Default: 10% of line width
|
|
311
|
+
let borderWidth = lineWidth * CONSTANTS.DEFAULT_BORDER_WIDTH_RATIO;
|
|
312
|
+
if (pathHasBorder && inlineBorderWidth) {
|
|
313
|
+
borderWidth = safeParseNumber(
|
|
314
|
+
inlineBorderWidth,
|
|
315
|
+
borderWidth,
|
|
316
|
+
0,
|
|
317
|
+
CONSTANTS.MAX_BORDER_WIDTH,
|
|
318
|
+
);
|
|
319
|
+
} else if (query.borderwidth !== undefined) {
|
|
320
|
+
borderWidth = safeParseNumber(
|
|
321
|
+
query.borderwidth,
|
|
322
|
+
borderWidth,
|
|
323
|
+
0,
|
|
324
|
+
CONSTANTS.MAX_BORDER_WIDTH,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Set rendering style for the start and end points of the path
|
|
329
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap
|
|
330
|
+
const validLineCaps = ['butt', 'round', 'square'];
|
|
331
|
+
ctx.lineCap = validLineCaps.includes(query.linecap) ? query.linecap : 'butt';
|
|
332
|
+
|
|
333
|
+
// Set rendering style for overlapping segments of the path with differing directions
|
|
334
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin
|
|
335
|
+
const validLineJoins = ['miter', 'round', 'bevel'];
|
|
336
|
+
ctx.lineJoin = validLineJoins.includes(query.linejoin)
|
|
337
|
+
? query.linejoin
|
|
338
|
+
: 'miter';
|
|
339
|
+
|
|
340
|
+
// The final border color, prioritized by inline over global query
|
|
341
|
+
const finalBorder = pathHasBorder ? inlineBorder : query.border;
|
|
342
|
+
|
|
343
|
+
// In order to simulate a border we draw the path two times with the first
|
|
344
|
+
// being the wider border part.
|
|
345
|
+
if (finalBorder !== undefined && borderWidth > 0) {
|
|
346
|
+
// Validate border color
|
|
347
|
+
if (isValidColor(finalBorder)) {
|
|
189
348
|
// We need to double the desired border width and add it to the line width
|
|
190
349
|
// in order to get the desired border on each side of the line.
|
|
191
350
|
ctx.lineWidth = lineWidth + borderWidth * 2;
|
|
192
|
-
|
|
193
|
-
ctx.strokeStyle = query.border;
|
|
351
|
+
ctx.strokeStyle = finalBorder;
|
|
194
352
|
ctx.stroke();
|
|
353
|
+
} else {
|
|
354
|
+
console.warn(`Invalid border color: ${finalBorder}, skipping border`);
|
|
195
355
|
}
|
|
196
|
-
ctx.lineWidth = lineWidth;
|
|
197
356
|
}
|
|
198
357
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
358
|
+
// Set line width for the main stroke
|
|
359
|
+
ctx.lineWidth = lineWidth;
|
|
360
|
+
|
|
361
|
+
// --- STROKE Logic ---
|
|
362
|
+
const inlineStroke = getInlineOption(splitPaths, 'stroke');
|
|
363
|
+
const pathHasStroke = inlineStroke !== undefined;
|
|
364
|
+
|
|
365
|
+
let strokeColor;
|
|
366
|
+
if (pathHasStroke) {
|
|
367
|
+
strokeColor = inlineStroke;
|
|
368
|
+
} else if ('stroke' in query) {
|
|
369
|
+
strokeColor = query.stroke;
|
|
211
370
|
} else {
|
|
212
|
-
|
|
371
|
+
strokeColor = CONSTANTS.DEFAULT_STROKE_COLOR;
|
|
213
372
|
}
|
|
373
|
+
|
|
374
|
+
// Validate stroke color
|
|
375
|
+
if (isValidColor(strokeColor)) {
|
|
376
|
+
ctx.strokeStyle = strokeColor;
|
|
377
|
+
} else {
|
|
378
|
+
console.warn(`Invalid stroke color: ${strokeColor}, using default`);
|
|
379
|
+
ctx.strokeStyle = CONSTANTS.DEFAULT_STROKE_COLOR;
|
|
380
|
+
}
|
|
381
|
+
|
|
214
382
|
ctx.stroke();
|
|
215
383
|
};
|
|
216
384
|
|
|
@@ -260,23 +428,32 @@ export const renderOverlay = async (
|
|
|
260
428
|
const canvas = createCanvas(scale * w, scale * h);
|
|
261
429
|
const ctx = canvas.getContext('2d');
|
|
262
430
|
ctx.scale(scale, scale);
|
|
431
|
+
|
|
263
432
|
if (bearing) {
|
|
264
433
|
ctx.translate(w / 2, h / 2);
|
|
265
434
|
ctx.rotate((-bearing / 180) * Math.PI);
|
|
266
435
|
ctx.translate(-center[0], -center[1]);
|
|
267
436
|
} else {
|
|
268
|
-
//
|
|
437
|
+
// Optimized path
|
|
269
438
|
ctx.translate(-center[0] + w / 2, -center[1] + h / 2);
|
|
270
439
|
}
|
|
271
440
|
|
|
272
441
|
// Draw provided paths if any
|
|
273
442
|
paths.forEach((path, i) => {
|
|
274
443
|
const pathQuery = Array.isArray(query.path) ? query.path.at(i) : query.path;
|
|
275
|
-
|
|
444
|
+
try {
|
|
445
|
+
drawPath(ctx, path, query, pathQuery, z);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
console.error(`Error drawing path ${i}:`, error);
|
|
448
|
+
}
|
|
276
449
|
});
|
|
277
450
|
|
|
278
451
|
// Await drawing of markers before rendering the canvas
|
|
279
|
-
|
|
452
|
+
try {
|
|
453
|
+
await drawMarkers(ctx, markers, z);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
console.error('Error drawing markers:', error);
|
|
456
|
+
}
|
|
280
457
|
|
|
281
458
|
return canvas.toBuffer();
|
|
282
459
|
};
|
package/src/serve_data.js
CHANGED
|
@@ -12,7 +12,6 @@ import { SphericalMercator } from '@mapbox/sphericalmercator';
|
|
|
12
12
|
import {
|
|
13
13
|
fixTileJSONCenter,
|
|
14
14
|
getTileUrls,
|
|
15
|
-
isS3Url,
|
|
16
15
|
isValidRemoteUrl,
|
|
17
16
|
fetchTileData,
|
|
18
17
|
} from './utils.js';
|
|
@@ -31,9 +30,9 @@ const packageJson = JSON.parse(
|
|
|
31
30
|
);
|
|
32
31
|
|
|
33
32
|
const isLight = packageJson.name.slice(-6) === '-light';
|
|
34
|
-
const serve_rendered = (
|
|
35
|
-
|
|
36
|
-
)
|
|
33
|
+
const { serve_rendered } = await import(
|
|
34
|
+
`${!isLight ? `./serve_rendered.js` : `./serve_light.js`}`
|
|
35
|
+
);
|
|
37
36
|
|
|
38
37
|
export const serve_data = {
|
|
39
38
|
/**
|
|
@@ -69,7 +68,7 @@ export const serve_data = {
|
|
|
69
68
|
String(req.params.format).replace(/\n|\r/g, ''),
|
|
70
69
|
);
|
|
71
70
|
}
|
|
72
|
-
|
|
71
|
+
|
|
73
72
|
const item = repo[req.params.id];
|
|
74
73
|
if (!item) {
|
|
75
74
|
return res.sendStatus(404);
|
|
@@ -193,7 +192,7 @@ export const serve_data = {
|
|
|
193
192
|
String(req.params.y).replace(/\n|\r/g, ''),
|
|
194
193
|
);
|
|
195
194
|
}
|
|
196
|
-
|
|
195
|
+
|
|
197
196
|
const item = repo?.[req.params.id];
|
|
198
197
|
if (!item) return res.sendStatus(404);
|
|
199
198
|
if (!item.source) return res.status(404).send('Missing source');
|
|
@@ -304,7 +303,7 @@ export const serve_data = {
|
|
|
304
303
|
String(req.params.id).replace(/\n|\r/g, ''),
|
|
305
304
|
);
|
|
306
305
|
}
|
|
307
|
-
|
|
306
|
+
|
|
308
307
|
const item = repo[req.params.id];
|
|
309
308
|
if (!item) {
|
|
310
309
|
return res.sendStatus(404);
|
|
@@ -373,7 +372,6 @@ export const serve_data = {
|
|
|
373
372
|
|
|
374
373
|
// Only check file stats for local files, not remote URLs
|
|
375
374
|
if (!isValidRemoteUrl(inputFile)) {
|
|
376
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- inputFile is from config file, validated above
|
|
377
375
|
const inputFileStats = await fsp.stat(inputFile);
|
|
378
376
|
if (!inputFileStats.isFile() || inputFileStats.size === 0) {
|
|
379
377
|
throw Error(`Not valid input file: "${inputFile}"`);
|
package/src/serve_light.js
CHANGED