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