@versatiles/svg-renderer 0.5.1 → 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 +393 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +393 -41
- package/dist/index.js.map +1 -1
- package/dist/maplibre.cjs +416 -53
- package/dist/maplibre.cjs.map +1 -1
- package/dist/maplibre.d.ts +1 -2
- package/dist/maplibre.js +416 -53
- package/dist/maplibre.js.map +1 -1
- package/dist/maplibre.umd.js +416 -53
- package/dist/maplibre.umd.js.map +1 -1
- package/package.json +2 -2
package/dist/maplibre.js
CHANGED
|
@@ -63,18 +63,26 @@ 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
|
-
grid-template-columns: 1fr 1fr
|
|
71
|
+
grid-template-columns: 1fr 1fr;
|
|
68
72
|
gap: 8px;
|
|
69
73
|
margin-bottom: 12px;
|
|
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],
|
|
@@ -9359,13 +9441,11 @@ class SVGRenderer {
|
|
|
9359
9441
|
width;
|
|
9360
9442
|
height;
|
|
9361
9443
|
#svg;
|
|
9362
|
-
#scale;
|
|
9363
9444
|
#backgroundColor;
|
|
9364
9445
|
constructor(opt) {
|
|
9365
9446
|
this.width = opt.width;
|
|
9366
9447
|
this.height = opt.height;
|
|
9367
9448
|
this.#svg = [];
|
|
9368
|
-
this.#scale = opt.scale;
|
|
9369
9449
|
this.#backgroundColor = Color.transparent;
|
|
9370
9450
|
}
|
|
9371
9451
|
drawBackgroundFill(style) {
|
|
@@ -9373,7 +9453,7 @@ class SVGRenderer {
|
|
|
9373
9453
|
color.alpha *= style.opacity;
|
|
9374
9454
|
this.#backgroundColor = color;
|
|
9375
9455
|
}
|
|
9376
|
-
drawPolygons(features) {
|
|
9456
|
+
drawPolygons(id, features) {
|
|
9377
9457
|
if (features.length === 0)
|
|
9378
9458
|
return;
|
|
9379
9459
|
const groups = new Map();
|
|
@@ -9385,7 +9465,7 @@ class SVGRenderer {
|
|
|
9385
9465
|
return;
|
|
9386
9466
|
const translate = style.translate[0] === 0 && style.translate[1] === 0
|
|
9387
9467
|
? ''
|
|
9388
|
-
: ` transform="translate(${formatPoint(style.translate
|
|
9468
|
+
: ` transform="translate(${formatPoint(style.translate)})"`;
|
|
9389
9469
|
const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
|
|
9390
9470
|
const key = color.hex + translate + opacityAttr;
|
|
9391
9471
|
let group = groups.get(key);
|
|
@@ -9394,15 +9474,17 @@ class SVGRenderer {
|
|
|
9394
9474
|
groups.set(key, group);
|
|
9395
9475
|
}
|
|
9396
9476
|
feature.geometry.forEach((ring) => {
|
|
9397
|
-
group.segments.push(ring.map((p) => roundXY(p.x, p.y
|
|
9477
|
+
group.segments.push(ring.map((p) => roundXY(p.x, p.y)));
|
|
9398
9478
|
});
|
|
9399
9479
|
});
|
|
9480
|
+
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
9400
9481
|
for (const { segments, attrs } of groups.values()) {
|
|
9401
9482
|
const d = segmentsToPath(segments, true);
|
|
9402
9483
|
this.#svg.push(`<path d="${d}" ${attrs} />`);
|
|
9403
9484
|
}
|
|
9485
|
+
this.#svg.push('</g>');
|
|
9404
9486
|
}
|
|
9405
|
-
drawLineStrings(features) {
|
|
9487
|
+
drawLineStrings(id, features) {
|
|
9406
9488
|
if (features.length === 0)
|
|
9407
9489
|
return;
|
|
9408
9490
|
const groups = new Map();
|
|
@@ -9414,10 +9496,10 @@ class SVGRenderer {
|
|
|
9414
9496
|
return;
|
|
9415
9497
|
const translate = style.translate[0] === 0 && style.translate[1] === 0
|
|
9416
9498
|
? ''
|
|
9417
|
-
: ` transform="translate(${formatPoint(style.translate
|
|
9418
|
-
const roundedWidth = formatScaled(style.width
|
|
9499
|
+
: ` transform="translate(${formatPoint(style.translate)})"`;
|
|
9500
|
+
const roundedWidth = formatScaled(style.width);
|
|
9419
9501
|
const dasharrayStr = style.dasharray
|
|
9420
|
-
? style.dasharray.map((v) => formatScaled(v * style.width
|
|
9502
|
+
? style.dasharray.map((v) => formatScaled(v * style.width)).join(',')
|
|
9421
9503
|
: '';
|
|
9422
9504
|
const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
|
|
9423
9505
|
const key = [
|
|
@@ -9448,16 +9530,18 @@ class SVGRenderer {
|
|
|
9448
9530
|
groups.set(key, group);
|
|
9449
9531
|
}
|
|
9450
9532
|
feature.geometry.forEach((line) => {
|
|
9451
|
-
group.segments.push(line.map((p) => roundXY(p.x, p.y
|
|
9533
|
+
group.segments.push(line.map((p) => roundXY(p.x, p.y)));
|
|
9452
9534
|
});
|
|
9453
9535
|
});
|
|
9536
|
+
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
9454
9537
|
for (const { segments, attrs } of groups.values()) {
|
|
9455
9538
|
const chains = chainSegments(segments);
|
|
9456
9539
|
const d = segmentsToPath(chains);
|
|
9457
9540
|
this.#svg.push(`<path d="${d}" ${attrs} />`);
|
|
9458
9541
|
}
|
|
9542
|
+
this.#svg.push('</g>');
|
|
9459
9543
|
}
|
|
9460
|
-
drawCircles(features) {
|
|
9544
|
+
drawCircles(id, features) {
|
|
9461
9545
|
if (features.length === 0)
|
|
9462
9546
|
return;
|
|
9463
9547
|
const groups = new Map();
|
|
@@ -9469,12 +9553,10 @@ class SVGRenderer {
|
|
|
9469
9553
|
return;
|
|
9470
9554
|
const translate = style.translate[0] === 0 && style.translate[1] === 0
|
|
9471
9555
|
? ''
|
|
9472
|
-
: ` transform="translate(${formatPoint(style.translate
|
|
9473
|
-
const roundedRadius = formatScaled(style.radius
|
|
9556
|
+
: ` transform="translate(${formatPoint(style.translate)})"`;
|
|
9557
|
+
const roundedRadius = formatScaled(style.radius);
|
|
9474
9558
|
const strokeColor = new Color(style.strokeColor);
|
|
9475
|
-
const strokeAttrs = style.strokeWidth > 0
|
|
9476
|
-
? ` ${strokeAttr(strokeColor, formatScaled(style.strokeWidth, this.#scale))}`
|
|
9477
|
-
: '';
|
|
9559
|
+
const strokeAttrs = style.strokeWidth > 0 ? ` ${strokeAttr(strokeColor, formatScaled(style.strokeWidth))}` : '';
|
|
9478
9560
|
const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
|
|
9479
9561
|
const key = [color.hex, roundedRadius, strokeAttrs, opacityAttr, translate].join('\0');
|
|
9480
9562
|
let group = groups.get(key);
|
|
@@ -9488,16 +9570,111 @@ class SVGRenderer {
|
|
|
9488
9570
|
feature.geometry.forEach((ring) => {
|
|
9489
9571
|
const p = ring[0];
|
|
9490
9572
|
if (p)
|
|
9491
|
-
group.points.push(roundXY(p.x, p.y
|
|
9573
|
+
group.points.push(roundXY(p.x, p.y));
|
|
9492
9574
|
});
|
|
9493
9575
|
});
|
|
9576
|
+
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
9494
9577
|
for (const { points, attrs } of groups.values()) {
|
|
9495
9578
|
for (const [x, y] of points) {
|
|
9496
9579
|
this.#svg.push(`<circle cx="${formatNum(x)}" cy="${formatNum(y)}" ${attrs} />`);
|
|
9497
9580
|
}
|
|
9498
9581
|
}
|
|
9582
|
+
this.#svg.push('</g>');
|
|
9583
|
+
}
|
|
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>');
|
|
9499
9676
|
}
|
|
9500
|
-
drawRasterTiles(tiles, style) {
|
|
9677
|
+
drawRasterTiles(id, tiles, style) {
|
|
9501
9678
|
if (tiles.length === 0)
|
|
9502
9679
|
return;
|
|
9503
9680
|
if (style.opacity <= 0)
|
|
@@ -9513,15 +9690,14 @@ class SVGRenderer {
|
|
|
9513
9690
|
const brightness = (style.brightnessMin + style.brightnessMax) / 2;
|
|
9514
9691
|
filters.push(`brightness(${String(brightness)})`);
|
|
9515
9692
|
}
|
|
9516
|
-
let gAttrs = `opacity="${String(style.opacity)}"`;
|
|
9693
|
+
let gAttrs = `id="${escapeXml(id)}" opacity="${String(style.opacity)}"`;
|
|
9517
9694
|
if (filters.length > 0)
|
|
9518
9695
|
gAttrs += ` filter="${filters.join(' ')}"`;
|
|
9519
9696
|
this.#svg.push(`<g ${gAttrs}>`);
|
|
9520
9697
|
const pixelated = style.resampling === 'nearest';
|
|
9521
9698
|
for (const tile of tiles) {
|
|
9522
9699
|
const overlap = Math.min(tile.width, tile.height) / 10000; // slight overlap to prevent sub-pixel gaps between tiles
|
|
9523
|
-
|
|
9524
|
-
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}"`;
|
|
9700
|
+
let attrs = `x="${formatScaled(tile.x - overlap)}" y="${formatScaled(tile.y - overlap)}" width="${formatScaled(tile.width + overlap * 2)}" height="${formatScaled(tile.height + overlap * 2)}" href="${tile.dataUri}"`;
|
|
9525
9701
|
if (pixelated)
|
|
9526
9702
|
attrs += ' style="image-rendering:pixelated"';
|
|
9527
9703
|
this.#svg.push(`<image ${attrs} />`);
|
|
@@ -9555,16 +9731,67 @@ function strokeAttr(color, width) {
|
|
|
9555
9731
|
attr += ` stroke-opacity="${color.opacity.toFixed(3)}"`;
|
|
9556
9732
|
return attr;
|
|
9557
9733
|
}
|
|
9558
|
-
function formatScaled(v
|
|
9559
|
-
return formatNum(Math.round(v *
|
|
9734
|
+
function formatScaled(v) {
|
|
9735
|
+
return formatNum(Math.round(v * 10));
|
|
9560
9736
|
}
|
|
9561
|
-
function roundXY(x, y
|
|
9562
|
-
return [Math.round(x *
|
|
9737
|
+
function roundXY(x, y) {
|
|
9738
|
+
return [Math.round(x * 10), Math.round(y * 10)];
|
|
9563
9739
|
}
|
|
9564
|
-
function formatPoint(p
|
|
9565
|
-
const [x, y] = roundXY(p[0], p[1]
|
|
9740
|
+
function formatPoint(p) {
|
|
9741
|
+
const [x, y] = roundXY(p[0], p[1]);
|
|
9566
9742
|
return formatNum(x) + ',' + formatNum(y);
|
|
9567
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
|
+
}
|
|
9568
9795
|
|
|
9569
9796
|
/*
|
|
9570
9797
|
* bignumber.js v9.3.1
|
|
@@ -16113,6 +16340,65 @@ async function getRasterTiles(job, sourceName) {
|
|
|
16113
16340
|
return rasterTiles.filter((tile) => tile !== null);
|
|
16114
16341
|
}
|
|
16115
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
|
+
|
|
16116
16402
|
async function getLayerFeatures(job) {
|
|
16117
16403
|
const { width, height } = job.renderer;
|
|
16118
16404
|
const { zoom, center } = job.view;
|
|
@@ -16273,6 +16559,12 @@ function getLayerStyles(layers) {
|
|
|
16273
16559
|
return layers.map(createStyleLayer);
|
|
16274
16560
|
}
|
|
16275
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
|
+
}
|
|
16276
16568
|
async function renderMap(job) {
|
|
16277
16569
|
await render(job);
|
|
16278
16570
|
return job.renderer.getString();
|
|
@@ -16283,9 +16575,12 @@ function getFeatures(layerFeatures, layerStyle) {
|
|
|
16283
16575
|
async function render(job) {
|
|
16284
16576
|
const { renderer } = job;
|
|
16285
16577
|
const { zoom } = job.view;
|
|
16286
|
-
const layerFeatures = await
|
|
16578
|
+
const [layerFeatures, spriteAtlas] = await Promise.all([
|
|
16579
|
+
getLayerFeatures(job),
|
|
16580
|
+
job.renderLabels ? loadSpriteAtlas(job.style) : Promise.resolve(new Map()),
|
|
16581
|
+
]);
|
|
16287
16582
|
const layerStyles = getLayerStyles(job.style.layers);
|
|
16288
|
-
const availableImages = [];
|
|
16583
|
+
const availableImages = [...spriteAtlas.keys()];
|
|
16289
16584
|
const featureState = {};
|
|
16290
16585
|
for (const layerStyle of layerStyles) {
|
|
16291
16586
|
if (layerStyle.isHidden(zoom))
|
|
@@ -16306,6 +16601,7 @@ async function render(job) {
|
|
|
16306
16601
|
function getLayout(key, feature) {
|
|
16307
16602
|
return getStyleValue(layerStyle.layout, key, feature);
|
|
16308
16603
|
}
|
|
16604
|
+
const layerId = layerStyle.id;
|
|
16309
16605
|
switch (layerStyle.type) {
|
|
16310
16606
|
case 'background':
|
|
16311
16607
|
{
|
|
@@ -16325,7 +16621,7 @@ async function render(job) {
|
|
|
16325
16621
|
: polygons;
|
|
16326
16622
|
if (polygonFeatures.length === 0)
|
|
16327
16623
|
continue;
|
|
16328
|
-
renderer.drawPolygons(polygonFeatures.map((feature) => [
|
|
16624
|
+
renderer.drawPolygons(layerId, polygonFeatures.map((feature) => [
|
|
16329
16625
|
feature,
|
|
16330
16626
|
{
|
|
16331
16627
|
color: getPaint('fill-color', feature),
|
|
@@ -16345,7 +16641,7 @@ async function render(job) {
|
|
|
16345
16641
|
: lineStrings;
|
|
16346
16642
|
if (lineStringFeatures.length === 0)
|
|
16347
16643
|
continue;
|
|
16348
|
-
renderer.drawLineStrings(lineStringFeatures.map((feature) => [
|
|
16644
|
+
renderer.drawLineStrings(layerId, lineStringFeatures.map((feature) => [
|
|
16349
16645
|
feature,
|
|
16350
16646
|
{
|
|
16351
16647
|
color: getPaint('line-color', feature),
|
|
@@ -16364,7 +16660,7 @@ async function render(job) {
|
|
|
16364
16660
|
case 'raster':
|
|
16365
16661
|
{
|
|
16366
16662
|
const tiles = await getRasterTiles(job, layerStyle.source);
|
|
16367
|
-
renderer.drawRasterTiles(tiles, {
|
|
16663
|
+
renderer.drawRasterTiles(layerId, tiles, {
|
|
16368
16664
|
opacity: getPaint('raster-opacity'),
|
|
16369
16665
|
hueRotate: getPaint('raster-hue-rotate'),
|
|
16370
16666
|
brightnessMin: getPaint('raster-brightness-min'),
|
|
@@ -16385,7 +16681,7 @@ async function render(job) {
|
|
|
16385
16681
|
: points;
|
|
16386
16682
|
if (pointFeatures.length === 0)
|
|
16387
16683
|
continue;
|
|
16388
|
-
renderer.drawCircles(pointFeatures.map((feature) => [
|
|
16684
|
+
renderer.drawCircles(layerId, pointFeatures.map((feature) => [
|
|
16389
16685
|
feature,
|
|
16390
16686
|
{
|
|
16391
16687
|
color: getPaint('circle-color', feature),
|
|
@@ -16398,11 +16694,76 @@ async function render(job) {
|
|
|
16398
16694
|
]));
|
|
16399
16695
|
}
|
|
16400
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;
|
|
16401
16763
|
case 'color-relief':
|
|
16402
16764
|
case 'fill-extrusion':
|
|
16403
16765
|
case 'heatmap':
|
|
16404
16766
|
case 'hillshade':
|
|
16405
|
-
case 'symbol':
|
|
16406
16767
|
continue;
|
|
16407
16768
|
default:
|
|
16408
16769
|
throw Error('layerStyle.type: ' + String(layerStyle.type));
|
|
@@ -16413,20 +16774,18 @@ async function render(job) {
|
|
|
16413
16774
|
async function renderToSVG(options) {
|
|
16414
16775
|
const width = options.width ?? 1024;
|
|
16415
16776
|
const height = options.height ?? 1024;
|
|
16416
|
-
const scale = options.scale ?? 1;
|
|
16417
16777
|
if (width <= 0)
|
|
16418
16778
|
throw new Error('width must be positive');
|
|
16419
16779
|
if (height <= 0)
|
|
16420
16780
|
throw new Error('height must be positive');
|
|
16421
|
-
if (scale <= 0)
|
|
16422
|
-
throw new Error('scale must be positive');
|
|
16423
16781
|
return await renderMap({
|
|
16424
|
-
renderer: new SVGRenderer({ width, height
|
|
16782
|
+
renderer: new SVGRenderer({ width, height }),
|
|
16425
16783
|
style: options.style,
|
|
16426
16784
|
view: {
|
|
16427
16785
|
center: [options.lon ?? 0, options.lat ?? 0],
|
|
16428
16786
|
zoom: options.zoom ?? 2,
|
|
16429
16787
|
},
|
|
16788
|
+
renderLabels: options.renderLabels ?? false,
|
|
16430
16789
|
});
|
|
16431
16790
|
}
|
|
16432
16791
|
|
|
@@ -16482,7 +16841,6 @@ class SVGExportControl {
|
|
|
16482
16841
|
this.options = {
|
|
16483
16842
|
defaultWidth: options?.defaultWidth ?? 1024,
|
|
16484
16843
|
defaultHeight: options?.defaultHeight ?? 1024,
|
|
16485
|
-
defaultScale: options?.defaultScale ?? 1,
|
|
16486
16844
|
};
|
|
16487
16845
|
}
|
|
16488
16846
|
onAdd(map) {
|
|
@@ -16523,12 +16881,14 @@ class SVGExportControl {
|
|
|
16523
16881
|
<div class="panel-notice">
|
|
16524
16882
|
Note:<br>
|
|
16525
16883
|
<span class="panel-attribution"></span><br>
|
|
16526
|
-
|
|
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>
|
|
16527
16885
|
</div>
|
|
16528
16886
|
<div class="panel-inputs">
|
|
16529
|
-
<
|
|
16530
|
-
|
|
16531
|
-
|
|
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>
|
|
16532
16892
|
</div>
|
|
16533
16893
|
<div class="preview-container">
|
|
16534
16894
|
<span class="preview-loading">Rendering preview\u2026</span>
|
|
@@ -16561,6 +16921,9 @@ class SVGExportControl {
|
|
|
16561
16921
|
input.addEventListener('input', () => {
|
|
16562
16922
|
this.schedulePreview();
|
|
16563
16923
|
});
|
|
16924
|
+
input.addEventListener('change', () => {
|
|
16925
|
+
this.schedulePreview();
|
|
16926
|
+
});
|
|
16564
16927
|
});
|
|
16565
16928
|
querySelector(this.panel, '.btn-download').addEventListener('click', () => {
|
|
16566
16929
|
this.downloadSVG();
|
|
@@ -16626,8 +16989,8 @@ class SVGExportControl {
|
|
|
16626
16989
|
openBtn.disabled = true;
|
|
16627
16990
|
const width = Number(querySelector(panel, '.input-width').value);
|
|
16628
16991
|
const height = Number(querySelector(panel, '.input-height').value);
|
|
16629
|
-
const
|
|
16630
|
-
if (!width || !height ||
|
|
16992
|
+
const renderLabels = querySelector(panel, '.input-labels').checked;
|
|
16993
|
+
if (!width || !height || width < 1 || height < 1) {
|
|
16631
16994
|
previewContainer.innerHTML = '<span class="preview-loading">Invalid input values</span>';
|
|
16632
16995
|
return;
|
|
16633
16996
|
}
|
|
@@ -16638,11 +17001,11 @@ class SVGExportControl {
|
|
|
16638
17001
|
const svg = await renderToSVG({
|
|
16639
17002
|
width,
|
|
16640
17003
|
height,
|
|
16641
|
-
scale,
|
|
16642
17004
|
style,
|
|
16643
17005
|
lon: center.lng,
|
|
16644
17006
|
lat: center.lat,
|
|
16645
17007
|
zoom,
|
|
17008
|
+
renderLabels,
|
|
16646
17009
|
});
|
|
16647
17010
|
if (this.renderGeneration !== generation)
|
|
16648
17011
|
return;
|