@versatiles/svg-renderer 0.4.0 → 0.5.1
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/README.md +37 -42
- package/dist/index.cjs +278 -230
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +278 -230
- package/dist/index.js.map +1 -1
- package/dist/maplibre.cjs +355 -231
- package/dist/maplibre.cjs.map +1 -1
- package/dist/maplibre.js +355 -231
- package/dist/maplibre.js.map +1 -1
- package/dist/maplibre.umd.js +355 -231
- package/dist/maplibre.umd.js.map +1 -1
- package/package.json +4 -2
package/dist/maplibre.umd.js
CHANGED
|
@@ -117,6 +117,23 @@
|
|
|
117
117
|
font-size: 13px;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
.svg-export-panel .panel-notice {
|
|
121
|
+
font-size: 11px;
|
|
122
|
+
color: #888;
|
|
123
|
+
line-height: 1.4;
|
|
124
|
+
margin-bottom: 12px;
|
|
125
|
+
max-width: 320px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.svg-export-panel .panel-notice a {
|
|
129
|
+
color: inherit;
|
|
130
|
+
text-decoration: underline;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.svg-export-panel .panel-notice a:hover {
|
|
134
|
+
text-decoration: underline;
|
|
135
|
+
}
|
|
136
|
+
|
|
120
137
|
.svg-export-panel .panel-actions {
|
|
121
138
|
display: flex;
|
|
122
139
|
gap: 8px;
|
|
@@ -9194,7 +9211,9 @@
|
|
|
9194
9211
|
this.values = [args[0], args[1], args[2], args[3]];
|
|
9195
9212
|
return;
|
|
9196
9213
|
}
|
|
9197
|
-
throw Error('Unsupported Color arguments: ' +
|
|
9214
|
+
throw Error('Unsupported Color arguments: ' +
|
|
9215
|
+
JSON.stringify(args) +
|
|
9216
|
+
'. Expected a MaplibreColor, hex string (#RRGGBB or #RRGGBBAA), or 3-4 numeric components.');
|
|
9198
9217
|
function h2d(text) {
|
|
9199
9218
|
return parseInt(text, 16);
|
|
9200
9219
|
}
|
|
@@ -9204,25 +9223,9 @@
|
|
|
9204
9223
|
}
|
|
9205
9224
|
get hex() {
|
|
9206
9225
|
return `#${d2h(this.values[0])}${d2h(this.values[1])}${d2h(this.values[2])}${this.values[3] === 255 ? '' : d2h(this.values[3])}`;
|
|
9207
|
-
function d2h(num) {
|
|
9208
|
-
if (num < 0)
|
|
9209
|
-
num = 0;
|
|
9210
|
-
if (num > 255)
|
|
9211
|
-
num = 255;
|
|
9212
|
-
const str = Math.round(num).toString(16).toUpperCase();
|
|
9213
|
-
return str.length < 2 ? '0' + str : str;
|
|
9214
|
-
}
|
|
9215
9226
|
}
|
|
9216
9227
|
get rgb() {
|
|
9217
9228
|
return `#${d2h(this.values[0])}${d2h(this.values[1])}${d2h(this.values[2])}`;
|
|
9218
|
-
function d2h(num) {
|
|
9219
|
-
if (num < 0)
|
|
9220
|
-
num = 0;
|
|
9221
|
-
if (num > 255)
|
|
9222
|
-
num = 255;
|
|
9223
|
-
const str = Math.round(num).toString(16).toUpperCase();
|
|
9224
|
-
return str.length < 2 ? '0' + str : str;
|
|
9225
|
-
}
|
|
9226
9229
|
}
|
|
9227
9230
|
get opacity() {
|
|
9228
9231
|
return this.values[3] / 255;
|
|
@@ -9237,6 +9240,126 @@
|
|
|
9237
9240
|
return new Color(...this.values);
|
|
9238
9241
|
}
|
|
9239
9242
|
}
|
|
9243
|
+
function d2h(num) {
|
|
9244
|
+
if (num < 0)
|
|
9245
|
+
num = 0;
|
|
9246
|
+
if (num > 255)
|
|
9247
|
+
num = 255;
|
|
9248
|
+
const str = Math.round(num).toString(16).toUpperCase();
|
|
9249
|
+
return str.length < 2 ? '0' + str : str;
|
|
9250
|
+
}
|
|
9251
|
+
|
|
9252
|
+
function chainSegments(segments) {
|
|
9253
|
+
// Phase 1: normalize segments left-to-right, then chain
|
|
9254
|
+
normalizeSegments(segments, 0);
|
|
9255
|
+
let chains = greedyChain(segments);
|
|
9256
|
+
// Phase 2: normalize remaining chains top-to-bottom, then chain again
|
|
9257
|
+
normalizeSegments(chains, 1);
|
|
9258
|
+
chains = greedyChain(chains);
|
|
9259
|
+
return chains;
|
|
9260
|
+
}
|
|
9261
|
+
function normalizeSegments(segments, coordIndex) {
|
|
9262
|
+
for (const seg of segments) {
|
|
9263
|
+
const first = seg[0];
|
|
9264
|
+
const last = seg[seg.length - 1];
|
|
9265
|
+
if (first && last && last[coordIndex] < first[coordIndex])
|
|
9266
|
+
seg.reverse();
|
|
9267
|
+
}
|
|
9268
|
+
}
|
|
9269
|
+
function greedyChain(segments) {
|
|
9270
|
+
const byStart = new Map();
|
|
9271
|
+
for (const seg of segments) {
|
|
9272
|
+
const start = seg[0];
|
|
9273
|
+
if (!start)
|
|
9274
|
+
continue;
|
|
9275
|
+
const key = String(start[0]) + ',' + String(start[1]);
|
|
9276
|
+
let list = byStart.get(key);
|
|
9277
|
+
if (!list) {
|
|
9278
|
+
list = [];
|
|
9279
|
+
byStart.set(key, list);
|
|
9280
|
+
}
|
|
9281
|
+
list.push(seg);
|
|
9282
|
+
}
|
|
9283
|
+
const visited = new Set();
|
|
9284
|
+
const chains = [];
|
|
9285
|
+
for (const seg of segments) {
|
|
9286
|
+
if (visited.has(seg))
|
|
9287
|
+
continue;
|
|
9288
|
+
visited.add(seg);
|
|
9289
|
+
const chain = [...seg];
|
|
9290
|
+
let endPoint = chain[chain.length - 1];
|
|
9291
|
+
let candidates = endPoint
|
|
9292
|
+
? byStart.get(String(endPoint[0]) + ',' + String(endPoint[1]))
|
|
9293
|
+
: undefined;
|
|
9294
|
+
while (candidates) {
|
|
9295
|
+
let next;
|
|
9296
|
+
for (const c of candidates) {
|
|
9297
|
+
if (!visited.has(c)) {
|
|
9298
|
+
next = c;
|
|
9299
|
+
break;
|
|
9300
|
+
}
|
|
9301
|
+
}
|
|
9302
|
+
if (!next)
|
|
9303
|
+
break;
|
|
9304
|
+
visited.add(next);
|
|
9305
|
+
for (let i = 1; i < next.length; i++)
|
|
9306
|
+
chain.push(next[i]);
|
|
9307
|
+
endPoint = chain[chain.length - 1];
|
|
9308
|
+
candidates = endPoint
|
|
9309
|
+
? byStart.get(String(endPoint[0]) + ',' + String(endPoint[1]))
|
|
9310
|
+
: undefined;
|
|
9311
|
+
}
|
|
9312
|
+
chains.push(chain);
|
|
9313
|
+
}
|
|
9314
|
+
return chains;
|
|
9315
|
+
}
|
|
9316
|
+
function segmentsToPath(chains, close = false) {
|
|
9317
|
+
let d = '';
|
|
9318
|
+
for (const chain of chains) {
|
|
9319
|
+
const first = chain[0];
|
|
9320
|
+
if (!first)
|
|
9321
|
+
continue;
|
|
9322
|
+
d += 'M' + formatNum(first[0]) + ',' + formatNum(first[1]);
|
|
9323
|
+
let px = first[0];
|
|
9324
|
+
let py = first[1];
|
|
9325
|
+
for (let i = 1; i < chain.length; i++) {
|
|
9326
|
+
const x = chain[i][0];
|
|
9327
|
+
const y = chain[i][1];
|
|
9328
|
+
const dx = x - px;
|
|
9329
|
+
const dy = y - py;
|
|
9330
|
+
if (dy === 0) {
|
|
9331
|
+
const rel = 'h' + formatNum(dx);
|
|
9332
|
+
const abs = 'H' + formatNum(x);
|
|
9333
|
+
d += rel.length <= abs.length ? rel : abs;
|
|
9334
|
+
}
|
|
9335
|
+
else if (dx === 0) {
|
|
9336
|
+
const rel = 'v' + formatNum(dy);
|
|
9337
|
+
const abs = 'V' + formatNum(y);
|
|
9338
|
+
d += rel.length <= abs.length ? rel : abs;
|
|
9339
|
+
}
|
|
9340
|
+
else {
|
|
9341
|
+
const rel = 'l' + formatNum(dx) + ',' + formatNum(dy);
|
|
9342
|
+
const abs = 'L' + formatNum(x) + ',' + formatNum(y);
|
|
9343
|
+
d += rel.length <= abs.length ? rel : abs;
|
|
9344
|
+
}
|
|
9345
|
+
px = x;
|
|
9346
|
+
py = y;
|
|
9347
|
+
}
|
|
9348
|
+
if (close)
|
|
9349
|
+
d += 'z';
|
|
9350
|
+
}
|
|
9351
|
+
return d;
|
|
9352
|
+
}
|
|
9353
|
+
function formatNum(tenths) {
|
|
9354
|
+
if (tenths % 10 === 0)
|
|
9355
|
+
return String(tenths / 10);
|
|
9356
|
+
const negative = tenths < 0;
|
|
9357
|
+
if (negative)
|
|
9358
|
+
tenths = -tenths;
|
|
9359
|
+
const whole = Math.floor(tenths / 10);
|
|
9360
|
+
const frac = tenths % 10;
|
|
9361
|
+
return (negative ? '-' : '') + String(whole) + '.' + String(frac);
|
|
9362
|
+
}
|
|
9240
9363
|
|
|
9241
9364
|
class SVGRenderer {
|
|
9242
9365
|
width;
|
|
@@ -9252,77 +9375,86 @@
|
|
|
9252
9375
|
this.#backgroundColor = Color.transparent;
|
|
9253
9376
|
}
|
|
9254
9377
|
drawBackgroundFill(style) {
|
|
9255
|
-
const color = style.color
|
|
9378
|
+
const color = new Color(style.color);
|
|
9256
9379
|
color.alpha *= style.opacity;
|
|
9257
9380
|
this.#backgroundColor = color;
|
|
9258
9381
|
}
|
|
9259
|
-
drawPolygons(features
|
|
9382
|
+
drawPolygons(features) {
|
|
9260
9383
|
if (features.length === 0)
|
|
9261
9384
|
return;
|
|
9262
|
-
if (opacity <= 0)
|
|
9263
|
-
return;
|
|
9264
|
-
this.#svg.push(`<g opacity="${String(opacity)}">`);
|
|
9265
9385
|
const groups = new Map();
|
|
9266
9386
|
features.forEach(([feature, style]) => {
|
|
9267
|
-
if (style.
|
|
9387
|
+
if (style.opacity <= 0)
|
|
9388
|
+
return;
|
|
9389
|
+
const color = new Color(style.color);
|
|
9390
|
+
if (color.alpha <= 0)
|
|
9268
9391
|
return;
|
|
9269
|
-
const translate = style.translate.
|
|
9392
|
+
const translate = style.translate[0] === 0 && style.translate[1] === 0
|
|
9270
9393
|
? ''
|
|
9271
9394
|
: ` transform="translate(${formatPoint(style.translate, this.#scale)})"`;
|
|
9272
|
-
const
|
|
9395
|
+
const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
|
|
9396
|
+
const key = color.hex + translate + opacityAttr;
|
|
9273
9397
|
let group = groups.get(key);
|
|
9274
9398
|
if (!group) {
|
|
9275
|
-
group = { segments: [], attrs: `${fillAttr(
|
|
9399
|
+
group = { segments: [], attrs: `${fillAttr(color)}${translate}${opacityAttr}` };
|
|
9276
9400
|
groups.set(key, group);
|
|
9277
9401
|
}
|
|
9278
9402
|
feature.geometry.forEach((ring) => {
|
|
9279
|
-
group.segments.push(ring.map((p) => roundXY(p, this.#scale)));
|
|
9403
|
+
group.segments.push(ring.map((p) => roundXY(p.x, p.y, this.#scale)));
|
|
9280
9404
|
});
|
|
9281
9405
|
});
|
|
9282
9406
|
for (const { segments, attrs } of groups.values()) {
|
|
9283
9407
|
const d = segmentsToPath(segments, true);
|
|
9284
9408
|
this.#svg.push(`<path d="${d}" ${attrs} />`);
|
|
9285
9409
|
}
|
|
9286
|
-
this.#svg.push('</g>');
|
|
9287
9410
|
}
|
|
9288
|
-
drawLineStrings(features
|
|
9411
|
+
drawLineStrings(features) {
|
|
9289
9412
|
if (features.length === 0)
|
|
9290
9413
|
return;
|
|
9291
|
-
if (opacity <= 0)
|
|
9292
|
-
return;
|
|
9293
|
-
this.#svg.push(`<g opacity="${String(opacity)}">`);
|
|
9294
9414
|
const groups = new Map();
|
|
9295
9415
|
features.forEach(([feature, style]) => {
|
|
9296
|
-
if (style.
|
|
9416
|
+
if (style.opacity <= 0)
|
|
9297
9417
|
return;
|
|
9298
|
-
const
|
|
9418
|
+
const color = new Color(style.color);
|
|
9419
|
+
if (style.width <= 0 || color.alpha <= 0)
|
|
9420
|
+
return;
|
|
9421
|
+
const translate = style.translate[0] === 0 && style.translate[1] === 0
|
|
9299
9422
|
? ''
|
|
9300
9423
|
: ` transform="translate(${formatPoint(style.translate, this.#scale)})"`;
|
|
9301
|
-
const roundedWidth =
|
|
9424
|
+
const roundedWidth = formatScaled(style.width, this.#scale);
|
|
9425
|
+
const dasharrayStr = style.dasharray
|
|
9426
|
+
? style.dasharray.map((v) => formatScaled(v * style.width, this.#scale)).join(',')
|
|
9427
|
+
: '';
|
|
9428
|
+
const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
|
|
9302
9429
|
const key = [
|
|
9303
|
-
|
|
9430
|
+
color.hex,
|
|
9304
9431
|
roundedWidth,
|
|
9305
9432
|
style.cap,
|
|
9306
9433
|
style.join,
|
|
9307
9434
|
String(style.miterLimit),
|
|
9435
|
+
dasharrayStr,
|
|
9436
|
+
opacityAttr,
|
|
9308
9437
|
translate,
|
|
9309
9438
|
].join('\0');
|
|
9310
9439
|
let group = groups.get(key);
|
|
9311
9440
|
if (!group) {
|
|
9441
|
+
const attrs = [
|
|
9442
|
+
'fill="none"',
|
|
9443
|
+
strokeAttr(color, roundedWidth),
|
|
9444
|
+
`stroke-linecap="${style.cap}"`,
|
|
9445
|
+
`stroke-linejoin="${style.join}"`,
|
|
9446
|
+
`stroke-miterlimit="${String(style.miterLimit)}"`,
|
|
9447
|
+
];
|
|
9448
|
+
if (dasharrayStr)
|
|
9449
|
+
attrs.push(`stroke-dasharray="${dasharrayStr}"`);
|
|
9312
9450
|
group = {
|
|
9313
9451
|
segments: [],
|
|
9314
|
-
attrs:
|
|
9315
|
-
'fill="none"',
|
|
9316
|
-
strokeAttr(style.color, roundedWidth),
|
|
9317
|
-
`stroke-linecap="${style.cap}"`,
|
|
9318
|
-
`stroke-linejoin="${style.join}"`,
|
|
9319
|
-
`stroke-miterlimit="${String(style.miterLimit)}"`,
|
|
9320
|
-
].join(' ') + translate,
|
|
9452
|
+
attrs: attrs.join(' ') + translate + opacityAttr,
|
|
9321
9453
|
};
|
|
9322
9454
|
groups.set(key, group);
|
|
9323
9455
|
}
|
|
9324
9456
|
feature.geometry.forEach((line) => {
|
|
9325
|
-
group.segments.push(line.map((p) => roundXY(p, this.#scale)));
|
|
9457
|
+
group.segments.push(line.map((p) => roundXY(p.x, p.y, this.#scale)));
|
|
9326
9458
|
});
|
|
9327
9459
|
});
|
|
9328
9460
|
for (const { segments, attrs } of groups.values()) {
|
|
@@ -9330,36 +9462,39 @@
|
|
|
9330
9462
|
const d = segmentsToPath(chains);
|
|
9331
9463
|
this.#svg.push(`<path d="${d}" ${attrs} />`);
|
|
9332
9464
|
}
|
|
9333
|
-
this.#svg.push('</g>');
|
|
9334
9465
|
}
|
|
9335
|
-
drawCircles(features
|
|
9466
|
+
drawCircles(features) {
|
|
9336
9467
|
if (features.length === 0)
|
|
9337
9468
|
return;
|
|
9338
|
-
if (opacity <= 0)
|
|
9339
|
-
return;
|
|
9340
|
-
this.#svg.push(`<g opacity="${String(opacity)}">`);
|
|
9341
9469
|
const groups = new Map();
|
|
9342
9470
|
features.forEach(([feature, style]) => {
|
|
9343
|
-
if (style.
|
|
9471
|
+
if (style.opacity <= 0)
|
|
9472
|
+
return;
|
|
9473
|
+
const color = new Color(style.color);
|
|
9474
|
+
if (style.radius <= 0 || color.alpha <= 0)
|
|
9344
9475
|
return;
|
|
9345
|
-
const translate = style.translate.
|
|
9476
|
+
const translate = style.translate[0] === 0 && style.translate[1] === 0
|
|
9346
9477
|
? ''
|
|
9347
9478
|
: ` transform="translate(${formatPoint(style.translate, this.#scale)})"`;
|
|
9348
|
-
const roundedRadius =
|
|
9479
|
+
const roundedRadius = formatScaled(style.radius, this.#scale);
|
|
9480
|
+
const strokeColor = new Color(style.strokeColor);
|
|
9349
9481
|
const strokeAttrs = style.strokeWidth > 0
|
|
9350
|
-
? ` ${strokeAttr(
|
|
9482
|
+
? ` ${strokeAttr(strokeColor, formatScaled(style.strokeWidth, this.#scale))}`
|
|
9351
9483
|
: '';
|
|
9352
|
-
const
|
|
9484
|
+
const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
|
|
9485
|
+
const key = [color.hex, roundedRadius, strokeAttrs, opacityAttr, translate].join('\0');
|
|
9353
9486
|
let group = groups.get(key);
|
|
9354
9487
|
if (!group) {
|
|
9355
9488
|
group = {
|
|
9356
9489
|
points: [],
|
|
9357
|
-
attrs: `r="${roundedRadius}" ${fillAttr(
|
|
9490
|
+
attrs: `r="${roundedRadius}" ${fillAttr(color)}${strokeAttrs}${translate}${opacityAttr}`,
|
|
9358
9491
|
};
|
|
9359
9492
|
groups.set(key, group);
|
|
9360
9493
|
}
|
|
9361
9494
|
feature.geometry.forEach((ring) => {
|
|
9362
|
-
|
|
9495
|
+
const p = ring[0];
|
|
9496
|
+
if (p)
|
|
9497
|
+
group.points.push(roundXY(p.x, p.y, this.#scale));
|
|
9363
9498
|
});
|
|
9364
9499
|
});
|
|
9365
9500
|
for (const { points, attrs } of groups.values()) {
|
|
@@ -9367,7 +9502,6 @@
|
|
|
9367
9502
|
this.#svg.push(`<circle cx="${formatNum(x)}" cy="${formatNum(y)}" ${attrs} />`);
|
|
9368
9503
|
}
|
|
9369
9504
|
}
|
|
9370
|
-
this.#svg.push('</g>');
|
|
9371
9505
|
}
|
|
9372
9506
|
drawRasterTiles(tiles, style) {
|
|
9373
9507
|
if (tiles.length === 0)
|
|
@@ -9393,7 +9527,7 @@
|
|
|
9393
9527
|
for (const tile of tiles) {
|
|
9394
9528
|
const overlap = Math.min(tile.width, tile.height) / 10000; // slight overlap to prevent sub-pixel gaps between tiles
|
|
9395
9529
|
const s = this.#scale;
|
|
9396
|
-
let attrs = `x="${
|
|
9530
|
+
let attrs = `x="${formatScaled(tile.x - overlap, s)}" y="${formatScaled(tile.y - overlap, s)}" width="${formatScaled(tile.width + overlap * 2, s)}" height="${formatScaled(tile.height + overlap * 2, s)}" href="${tile.dataUri}"`;
|
|
9397
9531
|
if (pixelated)
|
|
9398
9532
|
attrs += ' style="image-rendering:pixelated"';
|
|
9399
9533
|
this.#svg.push(`<image ${attrs} />`);
|
|
@@ -9427,115 +9561,16 @@
|
|
|
9427
9561
|
attr += ` stroke-opacity="${color.opacity.toFixed(3)}"`;
|
|
9428
9562
|
return attr;
|
|
9429
9563
|
}
|
|
9430
|
-
function
|
|
9431
|
-
return (v * scale)
|
|
9564
|
+
function formatScaled(v, scale) {
|
|
9565
|
+
return formatNum(Math.round(v * scale * 10));
|
|
9432
9566
|
}
|
|
9433
|
-
function roundXY(
|
|
9434
|
-
return [Math.round(
|
|
9567
|
+
function roundXY(x, y, scale) {
|
|
9568
|
+
return [Math.round(x * scale * 10), Math.round(y * scale * 10)];
|
|
9435
9569
|
}
|
|
9436
9570
|
function formatPoint(p, scale) {
|
|
9437
|
-
const [x, y] = roundXY(p, scale);
|
|
9571
|
+
const [x, y] = roundXY(p[0], p[1], scale);
|
|
9438
9572
|
return formatNum(x) + ',' + formatNum(y);
|
|
9439
9573
|
}
|
|
9440
|
-
function chainSegments(segments) {
|
|
9441
|
-
// Phase 1: normalize segments left-to-right, then chain
|
|
9442
|
-
normalizeSegments(segments, 0);
|
|
9443
|
-
let chains = greedyChain(segments);
|
|
9444
|
-
// Phase 2: normalize remaining chains top-to-bottom, then chain again
|
|
9445
|
-
normalizeSegments(chains, 1);
|
|
9446
|
-
chains = greedyChain(chains);
|
|
9447
|
-
return chains;
|
|
9448
|
-
}
|
|
9449
|
-
function normalizeSegments(segments, coordIndex) {
|
|
9450
|
-
for (const seg of segments) {
|
|
9451
|
-
if (seg[seg.length - 1][coordIndex] < seg[0][coordIndex])
|
|
9452
|
-
seg.reverse();
|
|
9453
|
-
}
|
|
9454
|
-
}
|
|
9455
|
-
function greedyChain(segments) {
|
|
9456
|
-
const byStart = new Map();
|
|
9457
|
-
for (const seg of segments) {
|
|
9458
|
-
const key = String(seg[0][0]) + ',' + String(seg[0][1]);
|
|
9459
|
-
let list = byStart.get(key);
|
|
9460
|
-
if (!list) {
|
|
9461
|
-
list = [];
|
|
9462
|
-
byStart.set(key, list);
|
|
9463
|
-
}
|
|
9464
|
-
list.push(seg);
|
|
9465
|
-
}
|
|
9466
|
-
const visited = new Set();
|
|
9467
|
-
const chains = [];
|
|
9468
|
-
for (const seg of segments) {
|
|
9469
|
-
if (visited.has(seg))
|
|
9470
|
-
continue;
|
|
9471
|
-
visited.add(seg);
|
|
9472
|
-
const chain = [...seg];
|
|
9473
|
-
let endPoint = chain[chain.length - 1];
|
|
9474
|
-
let candidates = byStart.get(String(endPoint[0]) + ',' + String(endPoint[1]));
|
|
9475
|
-
while (candidates) {
|
|
9476
|
-
let next;
|
|
9477
|
-
for (const c of candidates) {
|
|
9478
|
-
if (!visited.has(c)) {
|
|
9479
|
-
next = c;
|
|
9480
|
-
break;
|
|
9481
|
-
}
|
|
9482
|
-
}
|
|
9483
|
-
if (!next)
|
|
9484
|
-
break;
|
|
9485
|
-
visited.add(next);
|
|
9486
|
-
for (let i = 1; i < next.length; i++)
|
|
9487
|
-
chain.push(next[i]);
|
|
9488
|
-
endPoint = chain[chain.length - 1];
|
|
9489
|
-
candidates = byStart.get(String(endPoint[0]) + ',' + String(endPoint[1]));
|
|
9490
|
-
}
|
|
9491
|
-
chains.push(chain);
|
|
9492
|
-
}
|
|
9493
|
-
return chains;
|
|
9494
|
-
}
|
|
9495
|
-
function segmentsToPath(chains, close = false) {
|
|
9496
|
-
let d = '';
|
|
9497
|
-
for (const chain of chains) {
|
|
9498
|
-
d += 'M' + formatNum(chain[0][0]) + ',' + formatNum(chain[0][1]);
|
|
9499
|
-
let px = chain[0][0];
|
|
9500
|
-
let py = chain[0][1];
|
|
9501
|
-
for (let i = 1; i < chain.length; i++) {
|
|
9502
|
-
const x = chain[i][0];
|
|
9503
|
-
const y = chain[i][1];
|
|
9504
|
-
const dx = x - px;
|
|
9505
|
-
const dy = y - py;
|
|
9506
|
-
if (dy === 0) {
|
|
9507
|
-
const rel = 'h' + formatNum(dx);
|
|
9508
|
-
const abs = 'H' + formatNum(x);
|
|
9509
|
-
d += rel.length <= abs.length ? rel : abs;
|
|
9510
|
-
}
|
|
9511
|
-
else if (dx === 0) {
|
|
9512
|
-
const rel = 'v' + formatNum(dy);
|
|
9513
|
-
const abs = 'V' + formatNum(y);
|
|
9514
|
-
d += rel.length <= abs.length ? rel : abs;
|
|
9515
|
-
}
|
|
9516
|
-
else {
|
|
9517
|
-
const rel = 'l' + formatNum(dx) + ',' + formatNum(dy);
|
|
9518
|
-
const abs = 'L' + formatNum(x) + ',' + formatNum(y);
|
|
9519
|
-
d += rel.length <= abs.length ? rel : abs;
|
|
9520
|
-
}
|
|
9521
|
-
px = x;
|
|
9522
|
-
py = y;
|
|
9523
|
-
}
|
|
9524
|
-
if (close)
|
|
9525
|
-
d += 'z';
|
|
9526
|
-
}
|
|
9527
|
-
return d;
|
|
9528
|
-
}
|
|
9529
|
-
function formatNum(tenths) {
|
|
9530
|
-
if (tenths % 10 === 0)
|
|
9531
|
-
return String(tenths / 10);
|
|
9532
|
-
const negative = tenths < 0;
|
|
9533
|
-
if (negative)
|
|
9534
|
-
tenths = -tenths;
|
|
9535
|
-
const whole = Math.floor(tenths / 10);
|
|
9536
|
-
const frac = tenths % 10;
|
|
9537
|
-
return (negative ? '-' : '') + String(whole) + '.' + String(frac);
|
|
9538
|
-
}
|
|
9539
9574
|
|
|
9540
9575
|
/*
|
|
9541
9576
|
* bignumber.js v9.3.1
|
|
@@ -14199,6 +14234,7 @@
|
|
|
14199
14234
|
properties;
|
|
14200
14235
|
patterns;
|
|
14201
14236
|
geometry;
|
|
14237
|
+
#bbox;
|
|
14202
14238
|
constructor(opt) {
|
|
14203
14239
|
this.type = opt.type;
|
|
14204
14240
|
this.id = opt.id;
|
|
@@ -14207,6 +14243,8 @@
|
|
|
14207
14243
|
this.geometry = opt.geometry;
|
|
14208
14244
|
}
|
|
14209
14245
|
getBbox() {
|
|
14246
|
+
if (this.#bbox)
|
|
14247
|
+
return this.#bbox;
|
|
14210
14248
|
let xMin = Infinity;
|
|
14211
14249
|
let yMin = Infinity;
|
|
14212
14250
|
let xMax = -Infinity;
|
|
@@ -14223,7 +14261,8 @@
|
|
|
14223
14261
|
yMax = point.y;
|
|
14224
14262
|
});
|
|
14225
14263
|
});
|
|
14226
|
-
|
|
14264
|
+
this.#bbox = [xMin, yMin, xMax, yMax];
|
|
14265
|
+
return this.#bbox;
|
|
14227
14266
|
}
|
|
14228
14267
|
doesOverlap(bbox) {
|
|
14229
14268
|
const featureBbox = this.getBbox();
|
|
@@ -14241,7 +14280,7 @@
|
|
|
14241
14280
|
|
|
14242
14281
|
function geojsonToFeature(id, polygonFeature) {
|
|
14243
14282
|
const geometry = polygonFeature.geometry.coordinates.map((ring) => {
|
|
14244
|
-
return ring.map((coord) => new Point2D(coord[0], coord[1]));
|
|
14283
|
+
return ring.map((coord) => new Point2D(coord[0] ?? 0, coord[1] ?? 0));
|
|
14245
14284
|
});
|
|
14246
14285
|
return new Feature({
|
|
14247
14286
|
type: 'Polygon',
|
|
@@ -14250,7 +14289,7 @@
|
|
|
14250
14289
|
properties: polygonFeature.properties ?? {},
|
|
14251
14290
|
});
|
|
14252
14291
|
}
|
|
14253
|
-
function
|
|
14292
|
+
function mergePolygonsByFeatureId(featureList) {
|
|
14254
14293
|
const featuresById = new Map();
|
|
14255
14294
|
let nextId = -1;
|
|
14256
14295
|
for (const feature of featureList) {
|
|
@@ -14310,7 +14349,9 @@
|
|
|
14310
14349
|
|
|
14311
14350
|
function calculateTileGrid(width, height, center, zoom, maxzoom) {
|
|
14312
14351
|
const zoomLevel = Math.min(Math.floor(zoom), maxzoom ?? Infinity);
|
|
14313
|
-
const tileCenterCoordinate = center
|
|
14352
|
+
const tileCenterCoordinate = new Point2D(center[0], center[1])
|
|
14353
|
+
.getProject2Pixel()
|
|
14354
|
+
.scale(2 ** zoomLevel);
|
|
14314
14355
|
const tileSize = 2 ** (zoom - zoomLevel + 9); // 512 (2^9) is the standard tile size
|
|
14315
14356
|
const tileCols = width / tileSize;
|
|
14316
14357
|
const tileRows = height / tileSize;
|
|
@@ -14318,11 +14359,15 @@
|
|
|
14318
14359
|
const tileMinY = Math.floor(tileCenterCoordinate.y - tileRows / 2);
|
|
14319
14360
|
const tileMaxX = Math.floor(tileCenterCoordinate.x + tileCols / 2);
|
|
14320
14361
|
const tileMaxY = Math.floor(tileCenterCoordinate.y + tileRows / 2);
|
|
14362
|
+
const tilesPerZoom = 2 ** zoomLevel;
|
|
14321
14363
|
const tiles = [];
|
|
14322
14364
|
for (let x = tileMinX; x <= tileMaxX; x++) {
|
|
14365
|
+
const wrappedX = ((x % tilesPerZoom) + tilesPerZoom) % tilesPerZoom;
|
|
14323
14366
|
for (let y = tileMinY; y <= tileMaxY; y++) {
|
|
14367
|
+
if (y < 0 || y >= tilesPerZoom)
|
|
14368
|
+
continue;
|
|
14324
14369
|
tiles.push({
|
|
14325
|
-
x,
|
|
14370
|
+
x: wrappedX,
|
|
14326
14371
|
y,
|
|
14327
14372
|
offsetX: width / 2 + (x - tileCenterCoordinate.x) * tileSize,
|
|
14328
14373
|
offsetY: height / 2 + (y - tileCenterCoordinate.y) * tileSize,
|
|
@@ -14341,8 +14386,8 @@
|
|
|
14341
14386
|
const contentType = response.headers.get('content-type') ?? 'application/octet-stream';
|
|
14342
14387
|
return { buffer, contentType };
|
|
14343
14388
|
}
|
|
14344
|
-
catch {
|
|
14345
|
-
console.warn(`Failed to load tile: ${tileUrl}
|
|
14389
|
+
catch (error) {
|
|
14390
|
+
console.warn(`Failed to load tile: ${tileUrl}`, error);
|
|
14346
14391
|
return null;
|
|
14347
14392
|
}
|
|
14348
14393
|
}
|
|
@@ -15887,6 +15932,7 @@
|
|
|
15887
15932
|
}
|
|
15888
15933
|
|
|
15889
15934
|
const TILE_EXTENT = 4096;
|
|
15935
|
+
const VTFeatureType = { Unknown: 0, Point: 1, LineString: 2, Polygon: 3 };
|
|
15890
15936
|
async function loadVectorSource(source, job, layerFeatures) {
|
|
15891
15937
|
const tiles = source.tiles;
|
|
15892
15938
|
if (!tiles)
|
|
@@ -15914,17 +15960,17 @@
|
|
|
15914
15960
|
let type;
|
|
15915
15961
|
let list;
|
|
15916
15962
|
switch (featureSrc.type) {
|
|
15917
|
-
case
|
|
15963
|
+
case VTFeatureType.Unknown:
|
|
15918
15964
|
throw Error('Unknown feature type in vector tile');
|
|
15919
|
-
case
|
|
15965
|
+
case VTFeatureType.Point:
|
|
15920
15966
|
type = 'Point';
|
|
15921
15967
|
list = features.points;
|
|
15922
15968
|
break;
|
|
15923
|
-
case
|
|
15969
|
+
case VTFeatureType.LineString:
|
|
15924
15970
|
type = 'LineString';
|
|
15925
15971
|
list = features.linestrings;
|
|
15926
15972
|
break;
|
|
15927
|
-
case
|
|
15973
|
+
case VTFeatureType.Polygon:
|
|
15928
15974
|
type = 'Polygon';
|
|
15929
15975
|
list = features.polygons;
|
|
15930
15976
|
break;
|
|
@@ -15942,13 +15988,14 @@
|
|
|
15942
15988
|
}));
|
|
15943
15989
|
}
|
|
15944
15990
|
|
|
15945
|
-
function loadGeoJSONSource(
|
|
15991
|
+
function loadGeoJSONSource(options) {
|
|
15992
|
+
const { sourceName, data, width, height, zoom, center, layerFeatures } = options;
|
|
15946
15993
|
const existing = layerFeatures.get(sourceName);
|
|
15947
15994
|
const features = existing ?? { points: [], linestrings: [], polygons: [] };
|
|
15948
15995
|
if (!existing)
|
|
15949
15996
|
layerFeatures.set(sourceName, features);
|
|
15950
15997
|
const worldSize = 512 * 2 ** zoom;
|
|
15951
|
-
const centerMercator = center.getProject2Pixel();
|
|
15998
|
+
const centerMercator = new Point2D(center[0], center[1]).getProject2Pixel();
|
|
15952
15999
|
function projectCoord(coord) {
|
|
15953
16000
|
const mercator = new Point2D(coord[0], coord[1]).getProject2Pixel();
|
|
15954
16001
|
return new Point2D((mercator.x - centerMercator.x) * worldSize + width / 2, (mercator.y - centerMercator.y) * worldSize + height / 2);
|
|
@@ -16001,25 +16048,26 @@
|
|
|
16001
16048
|
}
|
|
16002
16049
|
}
|
|
16003
16050
|
function processGeometry(geom, id, properties) {
|
|
16004
|
-
const coords = geom.coordinates;
|
|
16005
16051
|
switch (geom.type) {
|
|
16006
16052
|
case 'Point':
|
|
16007
|
-
addFeature('Point', [[projectCoord(
|
|
16053
|
+
addFeature('Point', [[projectCoord(geom.coordinates)]], id, properties);
|
|
16008
16054
|
break;
|
|
16009
16055
|
case 'MultiPoint':
|
|
16010
|
-
addFeature('Point',
|
|
16056
|
+
addFeature('Point', geom.coordinates.map((c) => [projectCoord(c)]), id, properties);
|
|
16011
16057
|
break;
|
|
16012
16058
|
case 'LineString':
|
|
16013
|
-
addFeature('LineString', [
|
|
16059
|
+
addFeature('LineString', [geom.coordinates.map((c) => projectCoord(c))], id, properties);
|
|
16014
16060
|
break;
|
|
16015
16061
|
case 'MultiLineString':
|
|
16016
|
-
addFeature('LineString',
|
|
16062
|
+
addFeature('LineString', geom.coordinates.map((line) => line.map((c) => projectCoord(c))), id, properties);
|
|
16017
16063
|
break;
|
|
16018
16064
|
case 'Polygon':
|
|
16019
|
-
addFeature('Polygon',
|
|
16065
|
+
addFeature('Polygon', geom.coordinates.map((ring) => ring.map((c) => projectCoord(c))), id, properties);
|
|
16020
16066
|
break;
|
|
16021
16067
|
case 'MultiPolygon':
|
|
16022
|
-
|
|
16068
|
+
for (const polygon of geom.coordinates) {
|
|
16069
|
+
addFeature('Polygon', polygon.map((ring) => ring.map((c) => projectCoord(c))), id, properties);
|
|
16070
|
+
}
|
|
16023
16071
|
break;
|
|
16024
16072
|
case 'GeometryCollection':
|
|
16025
16073
|
for (const g of geom.geometries) {
|
|
@@ -16028,18 +16076,17 @@
|
|
|
16028
16076
|
break;
|
|
16029
16077
|
}
|
|
16030
16078
|
}
|
|
16031
|
-
|
|
16032
|
-
switch (geojson.type) {
|
|
16079
|
+
switch (data.type) {
|
|
16033
16080
|
case 'FeatureCollection':
|
|
16034
|
-
for (const f of
|
|
16081
|
+
for (const f of data.features) {
|
|
16035
16082
|
processGeometry(f.geometry, f.id, (f.properties ?? {}));
|
|
16036
16083
|
}
|
|
16037
16084
|
break;
|
|
16038
16085
|
case 'Feature':
|
|
16039
|
-
processGeometry(
|
|
16086
|
+
processGeometry(data.geometry, data.id, (data.properties ?? {}));
|
|
16040
16087
|
break;
|
|
16041
16088
|
default:
|
|
16042
|
-
processGeometry(
|
|
16089
|
+
processGeometry(data, undefined, {});
|
|
16043
16090
|
break;
|
|
16044
16091
|
}
|
|
16045
16092
|
}
|
|
@@ -16049,7 +16096,7 @@
|
|
|
16049
16096
|
const { zoom, center } = job.view;
|
|
16050
16097
|
const source = job.style.sources[sourceName];
|
|
16051
16098
|
if (source?.type !== 'raster' || !source.tiles) {
|
|
16052
|
-
throw Error(
|
|
16099
|
+
throw Error(`Invalid raster source "${sourceName}": expected type "raster" with a "tiles" array`);
|
|
16053
16100
|
}
|
|
16054
16101
|
const sourceUrl = source.tiles[0];
|
|
16055
16102
|
const { zoomLevel, tileSize, tiles } = calculateTileGrid(width, height, center, zoom, source.maxzoom);
|
|
@@ -16077,24 +16124,34 @@
|
|
|
16077
16124
|
const { zoom, center } = job.view;
|
|
16078
16125
|
const { sources } = job.style;
|
|
16079
16126
|
const layerFeatures = new Map();
|
|
16127
|
+
const loadPromises = [];
|
|
16080
16128
|
for (const [sourceName, sourceSpec] of Object.entries(sources)) {
|
|
16081
16129
|
const source = sourceSpec;
|
|
16082
16130
|
switch (source.type) {
|
|
16083
16131
|
case 'vector':
|
|
16084
|
-
|
|
16132
|
+
loadPromises.push(loadVectorSource(source, job, layerFeatures));
|
|
16085
16133
|
break;
|
|
16086
16134
|
case 'geojson':
|
|
16087
16135
|
if (source.data) {
|
|
16088
|
-
loadGeoJSONSource(
|
|
16136
|
+
loadGeoJSONSource({
|
|
16137
|
+
sourceName,
|
|
16138
|
+
data: source.data,
|
|
16139
|
+
width,
|
|
16140
|
+
height,
|
|
16141
|
+
zoom,
|
|
16142
|
+
center,
|
|
16143
|
+
layerFeatures,
|
|
16144
|
+
});
|
|
16089
16145
|
}
|
|
16090
16146
|
break;
|
|
16091
16147
|
}
|
|
16092
16148
|
}
|
|
16149
|
+
await Promise.all(loadPromises);
|
|
16093
16150
|
for (const [name, features] of layerFeatures) {
|
|
16094
16151
|
layerFeatures.set(name, {
|
|
16095
16152
|
points: features.points,
|
|
16096
16153
|
linestrings: features.linestrings,
|
|
16097
|
-
polygons:
|
|
16154
|
+
polygons: mergePolygonsByFeatureId(features.polygons),
|
|
16098
16155
|
});
|
|
16099
16156
|
}
|
|
16100
16157
|
return layerFeatures;
|
|
@@ -16142,6 +16199,7 @@
|
|
|
16142
16199
|
minzoom;
|
|
16143
16200
|
maxzoom;
|
|
16144
16201
|
filter;
|
|
16202
|
+
filterFn;
|
|
16145
16203
|
paint;
|
|
16146
16204
|
layout;
|
|
16147
16205
|
paintExpressions;
|
|
@@ -16160,6 +16218,7 @@
|
|
|
16160
16218
|
this.source = spec.source;
|
|
16161
16219
|
this.sourceLayer = spec['source-layer'];
|
|
16162
16220
|
this.filter = spec.filter;
|
|
16221
|
+
this.filterFn = featureFilter(this.filter);
|
|
16163
16222
|
}
|
|
16164
16223
|
this.visibility = (spec.layout?.visibility ?? 'visible');
|
|
16165
16224
|
// Initialize paint property expressions
|
|
@@ -16197,7 +16256,7 @@
|
|
|
16197
16256
|
this.layout = new EvaluatedProperties();
|
|
16198
16257
|
for (const [name, expr] of this.paintExpressions) {
|
|
16199
16258
|
if (expr.kind === 'constant' || expr.kind === 'camera') {
|
|
16200
|
-
this.paint.set(name, expr.evaluate(params,
|
|
16259
|
+
this.paint.set(name, expr.evaluate(params, undefined, {}, undefined, availableImages));
|
|
16201
16260
|
}
|
|
16202
16261
|
else {
|
|
16203
16262
|
this.paint.set(name, new PossiblyEvaluatedPropertyValue(expr, params));
|
|
@@ -16205,7 +16264,7 @@
|
|
|
16205
16264
|
}
|
|
16206
16265
|
for (const [name, expr] of this.layoutExpressions) {
|
|
16207
16266
|
if (expr.kind === 'constant' || expr.kind === 'camera') {
|
|
16208
|
-
this.layout.set(name, expr.evaluate(params,
|
|
16267
|
+
this.layout.set(name, expr.evaluate(params, undefined, {}, undefined, availableImages));
|
|
16209
16268
|
}
|
|
16210
16269
|
else {
|
|
16211
16270
|
this.layout.set(name, new PossiblyEvaluatedPropertyValue(expr, params));
|
|
@@ -16216,18 +16275,17 @@
|
|
|
16216
16275
|
function createStyleLayer(spec) {
|
|
16217
16276
|
return new StyleLayer(spec);
|
|
16218
16277
|
}
|
|
16219
|
-
|
|
16220
16278
|
function getLayerStyles(layers) {
|
|
16221
|
-
return layers.map(
|
|
16222
|
-
const styleLayer = createStyleLayer(layerSpecification);
|
|
16223
|
-
return styleLayer;
|
|
16224
|
-
});
|
|
16279
|
+
return layers.map(createStyleLayer);
|
|
16225
16280
|
}
|
|
16226
16281
|
|
|
16227
|
-
async function
|
|
16282
|
+
async function renderMap(job) {
|
|
16228
16283
|
await render(job);
|
|
16229
16284
|
return job.renderer.getString();
|
|
16230
16285
|
}
|
|
16286
|
+
function getFeatures(layerFeatures, layerStyle) {
|
|
16287
|
+
return layerFeatures.get(layerStyle.sourceLayer) ?? layerFeatures.get(layerStyle.source);
|
|
16288
|
+
}
|
|
16231
16289
|
async function render(job) {
|
|
16232
16290
|
const { renderer } = job;
|
|
16233
16291
|
const { zoom } = job.view;
|
|
@@ -16258,54 +16316,55 @@
|
|
|
16258
16316
|
case 'background':
|
|
16259
16317
|
{
|
|
16260
16318
|
renderer.drawBackgroundFill({
|
|
16261
|
-
color:
|
|
16319
|
+
color: getPaint('background-color'),
|
|
16262
16320
|
opacity: getPaint('background-opacity'),
|
|
16263
16321
|
});
|
|
16264
16322
|
}
|
|
16265
16323
|
continue;
|
|
16266
16324
|
case 'fill':
|
|
16267
16325
|
{
|
|
16268
|
-
const polygons = (layerFeatures
|
|
16326
|
+
const polygons = getFeatures(layerFeatures, layerStyle)?.polygons;
|
|
16269
16327
|
if (!polygons || polygons.length === 0)
|
|
16270
16328
|
continue;
|
|
16271
|
-
const
|
|
16272
|
-
|
|
16329
|
+
const polygonFeatures = layerStyle.filterFn
|
|
16330
|
+
? polygons.filter((feature) => layerStyle.filterFn.filter({ zoom }, feature))
|
|
16331
|
+
: polygons;
|
|
16273
16332
|
if (polygonFeatures.length === 0)
|
|
16274
16333
|
continue;
|
|
16275
16334
|
renderer.drawPolygons(polygonFeatures.map((feature) => [
|
|
16276
16335
|
feature,
|
|
16277
16336
|
{
|
|
16278
|
-
color:
|
|
16279
|
-
|
|
16337
|
+
color: getPaint('fill-color', feature),
|
|
16338
|
+
opacity: getPaint('fill-opacity', feature),
|
|
16339
|
+
translate: getPaint('fill-translate', feature),
|
|
16280
16340
|
},
|
|
16281
|
-
])
|
|
16341
|
+
]));
|
|
16282
16342
|
}
|
|
16283
16343
|
continue;
|
|
16284
16344
|
case 'line':
|
|
16285
16345
|
{
|
|
16286
|
-
const lineStrings = (layerFeatures
|
|
16346
|
+
const lineStrings = getFeatures(layerFeatures, layerStyle)?.linestrings;
|
|
16287
16347
|
if (!lineStrings || lineStrings.length === 0)
|
|
16288
16348
|
continue;
|
|
16289
|
-
const
|
|
16290
|
-
|
|
16349
|
+
const lineStringFeatures = layerStyle.filterFn
|
|
16350
|
+
? lineStrings.filter((feature) => layerStyle.filterFn.filter({ zoom }, feature))
|
|
16351
|
+
: lineStrings;
|
|
16291
16352
|
if (lineStringFeatures.length === 0)
|
|
16292
16353
|
continue;
|
|
16293
16354
|
renderer.drawLineStrings(lineStringFeatures.map((feature) => [
|
|
16294
16355
|
feature,
|
|
16295
16356
|
{
|
|
16296
|
-
color:
|
|
16297
|
-
translate:
|
|
16298
|
-
blur: getPaint('line-blur', feature),
|
|
16357
|
+
color: getPaint('line-color', feature),
|
|
16358
|
+
translate: getPaint('line-translate', feature),
|
|
16299
16359
|
cap: getLayout('line-cap', feature),
|
|
16300
16360
|
dasharray: getPaint('line-dasharray', feature),
|
|
16301
|
-
gapWidth: getPaint('line-gap-width', feature),
|
|
16302
16361
|
join: getLayout('line-join', feature),
|
|
16303
16362
|
miterLimit: getLayout('line-miter-limit', feature),
|
|
16304
16363
|
offset: getPaint('line-offset', feature),
|
|
16305
|
-
|
|
16364
|
+
opacity: getPaint('line-opacity', feature),
|
|
16306
16365
|
width: getPaint('line-width', feature),
|
|
16307
16366
|
},
|
|
16308
|
-
])
|
|
16367
|
+
]));
|
|
16309
16368
|
}
|
|
16310
16369
|
continue;
|
|
16311
16370
|
case 'raster':
|
|
@@ -16324,24 +16383,25 @@
|
|
|
16324
16383
|
continue;
|
|
16325
16384
|
case 'circle':
|
|
16326
16385
|
{
|
|
16327
|
-
const points = (layerFeatures
|
|
16386
|
+
const points = getFeatures(layerFeatures, layerStyle)?.points;
|
|
16328
16387
|
if (!points || points.length === 0)
|
|
16329
16388
|
continue;
|
|
16330
|
-
const
|
|
16331
|
-
|
|
16389
|
+
const pointFeatures = layerStyle.filterFn
|
|
16390
|
+
? points.filter((feature) => layerStyle.filterFn.filter({ zoom }, feature))
|
|
16391
|
+
: points;
|
|
16332
16392
|
if (pointFeatures.length === 0)
|
|
16333
16393
|
continue;
|
|
16334
16394
|
renderer.drawCircles(pointFeatures.map((feature) => [
|
|
16335
16395
|
feature,
|
|
16336
16396
|
{
|
|
16337
|
-
color:
|
|
16397
|
+
color: getPaint('circle-color', feature),
|
|
16398
|
+
opacity: getPaint('circle-opacity', feature),
|
|
16338
16399
|
radius: getPaint('circle-radius', feature),
|
|
16339
|
-
|
|
16340
|
-
translate: new Point2D(...getPaint('circle-translate', feature)),
|
|
16400
|
+
translate: getPaint('circle-translate', feature),
|
|
16341
16401
|
strokeWidth: getPaint('circle-stroke-width', feature),
|
|
16342
|
-
strokeColor:
|
|
16402
|
+
strokeColor: getPaint('circle-stroke-color', feature),
|
|
16343
16403
|
},
|
|
16344
|
-
])
|
|
16404
|
+
]));
|
|
16345
16405
|
}
|
|
16346
16406
|
continue;
|
|
16347
16407
|
case 'color-relief':
|
|
@@ -16357,15 +16417,20 @@
|
|
|
16357
16417
|
}
|
|
16358
16418
|
|
|
16359
16419
|
async function renderToSVG(options) {
|
|
16360
|
-
|
|
16361
|
-
|
|
16362
|
-
|
|
16363
|
-
|
|
16364
|
-
|
|
16365
|
-
|
|
16420
|
+
const width = options.width ?? 1024;
|
|
16421
|
+
const height = options.height ?? 1024;
|
|
16422
|
+
const scale = options.scale ?? 1;
|
|
16423
|
+
if (width <= 0)
|
|
16424
|
+
throw new Error('width must be positive');
|
|
16425
|
+
if (height <= 0)
|
|
16426
|
+
throw new Error('height must be positive');
|
|
16427
|
+
if (scale <= 0)
|
|
16428
|
+
throw new Error('scale must be positive');
|
|
16429
|
+
return await renderMap({
|
|
16430
|
+
renderer: new SVGRenderer({ width, height, scale }),
|
|
16366
16431
|
style: options.style,
|
|
16367
16432
|
view: {
|
|
16368
|
-
center:
|
|
16433
|
+
center: [options.lon ?? 0, options.lat ?? 0],
|
|
16369
16434
|
zoom: options.zoom ?? 2,
|
|
16370
16435
|
},
|
|
16371
16436
|
});
|
|
@@ -16377,6 +16442,39 @@
|
|
|
16377
16442
|
throw new Error(`Element not found: ${selector}`);
|
|
16378
16443
|
return el;
|
|
16379
16444
|
}
|
|
16445
|
+
const ALLOWED_TAGS = new Set(['a', 'b', 'i', 'em', 'strong', 'span']);
|
|
16446
|
+
function sanitizeHTML(html) {
|
|
16447
|
+
const parser = new DOMParser();
|
|
16448
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
16449
|
+
return sanitizeNode(doc.body).textContent ?? '';
|
|
16450
|
+
}
|
|
16451
|
+
function sanitizeNode(node) {
|
|
16452
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
16453
|
+
return document.createTextNode(node.textContent ?? '');
|
|
16454
|
+
}
|
|
16455
|
+
if (node.nodeType === Node.ELEMENT_NODE && node instanceof HTMLElement) {
|
|
16456
|
+
const tag = node.tagName.toLowerCase();
|
|
16457
|
+
let span;
|
|
16458
|
+
if (!ALLOWED_TAGS.has(tag)) {
|
|
16459
|
+
span = document.createDocumentFragment();
|
|
16460
|
+
}
|
|
16461
|
+
else {
|
|
16462
|
+
span = document.createElement(tag);
|
|
16463
|
+
if (tag === 'a') {
|
|
16464
|
+
const href = node.getAttribute('href');
|
|
16465
|
+
if (href && /^https?:\/\//i.test(href))
|
|
16466
|
+
span.setAttribute('href', href);
|
|
16467
|
+
span.setAttribute('target', '_blank');
|
|
16468
|
+
span.setAttribute('rel', 'noopener noreferrer');
|
|
16469
|
+
}
|
|
16470
|
+
}
|
|
16471
|
+
for (const child of Array.from(node.childNodes)) {
|
|
16472
|
+
span.append(sanitizeNode(child));
|
|
16473
|
+
}
|
|
16474
|
+
return span;
|
|
16475
|
+
}
|
|
16476
|
+
return document.createTextNode('');
|
|
16477
|
+
}
|
|
16380
16478
|
class SVGExportControl {
|
|
16381
16479
|
map;
|
|
16382
16480
|
container;
|
|
@@ -16428,6 +16526,11 @@
|
|
|
16428
16526
|
<h3>Export SVG</h3>
|
|
16429
16527
|
<button class="panel-close" title="Close">\u00d7</button>
|
|
16430
16528
|
</div>
|
|
16529
|
+
<div class="panel-notice">
|
|
16530
|
+
Note:<br>
|
|
16531
|
+
<span class="panel-attribution"></span><br>
|
|
16532
|
+
Also export of symbols and texts is not supported yet, but you can improve me on <a href="https://github.com/versatiles-org/versatiles-svg-renderer" target="_blank" rel="noopener noreferrer">GitHub</a>.<br>
|
|
16533
|
+
</div>
|
|
16431
16534
|
<div class="panel-inputs">
|
|
16432
16535
|
<label>Width<input type="number" class="input-width" value="${String(this.options.defaultWidth)}" min="1" max="8192"></label>
|
|
16433
16536
|
<label>Height<input type="number" class="input-height" value="${String(this.options.defaultHeight)}" min="1" max="8192"></label>
|
|
@@ -16441,6 +16544,22 @@
|
|
|
16441
16544
|
<button class="btn-open" disabled>Open in Tab</button>
|
|
16442
16545
|
</div>
|
|
16443
16546
|
`;
|
|
16547
|
+
const noticeEl = querySelector(this.panel, '.panel-attribution');
|
|
16548
|
+
const sources = this.map.getStyle().sources;
|
|
16549
|
+
const attributions = [
|
|
16550
|
+
...new Set(Object.values(sources)
|
|
16551
|
+
.map((s) => s.attribution?.trim())
|
|
16552
|
+
.filter((a) => !!a)),
|
|
16553
|
+
];
|
|
16554
|
+
if (attributions.length > 0) {
|
|
16555
|
+
noticeEl.innerHTML =
|
|
16556
|
+
"When publishing the exported map, don't forget to add an attribution like: " +
|
|
16557
|
+
attributions.map(sanitizeHTML).join(', ');
|
|
16558
|
+
}
|
|
16559
|
+
else {
|
|
16560
|
+
noticeEl.textContent =
|
|
16561
|
+
'When publishing the exported map, please check the license terms of the data and include proper attribution.';
|
|
16562
|
+
}
|
|
16444
16563
|
querySelector(this.panel, '.panel-close').addEventListener('click', () => {
|
|
16445
16564
|
this.closePanel();
|
|
16446
16565
|
});
|
|
@@ -16545,7 +16664,11 @@
|
|
|
16545
16664
|
if (this.renderGeneration !== generation)
|
|
16546
16665
|
return;
|
|
16547
16666
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
16548
|
-
|
|
16667
|
+
const errorSpan = document.createElement('span');
|
|
16668
|
+
errorSpan.className = 'preview-loading';
|
|
16669
|
+
errorSpan.textContent = `Error: ${message}`;
|
|
16670
|
+
previewContainer.innerHTML = '';
|
|
16671
|
+
previewContainer.appendChild(errorSpan);
|
|
16549
16672
|
}
|
|
16550
16673
|
}
|
|
16551
16674
|
downloadSVG() {
|
|
@@ -16567,6 +16690,7 @@
|
|
|
16567
16690
|
const blob = new Blob([this.currentSVG], { type: 'image/svg+xml' });
|
|
16568
16691
|
const url = URL.createObjectURL(blob);
|
|
16569
16692
|
window.open(url, '_blank');
|
|
16693
|
+
setTimeout(() => URL.revokeObjectURL(url), 60000);
|
|
16570
16694
|
}
|
|
16571
16695
|
}
|
|
16572
16696
|
|