@versatiles/svg-renderer 0.5.2 → 0.6.0
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 +26 -23
- package/dist/index.cjs +375 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +375 -15
- package/dist/index.js.map +1 -1
- package/dist/maplibre.cjs +396 -21
- package/dist/maplibre.cjs.map +1 -1
- package/dist/maplibre.d.ts +1 -0
- package/dist/maplibre.js +396 -21
- package/dist/maplibre.js.map +1 -1
- package/dist/maplibre.umd.js +396 -21
- package/dist/maplibre.umd.js.map +1 -1
- package/package.json +2 -2
package/dist/maplibre.cjs
CHANGED
|
@@ -65,6 +65,10 @@ const PANEL_CSS = `
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
.svg-export-panel .panel-inputs {
|
|
68
|
+
margin-bottom: 12px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.svg-export-panel .panel-inputs .grid {
|
|
68
72
|
display: grid;
|
|
69
73
|
grid-template-columns: 1fr 1fr;
|
|
70
74
|
gap: 8px;
|
|
@@ -72,11 +76,15 @@ const PANEL_CSS = `
|
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
.svg-export-panel .panel-inputs label {
|
|
79
|
+
font-size: 12px;
|
|
80
|
+
color: #666;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.svg-export-panel .panel-inputs .grid label {
|
|
75
84
|
display: flex;
|
|
76
85
|
flex-direction: column;
|
|
77
86
|
gap: 4px;
|
|
78
|
-
|
|
79
|
-
color: #666;
|
|
87
|
+
width: 100%;
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
.svg-export-panel .panel-inputs input {
|
|
@@ -84,7 +92,6 @@ const PANEL_CSS = `
|
|
|
84
92
|
border: 1px solid #ccc;
|
|
85
93
|
border-radius: 4px;
|
|
86
94
|
font-size: 13px;
|
|
87
|
-
width: 100%;
|
|
88
95
|
box-sizing: border-box;
|
|
89
96
|
}
|
|
90
97
|
|
|
@@ -3002,6 +3009,23 @@ var paint_raster = {
|
|
|
3002
3009
|
},
|
|
3003
3010
|
"property-type": "data-constant"
|
|
3004
3011
|
},
|
|
3012
|
+
resampling: {
|
|
3013
|
+
type: "enum",
|
|
3014
|
+
values: {
|
|
3015
|
+
linear: {
|
|
3016
|
+
},
|
|
3017
|
+
nearest: {
|
|
3018
|
+
}
|
|
3019
|
+
},
|
|
3020
|
+
"default": "linear",
|
|
3021
|
+
expression: {
|
|
3022
|
+
interpolated: false,
|
|
3023
|
+
parameters: [
|
|
3024
|
+
"zoom"
|
|
3025
|
+
]
|
|
3026
|
+
},
|
|
3027
|
+
"property-type": "data-constant"
|
|
3028
|
+
},
|
|
3005
3029
|
"raster-resampling": {
|
|
3006
3030
|
type: "enum",
|
|
3007
3031
|
values: {
|
|
@@ -3152,6 +3176,23 @@ var paint_hillshade = {
|
|
|
3152
3176
|
]
|
|
3153
3177
|
},
|
|
3154
3178
|
"property-type": "data-constant"
|
|
3179
|
+
},
|
|
3180
|
+
resampling: {
|
|
3181
|
+
type: "enum",
|
|
3182
|
+
values: {
|
|
3183
|
+
linear: {
|
|
3184
|
+
},
|
|
3185
|
+
nearest: {
|
|
3186
|
+
}
|
|
3187
|
+
},
|
|
3188
|
+
"default": "linear",
|
|
3189
|
+
expression: {
|
|
3190
|
+
interpolated: false,
|
|
3191
|
+
parameters: [
|
|
3192
|
+
"zoom"
|
|
3193
|
+
]
|
|
3194
|
+
},
|
|
3195
|
+
"property-type": "data-constant"
|
|
3155
3196
|
}
|
|
3156
3197
|
};
|
|
3157
3198
|
var paint_background = {
|
|
@@ -3572,6 +3613,23 @@ var v8Spec = {
|
|
|
3572
3613
|
]
|
|
3573
3614
|
},
|
|
3574
3615
|
"property-type": "color-ramp"
|
|
3616
|
+
},
|
|
3617
|
+
resampling: {
|
|
3618
|
+
type: "enum",
|
|
3619
|
+
values: {
|
|
3620
|
+
linear: {
|
|
3621
|
+
},
|
|
3622
|
+
nearest: {
|
|
3623
|
+
}
|
|
3624
|
+
},
|
|
3625
|
+
"default": "linear",
|
|
3626
|
+
expression: {
|
|
3627
|
+
interpolated: false,
|
|
3628
|
+
parameters: [
|
|
3629
|
+
"zoom"
|
|
3630
|
+
]
|
|
3631
|
+
},
|
|
3632
|
+
"property-type": "data-constant"
|
|
3575
3633
|
}
|
|
3576
3634
|
},
|
|
3577
3635
|
paint_background: paint_background,
|
|
@@ -6416,11 +6474,12 @@ class CollatorExpression {
|
|
|
6416
6474
|
}
|
|
6417
6475
|
|
|
6418
6476
|
class NumberFormat {
|
|
6419
|
-
constructor(number, locale, currency, minFractionDigits, maxFractionDigits) {
|
|
6477
|
+
constructor(number, locale, currency, unit, minFractionDigits, maxFractionDigits) {
|
|
6420
6478
|
this.type = StringType;
|
|
6421
6479
|
this.number = number;
|
|
6422
6480
|
this.locale = locale;
|
|
6423
6481
|
this.currency = currency;
|
|
6482
|
+
this.unit = unit;
|
|
6424
6483
|
this.minFractionDigits = minFractionDigits;
|
|
6425
6484
|
this.maxFractionDigits = maxFractionDigits;
|
|
6426
6485
|
}
|
|
@@ -6445,6 +6504,15 @@ class NumberFormat {
|
|
|
6445
6504
|
if (!currency)
|
|
6446
6505
|
return null;
|
|
6447
6506
|
}
|
|
6507
|
+
let unit = null;
|
|
6508
|
+
if (options['unit']) {
|
|
6509
|
+
unit = context.parse(options['unit'], 1, StringType);
|
|
6510
|
+
if (!unit)
|
|
6511
|
+
return null;
|
|
6512
|
+
}
|
|
6513
|
+
if (currency && unit) {
|
|
6514
|
+
return context.error('NumberFormat options `currency` and `unit` are mutually exclusive');
|
|
6515
|
+
}
|
|
6448
6516
|
let minFractionDigits = null;
|
|
6449
6517
|
if (options['min-fraction-digits']) {
|
|
6450
6518
|
minFractionDigits = context.parse(options['min-fraction-digits'], 1, NumberType);
|
|
@@ -6457,12 +6525,13 @@ class NumberFormat {
|
|
|
6457
6525
|
if (!maxFractionDigits)
|
|
6458
6526
|
return null;
|
|
6459
6527
|
}
|
|
6460
|
-
return new NumberFormat(number, locale, currency, minFractionDigits, maxFractionDigits);
|
|
6528
|
+
return new NumberFormat(number, locale, currency, unit, minFractionDigits, maxFractionDigits);
|
|
6461
6529
|
}
|
|
6462
6530
|
evaluate(ctx) {
|
|
6463
6531
|
return new Intl.NumberFormat(this.locale ? this.locale.evaluate(ctx) : [], {
|
|
6464
|
-
style: this.currency ? 'currency' : 'decimal',
|
|
6532
|
+
style: this.currency ? 'currency' : this.unit ? 'unit' : 'decimal',
|
|
6465
6533
|
currency: this.currency ? this.currency.evaluate(ctx) : undefined,
|
|
6534
|
+
unit: this.unit ? this.unit.evaluate(ctx) : undefined,
|
|
6466
6535
|
minimumFractionDigits: this.minFractionDigits
|
|
6467
6536
|
? this.minFractionDigits.evaluate(ctx)
|
|
6468
6537
|
: undefined,
|
|
@@ -6479,6 +6548,9 @@ class NumberFormat {
|
|
|
6479
6548
|
if (this.currency) {
|
|
6480
6549
|
fn(this.currency);
|
|
6481
6550
|
}
|
|
6551
|
+
if (this.unit) {
|
|
6552
|
+
fn(this.unit);
|
|
6553
|
+
}
|
|
6482
6554
|
if (this.minFractionDigits) {
|
|
6483
6555
|
fn(this.minFractionDigits);
|
|
6484
6556
|
}
|
|
@@ -8217,6 +8289,16 @@ CompoundExpression.register(expressions$1, {
|
|
|
8217
8289
|
varargs(ValueType),
|
|
8218
8290
|
(ctx, args) => args.map((arg) => valueToString(arg.evaluate(ctx))).join('')
|
|
8219
8291
|
],
|
|
8292
|
+
split: [
|
|
8293
|
+
array(StringType),
|
|
8294
|
+
[StringType, StringType],
|
|
8295
|
+
(ctx, [s, delim]) => s.evaluate(ctx).split(delim.evaluate(ctx))
|
|
8296
|
+
],
|
|
8297
|
+
join: [
|
|
8298
|
+
StringType,
|
|
8299
|
+
[array(StringType), StringType],
|
|
8300
|
+
(ctx, [arr, delim]) => arr.value.join(delim.evaluate(ctx))
|
|
8301
|
+
],
|
|
8220
8302
|
'resolved-locale': [
|
|
8221
8303
|
StringType,
|
|
8222
8304
|
[CollatorType],
|
|
@@ -9373,7 +9455,7 @@ class SVGRenderer {
|
|
|
9373
9455
|
color.alpha *= style.opacity;
|
|
9374
9456
|
this.#backgroundColor = color;
|
|
9375
9457
|
}
|
|
9376
|
-
drawPolygons(features) {
|
|
9458
|
+
drawPolygons(id, features) {
|
|
9377
9459
|
if (features.length === 0)
|
|
9378
9460
|
return;
|
|
9379
9461
|
const groups = new Map();
|
|
@@ -9397,12 +9479,14 @@ class SVGRenderer {
|
|
|
9397
9479
|
group.segments.push(ring.map((p) => roundXY(p.x, p.y)));
|
|
9398
9480
|
});
|
|
9399
9481
|
});
|
|
9482
|
+
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
9400
9483
|
for (const { segments, attrs } of groups.values()) {
|
|
9401
9484
|
const d = segmentsToPath(segments, true);
|
|
9402
9485
|
this.#svg.push(`<path d="${d}" ${attrs} />`);
|
|
9403
9486
|
}
|
|
9487
|
+
this.#svg.push('</g>');
|
|
9404
9488
|
}
|
|
9405
|
-
drawLineStrings(features) {
|
|
9489
|
+
drawLineStrings(id, features) {
|
|
9406
9490
|
if (features.length === 0)
|
|
9407
9491
|
return;
|
|
9408
9492
|
const groups = new Map();
|
|
@@ -9451,13 +9535,15 @@ class SVGRenderer {
|
|
|
9451
9535
|
group.segments.push(line.map((p) => roundXY(p.x, p.y)));
|
|
9452
9536
|
});
|
|
9453
9537
|
});
|
|
9538
|
+
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
9454
9539
|
for (const { segments, attrs } of groups.values()) {
|
|
9455
9540
|
const chains = chainSegments(segments);
|
|
9456
9541
|
const d = segmentsToPath(chains);
|
|
9457
9542
|
this.#svg.push(`<path d="${d}" ${attrs} />`);
|
|
9458
9543
|
}
|
|
9544
|
+
this.#svg.push('</g>');
|
|
9459
9545
|
}
|
|
9460
|
-
drawCircles(features) {
|
|
9546
|
+
drawCircles(id, features) {
|
|
9461
9547
|
if (features.length === 0)
|
|
9462
9548
|
return;
|
|
9463
9549
|
const groups = new Map();
|
|
@@ -9489,13 +9575,108 @@ class SVGRenderer {
|
|
|
9489
9575
|
group.points.push(roundXY(p.x, p.y));
|
|
9490
9576
|
});
|
|
9491
9577
|
});
|
|
9578
|
+
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
9492
9579
|
for (const { points, attrs } of groups.values()) {
|
|
9493
9580
|
for (const [x, y] of points) {
|
|
9494
9581
|
this.#svg.push(`<circle cx="${formatNum(x)}" cy="${formatNum(y)}" ${attrs} />`);
|
|
9495
9582
|
}
|
|
9496
9583
|
}
|
|
9584
|
+
this.#svg.push('</g>');
|
|
9497
9585
|
}
|
|
9498
|
-
|
|
9586
|
+
drawLabels(id, features) {
|
|
9587
|
+
if (features.length === 0)
|
|
9588
|
+
return;
|
|
9589
|
+
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
9590
|
+
for (const [feature, style] of features) {
|
|
9591
|
+
if (style.opacity <= 0 || !style.text)
|
|
9592
|
+
continue;
|
|
9593
|
+
const color = new Color(style.color);
|
|
9594
|
+
if (color.alpha <= 0)
|
|
9595
|
+
continue;
|
|
9596
|
+
const ring = feature.geometry[0];
|
|
9597
|
+
if (!ring || ring.length === 0)
|
|
9598
|
+
continue;
|
|
9599
|
+
const point = ring[Math.floor(ring.length / 2)];
|
|
9600
|
+
const [px, py] = roundXY(point.x, point.y);
|
|
9601
|
+
const fontSize = formatScaled(style.size);
|
|
9602
|
+
const fontFamily = style.font.join(', ') + ', Helvetica, Arial, sans-serif';
|
|
9603
|
+
const [svgAnchor, baseline] = mapTextAnchor(style.anchor);
|
|
9604
|
+
const offsetX = style.offset[0] * style.size;
|
|
9605
|
+
const offsetY = style.offset[1] * style.size;
|
|
9606
|
+
const [dx, dy] = roundXY(offsetX, offsetY);
|
|
9607
|
+
const attrs = [
|
|
9608
|
+
`x="${formatNum(px)}"`,
|
|
9609
|
+
`y="${formatNum(py)}"`,
|
|
9610
|
+
`font-family="${escapeXml(fontFamily)}"`,
|
|
9611
|
+
`font-size="${fontSize}"`,
|
|
9612
|
+
`text-anchor="${svgAnchor}"`,
|
|
9613
|
+
`dominant-baseline="${baseline}"`,
|
|
9614
|
+
];
|
|
9615
|
+
if (dx !== 0)
|
|
9616
|
+
attrs.push(`dx="${formatNum(dx)}"`);
|
|
9617
|
+
if (dy !== 0)
|
|
9618
|
+
attrs.push(`dy="${formatNum(dy)}"`);
|
|
9619
|
+
if (style.rotate !== 0) {
|
|
9620
|
+
attrs.push(`transform="rotate(${String(style.rotate)},${formatNum(px)},${formatNum(py)})"`);
|
|
9621
|
+
}
|
|
9622
|
+
const haloColor = new Color(style.haloColor);
|
|
9623
|
+
if (style.haloWidth > 0 && haloColor.alpha > 0) {
|
|
9624
|
+
const haloWidth = formatScaled(style.haloWidth);
|
|
9625
|
+
attrs.push('paint-order="stroke fill"', `stroke="${haloColor.rgb}"`, `stroke-width="${haloWidth}"`, 'stroke-linejoin="round"');
|
|
9626
|
+
if (haloColor.alpha < 255)
|
|
9627
|
+
attrs.push(`stroke-opacity="${haloColor.opacity.toFixed(3)}"`);
|
|
9628
|
+
}
|
|
9629
|
+
attrs.push(fillAttr(color));
|
|
9630
|
+
if (style.opacity < 1)
|
|
9631
|
+
attrs.push(`opacity="${style.opacity.toFixed(3)}"`);
|
|
9632
|
+
this.#svg.push(`<text ${attrs.join(' ')}>${escapeXml(style.text)}</text>`);
|
|
9633
|
+
}
|
|
9634
|
+
this.#svg.push('</g>');
|
|
9635
|
+
}
|
|
9636
|
+
drawIcons(id, features, spriteAtlas) {
|
|
9637
|
+
if (features.length === 0)
|
|
9638
|
+
return;
|
|
9639
|
+
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
9640
|
+
for (const [feature, style] of features) {
|
|
9641
|
+
if (style.opacity <= 0)
|
|
9642
|
+
continue;
|
|
9643
|
+
const sprite = spriteAtlas.get(style.image);
|
|
9644
|
+
if (!sprite)
|
|
9645
|
+
continue;
|
|
9646
|
+
const ring = feature.geometry[0];
|
|
9647
|
+
if (!ring || ring.length === 0)
|
|
9648
|
+
continue;
|
|
9649
|
+
const point = ring[Math.floor(ring.length / 2)];
|
|
9650
|
+
const scale = style.size / sprite.pixelRatio;
|
|
9651
|
+
const iconW = sprite.width * scale;
|
|
9652
|
+
const iconH = sprite.height * scale;
|
|
9653
|
+
const [anchorDx, anchorDy] = mapIconAnchor(style.anchor, iconW, iconH);
|
|
9654
|
+
const ox = style.offset[0] * style.size + anchorDx;
|
|
9655
|
+
const oy = style.offset[1] * style.size + anchorDy;
|
|
9656
|
+
const x = point.x + ox;
|
|
9657
|
+
const y = point.y + oy;
|
|
9658
|
+
const [sx, sy] = roundXY(x, y);
|
|
9659
|
+
const [sw, sh] = roundXY(iconW, iconH);
|
|
9660
|
+
const viewBox = `${String(sprite.x)} ${String(sprite.y)} ${String(sprite.width)} ${String(sprite.height)}`;
|
|
9661
|
+
const attrs = [
|
|
9662
|
+
`x="${formatNum(sx)}"`,
|
|
9663
|
+
`y="${formatNum(sy)}"`,
|
|
9664
|
+
`width="${formatNum(sw)}"`,
|
|
9665
|
+
`height="${formatNum(sh)}"`,
|
|
9666
|
+
];
|
|
9667
|
+
if (style.opacity < 1)
|
|
9668
|
+
attrs.push(`opacity="${style.opacity.toFixed(3)}"`);
|
|
9669
|
+
if (style.rotate !== 0) {
|
|
9670
|
+
const [cx, cy] = roundXY(point.x + style.offset[0] * style.size, point.y + style.offset[1] * style.size);
|
|
9671
|
+
attrs.push(`transform="rotate(${String(style.rotate)},${formatNum(cx)},${formatNum(cy)})"`);
|
|
9672
|
+
}
|
|
9673
|
+
this.#svg.push(`<svg ${attrs.join(' ')} viewBox="${viewBox}" xmlns="http://www.w3.org/2000/svg">` +
|
|
9674
|
+
`<image width="${String(sprite.sheetWidth)}" height="${String(sprite.sheetHeight)}" href="${sprite.sheetDataUri}" />` +
|
|
9675
|
+
`</svg>`);
|
|
9676
|
+
}
|
|
9677
|
+
this.#svg.push('</g>');
|
|
9678
|
+
}
|
|
9679
|
+
drawRasterTiles(id, tiles, style) {
|
|
9499
9680
|
if (tiles.length === 0)
|
|
9500
9681
|
return;
|
|
9501
9682
|
if (style.opacity <= 0)
|
|
@@ -9511,7 +9692,7 @@ class SVGRenderer {
|
|
|
9511
9692
|
const brightness = (style.brightnessMin + style.brightnessMax) / 2;
|
|
9512
9693
|
filters.push(`brightness(${String(brightness)})`);
|
|
9513
9694
|
}
|
|
9514
|
-
let gAttrs = `opacity="${String(style.opacity)}"`;
|
|
9695
|
+
let gAttrs = `id="${escapeXml(id)}" opacity="${String(style.opacity)}"`;
|
|
9515
9696
|
if (filters.length > 0)
|
|
9516
9697
|
gAttrs += ` filter="${filters.join(' ')}"`;
|
|
9517
9698
|
this.#svg.push(`<g ${gAttrs}>`);
|
|
@@ -9562,6 +9743,57 @@ function formatPoint(p) {
|
|
|
9562
9743
|
const [x, y] = roundXY(p[0], p[1]);
|
|
9563
9744
|
return formatNum(x) + ',' + formatNum(y);
|
|
9564
9745
|
}
|
|
9746
|
+
function mapTextAnchor(anchor) {
|
|
9747
|
+
switch (anchor) {
|
|
9748
|
+
case 'left':
|
|
9749
|
+
return ['start', 'central'];
|
|
9750
|
+
case 'right':
|
|
9751
|
+
return ['end', 'central'];
|
|
9752
|
+
case 'top':
|
|
9753
|
+
return ['middle', 'text-before-edge'];
|
|
9754
|
+
case 'bottom':
|
|
9755
|
+
return ['middle', 'text-after-edge'];
|
|
9756
|
+
case 'top-left':
|
|
9757
|
+
return ['start', 'text-before-edge'];
|
|
9758
|
+
case 'top-right':
|
|
9759
|
+
return ['end', 'text-before-edge'];
|
|
9760
|
+
case 'bottom-left':
|
|
9761
|
+
return ['start', 'text-after-edge'];
|
|
9762
|
+
case 'bottom-right':
|
|
9763
|
+
return ['end', 'text-after-edge'];
|
|
9764
|
+
default:
|
|
9765
|
+
return ['middle', 'central'];
|
|
9766
|
+
}
|
|
9767
|
+
}
|
|
9768
|
+
function mapIconAnchor(anchor, w, h) {
|
|
9769
|
+
switch (anchor) {
|
|
9770
|
+
case 'left':
|
|
9771
|
+
return [0, -h / 2];
|
|
9772
|
+
case 'right':
|
|
9773
|
+
return [-w, -h / 2];
|
|
9774
|
+
case 'top':
|
|
9775
|
+
return [-w / 2, 0];
|
|
9776
|
+
case 'bottom':
|
|
9777
|
+
return [-w / 2, -h];
|
|
9778
|
+
case 'top-left':
|
|
9779
|
+
return [0, 0];
|
|
9780
|
+
case 'top-right':
|
|
9781
|
+
return [-w, 0];
|
|
9782
|
+
case 'bottom-left':
|
|
9783
|
+
return [0, -h];
|
|
9784
|
+
case 'bottom-right':
|
|
9785
|
+
return [-w, -h];
|
|
9786
|
+
default:
|
|
9787
|
+
return [-w / 2, -h / 2];
|
|
9788
|
+
}
|
|
9789
|
+
}
|
|
9790
|
+
function escapeXml(s) {
|
|
9791
|
+
return s
|
|
9792
|
+
.replace(/&/g, '&')
|
|
9793
|
+
.replace(/</g, '<')
|
|
9794
|
+
.replace(/>/g, '>')
|
|
9795
|
+
.replace(/"/g, '"');
|
|
9796
|
+
}
|
|
9565
9797
|
|
|
9566
9798
|
/*
|
|
9567
9799
|
* bignumber.js v9.3.1
|
|
@@ -16110,6 +16342,65 @@ async function getRasterTiles(job, sourceName) {
|
|
|
16110
16342
|
return rasterTiles.filter((tile) => tile !== null);
|
|
16111
16343
|
}
|
|
16112
16344
|
|
|
16345
|
+
async function loadSpriteAtlas(style) {
|
|
16346
|
+
const atlas = new Map();
|
|
16347
|
+
const sprite = style.sprite;
|
|
16348
|
+
if (!sprite)
|
|
16349
|
+
return atlas;
|
|
16350
|
+
const sources = [];
|
|
16351
|
+
if (typeof sprite === 'string') {
|
|
16352
|
+
sources.push({ id: 'default', url: sprite });
|
|
16353
|
+
}
|
|
16354
|
+
else if (Array.isArray(sprite)) {
|
|
16355
|
+
for (const s of sprite) {
|
|
16356
|
+
sources.push({
|
|
16357
|
+
id: s.id,
|
|
16358
|
+
url: s.url,
|
|
16359
|
+
});
|
|
16360
|
+
}
|
|
16361
|
+
}
|
|
16362
|
+
await Promise.all(sources.map(async ({ id, url }) => {
|
|
16363
|
+
try {
|
|
16364
|
+
const [jsonResponse, imageResponse] = await Promise.all([
|
|
16365
|
+
fetch(`${url}.json`),
|
|
16366
|
+
fetch(`${url}.png`),
|
|
16367
|
+
]);
|
|
16368
|
+
if (!jsonResponse.ok || !imageResponse.ok)
|
|
16369
|
+
return;
|
|
16370
|
+
const json = (await jsonResponse.json());
|
|
16371
|
+
const imageBuffer = await imageResponse.arrayBuffer();
|
|
16372
|
+
const base64 = typeof Buffer !== 'undefined'
|
|
16373
|
+
? Buffer.from(imageBuffer).toString('base64')
|
|
16374
|
+
: btoa(String.fromCharCode(...new Uint8Array(imageBuffer)));
|
|
16375
|
+
const sheetDataUri = `data:image/png;base64,${base64}`;
|
|
16376
|
+
// Estimate sheet dimensions from sprite entries
|
|
16377
|
+
let sheetWidth = 0;
|
|
16378
|
+
let sheetHeight = 0;
|
|
16379
|
+
for (const entry of Object.values(json)) {
|
|
16380
|
+
sheetWidth = Math.max(sheetWidth, entry.x + entry.width);
|
|
16381
|
+
sheetHeight = Math.max(sheetHeight, entry.y + entry.height);
|
|
16382
|
+
}
|
|
16383
|
+
const prefix = id === 'default' ? '' : `${id}:`;
|
|
16384
|
+
for (const [name, entry] of Object.entries(json)) {
|
|
16385
|
+
atlas.set(`${prefix}${name}`, {
|
|
16386
|
+
width: entry.width,
|
|
16387
|
+
height: entry.height,
|
|
16388
|
+
x: entry.x,
|
|
16389
|
+
y: entry.y,
|
|
16390
|
+
pixelRatio: entry.pixelRatio ?? 1,
|
|
16391
|
+
sheetDataUri,
|
|
16392
|
+
sheetWidth,
|
|
16393
|
+
sheetHeight,
|
|
16394
|
+
});
|
|
16395
|
+
}
|
|
16396
|
+
}
|
|
16397
|
+
catch {
|
|
16398
|
+
// Silently skip failed sprite loads
|
|
16399
|
+
}
|
|
16400
|
+
}));
|
|
16401
|
+
return atlas;
|
|
16402
|
+
}
|
|
16403
|
+
|
|
16113
16404
|
async function getLayerFeatures(job) {
|
|
16114
16405
|
const { width, height } = job.renderer;
|
|
16115
16406
|
const { zoom, center } = job.view;
|
|
@@ -16270,6 +16561,12 @@ function getLayerStyles(layers) {
|
|
|
16270
16561
|
return layers.map(createStyleLayer);
|
|
16271
16562
|
}
|
|
16272
16563
|
|
|
16564
|
+
function resolveTokens(text, properties) {
|
|
16565
|
+
return text.replace(/\{([^}]+)\}/g, (_, key) => {
|
|
16566
|
+
const value = properties[key];
|
|
16567
|
+
return value != null ? String(value) : '';
|
|
16568
|
+
});
|
|
16569
|
+
}
|
|
16273
16570
|
async function renderMap(job) {
|
|
16274
16571
|
await render(job);
|
|
16275
16572
|
return job.renderer.getString();
|
|
@@ -16280,9 +16577,12 @@ function getFeatures(layerFeatures, layerStyle) {
|
|
|
16280
16577
|
async function render(job) {
|
|
16281
16578
|
const { renderer } = job;
|
|
16282
16579
|
const { zoom } = job.view;
|
|
16283
|
-
const layerFeatures = await
|
|
16580
|
+
const [layerFeatures, spriteAtlas] = await Promise.all([
|
|
16581
|
+
getLayerFeatures(job),
|
|
16582
|
+
job.renderLabels ? loadSpriteAtlas(job.style) : Promise.resolve(new Map()),
|
|
16583
|
+
]);
|
|
16284
16584
|
const layerStyles = getLayerStyles(job.style.layers);
|
|
16285
|
-
const availableImages = [];
|
|
16585
|
+
const availableImages = [...spriteAtlas.keys()];
|
|
16286
16586
|
const featureState = {};
|
|
16287
16587
|
for (const layerStyle of layerStyles) {
|
|
16288
16588
|
if (layerStyle.isHidden(zoom))
|
|
@@ -16303,6 +16603,7 @@ async function render(job) {
|
|
|
16303
16603
|
function getLayout(key, feature) {
|
|
16304
16604
|
return getStyleValue(layerStyle.layout, key, feature);
|
|
16305
16605
|
}
|
|
16606
|
+
const layerId = layerStyle.id;
|
|
16306
16607
|
switch (layerStyle.type) {
|
|
16307
16608
|
case 'background':
|
|
16308
16609
|
{
|
|
@@ -16322,7 +16623,7 @@ async function render(job) {
|
|
|
16322
16623
|
: polygons;
|
|
16323
16624
|
if (polygonFeatures.length === 0)
|
|
16324
16625
|
continue;
|
|
16325
|
-
renderer.drawPolygons(polygonFeatures.map((feature) => [
|
|
16626
|
+
renderer.drawPolygons(layerId, polygonFeatures.map((feature) => [
|
|
16326
16627
|
feature,
|
|
16327
16628
|
{
|
|
16328
16629
|
color: getPaint('fill-color', feature),
|
|
@@ -16342,7 +16643,7 @@ async function render(job) {
|
|
|
16342
16643
|
: lineStrings;
|
|
16343
16644
|
if (lineStringFeatures.length === 0)
|
|
16344
16645
|
continue;
|
|
16345
|
-
renderer.drawLineStrings(lineStringFeatures.map((feature) => [
|
|
16646
|
+
renderer.drawLineStrings(layerId, lineStringFeatures.map((feature) => [
|
|
16346
16647
|
feature,
|
|
16347
16648
|
{
|
|
16348
16649
|
color: getPaint('line-color', feature),
|
|
@@ -16361,7 +16662,7 @@ async function render(job) {
|
|
|
16361
16662
|
case 'raster':
|
|
16362
16663
|
{
|
|
16363
16664
|
const tiles = await getRasterTiles(job, layerStyle.source);
|
|
16364
|
-
renderer.drawRasterTiles(tiles, {
|
|
16665
|
+
renderer.drawRasterTiles(layerId, tiles, {
|
|
16365
16666
|
opacity: getPaint('raster-opacity'),
|
|
16366
16667
|
hueRotate: getPaint('raster-hue-rotate'),
|
|
16367
16668
|
brightnessMin: getPaint('raster-brightness-min'),
|
|
@@ -16382,7 +16683,7 @@ async function render(job) {
|
|
|
16382
16683
|
: points;
|
|
16383
16684
|
if (pointFeatures.length === 0)
|
|
16384
16685
|
continue;
|
|
16385
|
-
renderer.drawCircles(pointFeatures.map((feature) => [
|
|
16686
|
+
renderer.drawCircles(layerId, pointFeatures.map((feature) => [
|
|
16386
16687
|
feature,
|
|
16387
16688
|
{
|
|
16388
16689
|
color: getPaint('circle-color', feature),
|
|
@@ -16395,11 +16696,76 @@ async function render(job) {
|
|
|
16395
16696
|
]));
|
|
16396
16697
|
}
|
|
16397
16698
|
continue;
|
|
16699
|
+
case 'symbol':
|
|
16700
|
+
{
|
|
16701
|
+
if (!job.renderLabels)
|
|
16702
|
+
continue;
|
|
16703
|
+
const features = getFeatures(layerFeatures, layerStyle);
|
|
16704
|
+
const allFeatures = [
|
|
16705
|
+
...(features?.points ?? []),
|
|
16706
|
+
...(features?.linestrings ?? []),
|
|
16707
|
+
...(features?.polygons ?? []),
|
|
16708
|
+
];
|
|
16709
|
+
if (allFeatures.length === 0)
|
|
16710
|
+
continue;
|
|
16711
|
+
const symbolFeatures = layerStyle.filterFn
|
|
16712
|
+
? allFeatures.filter((feature) => layerStyle.filterFn.filter({ zoom }, feature))
|
|
16713
|
+
: allFeatures;
|
|
16714
|
+
if (symbolFeatures.length === 0)
|
|
16715
|
+
continue;
|
|
16716
|
+
// Render icons first (underneath text)
|
|
16717
|
+
renderer.drawIcons(`${layerId}-icons`, symbolFeatures.flatMap((feature) => {
|
|
16718
|
+
const iconImage = getLayout('icon-image', feature);
|
|
16719
|
+
const iconName = iconImage != null
|
|
16720
|
+
? resolveTokens(iconImage.toString(), feature.properties)
|
|
16721
|
+
: '';
|
|
16722
|
+
if (!iconName || !spriteAtlas.has(iconName))
|
|
16723
|
+
return [];
|
|
16724
|
+
return [
|
|
16725
|
+
[
|
|
16726
|
+
feature,
|
|
16727
|
+
{
|
|
16728
|
+
image: iconName,
|
|
16729
|
+
size: getLayout('icon-size', feature),
|
|
16730
|
+
anchor: getLayout('icon-anchor', feature),
|
|
16731
|
+
offset: getLayout('icon-offset', feature),
|
|
16732
|
+
rotate: getLayout('icon-rotate', feature),
|
|
16733
|
+
opacity: getPaint('icon-opacity', feature),
|
|
16734
|
+
},
|
|
16735
|
+
],
|
|
16736
|
+
];
|
|
16737
|
+
}), spriteAtlas);
|
|
16738
|
+
// Render text labels on top
|
|
16739
|
+
renderer.drawLabels(`${layerId}-labels`, symbolFeatures.flatMap((feature) => {
|
|
16740
|
+
const textField = getLayout('text-field', feature);
|
|
16741
|
+
const textRaw = textField != null ? textField.toString() : '';
|
|
16742
|
+
const text = resolveTokens(textRaw, feature.properties);
|
|
16743
|
+
if (!text)
|
|
16744
|
+
return [];
|
|
16745
|
+
return [
|
|
16746
|
+
[
|
|
16747
|
+
feature,
|
|
16748
|
+
{
|
|
16749
|
+
text,
|
|
16750
|
+
size: getLayout('text-size', feature),
|
|
16751
|
+
font: getLayout('text-font', feature),
|
|
16752
|
+
anchor: getLayout('text-anchor', feature),
|
|
16753
|
+
offset: getLayout('text-offset', feature),
|
|
16754
|
+
rotate: getLayout('text-rotate', feature),
|
|
16755
|
+
color: getPaint('text-color', feature),
|
|
16756
|
+
opacity: getPaint('text-opacity', feature),
|
|
16757
|
+
haloColor: getPaint('text-halo-color', feature),
|
|
16758
|
+
haloWidth: getPaint('text-halo-width', feature),
|
|
16759
|
+
},
|
|
16760
|
+
],
|
|
16761
|
+
];
|
|
16762
|
+
}));
|
|
16763
|
+
}
|
|
16764
|
+
continue;
|
|
16398
16765
|
case 'color-relief':
|
|
16399
16766
|
case 'fill-extrusion':
|
|
16400
16767
|
case 'heatmap':
|
|
16401
16768
|
case 'hillshade':
|
|
16402
|
-
case 'symbol':
|
|
16403
16769
|
continue;
|
|
16404
16770
|
default:
|
|
16405
16771
|
throw Error('layerStyle.type: ' + String(layerStyle.type));
|
|
@@ -16421,6 +16787,7 @@ async function renderToSVG(options) {
|
|
|
16421
16787
|
center: [options.lon ?? 0, options.lat ?? 0],
|
|
16422
16788
|
zoom: options.zoom ?? 2,
|
|
16423
16789
|
},
|
|
16790
|
+
renderLabels: options.renderLabels ?? false,
|
|
16424
16791
|
});
|
|
16425
16792
|
}
|
|
16426
16793
|
|
|
@@ -16516,11 +16883,14 @@ class SVGExportControl {
|
|
|
16516
16883
|
<div class="panel-notice">
|
|
16517
16884
|
Note:<br>
|
|
16518
16885
|
<span class="panel-attribution"></span><br>
|
|
16519
|
-
|
|
16886
|
+
Text labels are rendered without collision detection, so labels may overlap. You can improve me on <a href="https://github.com/versatiles-org/versatiles-svg-renderer" target="_blank" rel="noopener noreferrer">GitHub</a>.<br>
|
|
16520
16887
|
</div>
|
|
16521
16888
|
<div class="panel-inputs">
|
|
16522
|
-
<
|
|
16523
|
-
|
|
16889
|
+
<div class="grid">
|
|
16890
|
+
<label>Width<input type="number" class="input-width" value="${String(this.options.defaultWidth)}" min="1" max="8192"></label>
|
|
16891
|
+
<label>Height<input type="number" class="input-height" value="${String(this.options.defaultHeight)}" min="1" max="8192"></label>
|
|
16892
|
+
</div>
|
|
16893
|
+
<label class="label-checkbox"><input type="checkbox" class="input-labels"> Include labels and icons (buggy)</label>
|
|
16524
16894
|
</div>
|
|
16525
16895
|
<div class="preview-container">
|
|
16526
16896
|
<span class="preview-loading">Rendering preview\u2026</span>
|
|
@@ -16553,6 +16923,9 @@ class SVGExportControl {
|
|
|
16553
16923
|
input.addEventListener('input', () => {
|
|
16554
16924
|
this.schedulePreview();
|
|
16555
16925
|
});
|
|
16926
|
+
input.addEventListener('change', () => {
|
|
16927
|
+
this.schedulePreview();
|
|
16928
|
+
});
|
|
16556
16929
|
});
|
|
16557
16930
|
querySelector(this.panel, '.btn-download').addEventListener('click', () => {
|
|
16558
16931
|
this.downloadSVG();
|
|
@@ -16618,6 +16991,7 @@ class SVGExportControl {
|
|
|
16618
16991
|
openBtn.disabled = true;
|
|
16619
16992
|
const width = Number(querySelector(panel, '.input-width').value);
|
|
16620
16993
|
const height = Number(querySelector(panel, '.input-height').value);
|
|
16994
|
+
const renderLabels = querySelector(panel, '.input-labels').checked;
|
|
16621
16995
|
if (!width || !height || width < 1 || height < 1) {
|
|
16622
16996
|
previewContainer.innerHTML = '<span class="preview-loading">Invalid input values</span>';
|
|
16623
16997
|
return;
|
|
@@ -16633,6 +17007,7 @@ class SVGExportControl {
|
|
|
16633
17007
|
lon: center.lng,
|
|
16634
17008
|
lat: center.lat,
|
|
16635
17009
|
zoom,
|
|
17010
|
+
renderLabels,
|
|
16636
17011
|
});
|
|
16637
17012
|
if (this.renderGeneration !== generation)
|
|
16638
17013
|
return;
|