@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.cjs
CHANGED
|
@@ -65,18 +65,26 @@ 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
|
-
grid-template-columns: 1fr 1fr
|
|
73
|
+
grid-template-columns: 1fr 1fr;
|
|
70
74
|
gap: 8px;
|
|
71
75
|
margin-bottom: 12px;
|
|
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],
|
|
@@ -9361,13 +9443,11 @@ class SVGRenderer {
|
|
|
9361
9443
|
width;
|
|
9362
9444
|
height;
|
|
9363
9445
|
#svg;
|
|
9364
|
-
#scale;
|
|
9365
9446
|
#backgroundColor;
|
|
9366
9447
|
constructor(opt) {
|
|
9367
9448
|
this.width = opt.width;
|
|
9368
9449
|
this.height = opt.height;
|
|
9369
9450
|
this.#svg = [];
|
|
9370
|
-
this.#scale = opt.scale;
|
|
9371
9451
|
this.#backgroundColor = Color.transparent;
|
|
9372
9452
|
}
|
|
9373
9453
|
drawBackgroundFill(style) {
|
|
@@ -9375,7 +9455,7 @@ class SVGRenderer {
|
|
|
9375
9455
|
color.alpha *= style.opacity;
|
|
9376
9456
|
this.#backgroundColor = color;
|
|
9377
9457
|
}
|
|
9378
|
-
drawPolygons(features) {
|
|
9458
|
+
drawPolygons(id, features) {
|
|
9379
9459
|
if (features.length === 0)
|
|
9380
9460
|
return;
|
|
9381
9461
|
const groups = new Map();
|
|
@@ -9387,7 +9467,7 @@ class SVGRenderer {
|
|
|
9387
9467
|
return;
|
|
9388
9468
|
const translate = style.translate[0] === 0 && style.translate[1] === 0
|
|
9389
9469
|
? ''
|
|
9390
|
-
: ` transform="translate(${formatPoint(style.translate
|
|
9470
|
+
: ` transform="translate(${formatPoint(style.translate)})"`;
|
|
9391
9471
|
const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
|
|
9392
9472
|
const key = color.hex + translate + opacityAttr;
|
|
9393
9473
|
let group = groups.get(key);
|
|
@@ -9396,15 +9476,17 @@ class SVGRenderer {
|
|
|
9396
9476
|
groups.set(key, group);
|
|
9397
9477
|
}
|
|
9398
9478
|
feature.geometry.forEach((ring) => {
|
|
9399
|
-
group.segments.push(ring.map((p) => roundXY(p.x, p.y
|
|
9479
|
+
group.segments.push(ring.map((p) => roundXY(p.x, p.y)));
|
|
9400
9480
|
});
|
|
9401
9481
|
});
|
|
9482
|
+
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
9402
9483
|
for (const { segments, attrs } of groups.values()) {
|
|
9403
9484
|
const d = segmentsToPath(segments, true);
|
|
9404
9485
|
this.#svg.push(`<path d="${d}" ${attrs} />`);
|
|
9405
9486
|
}
|
|
9487
|
+
this.#svg.push('</g>');
|
|
9406
9488
|
}
|
|
9407
|
-
drawLineStrings(features) {
|
|
9489
|
+
drawLineStrings(id, features) {
|
|
9408
9490
|
if (features.length === 0)
|
|
9409
9491
|
return;
|
|
9410
9492
|
const groups = new Map();
|
|
@@ -9416,10 +9498,10 @@ class SVGRenderer {
|
|
|
9416
9498
|
return;
|
|
9417
9499
|
const translate = style.translate[0] === 0 && style.translate[1] === 0
|
|
9418
9500
|
? ''
|
|
9419
|
-
: ` transform="translate(${formatPoint(style.translate
|
|
9420
|
-
const roundedWidth = formatScaled(style.width
|
|
9501
|
+
: ` transform="translate(${formatPoint(style.translate)})"`;
|
|
9502
|
+
const roundedWidth = formatScaled(style.width);
|
|
9421
9503
|
const dasharrayStr = style.dasharray
|
|
9422
|
-
? style.dasharray.map((v) => formatScaled(v * style.width
|
|
9504
|
+
? style.dasharray.map((v) => formatScaled(v * style.width)).join(',')
|
|
9423
9505
|
: '';
|
|
9424
9506
|
const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
|
|
9425
9507
|
const key = [
|
|
@@ -9450,16 +9532,18 @@ class SVGRenderer {
|
|
|
9450
9532
|
groups.set(key, group);
|
|
9451
9533
|
}
|
|
9452
9534
|
feature.geometry.forEach((line) => {
|
|
9453
|
-
group.segments.push(line.map((p) => roundXY(p.x, p.y
|
|
9535
|
+
group.segments.push(line.map((p) => roundXY(p.x, p.y)));
|
|
9454
9536
|
});
|
|
9455
9537
|
});
|
|
9538
|
+
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
9456
9539
|
for (const { segments, attrs } of groups.values()) {
|
|
9457
9540
|
const chains = chainSegments(segments);
|
|
9458
9541
|
const d = segmentsToPath(chains);
|
|
9459
9542
|
this.#svg.push(`<path d="${d}" ${attrs} />`);
|
|
9460
9543
|
}
|
|
9544
|
+
this.#svg.push('</g>');
|
|
9461
9545
|
}
|
|
9462
|
-
drawCircles(features) {
|
|
9546
|
+
drawCircles(id, features) {
|
|
9463
9547
|
if (features.length === 0)
|
|
9464
9548
|
return;
|
|
9465
9549
|
const groups = new Map();
|
|
@@ -9471,12 +9555,10 @@ class SVGRenderer {
|
|
|
9471
9555
|
return;
|
|
9472
9556
|
const translate = style.translate[0] === 0 && style.translate[1] === 0
|
|
9473
9557
|
? ''
|
|
9474
|
-
: ` transform="translate(${formatPoint(style.translate
|
|
9475
|
-
const roundedRadius = formatScaled(style.radius
|
|
9558
|
+
: ` transform="translate(${formatPoint(style.translate)})"`;
|
|
9559
|
+
const roundedRadius = formatScaled(style.radius);
|
|
9476
9560
|
const strokeColor = new Color(style.strokeColor);
|
|
9477
|
-
const strokeAttrs = style.strokeWidth > 0
|
|
9478
|
-
? ` ${strokeAttr(strokeColor, formatScaled(style.strokeWidth, this.#scale))}`
|
|
9479
|
-
: '';
|
|
9561
|
+
const strokeAttrs = style.strokeWidth > 0 ? ` ${strokeAttr(strokeColor, formatScaled(style.strokeWidth))}` : '';
|
|
9480
9562
|
const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
|
|
9481
9563
|
const key = [color.hex, roundedRadius, strokeAttrs, opacityAttr, translate].join('\0');
|
|
9482
9564
|
let group = groups.get(key);
|
|
@@ -9490,16 +9572,111 @@ class SVGRenderer {
|
|
|
9490
9572
|
feature.geometry.forEach((ring) => {
|
|
9491
9573
|
const p = ring[0];
|
|
9492
9574
|
if (p)
|
|
9493
|
-
group.points.push(roundXY(p.x, p.y
|
|
9575
|
+
group.points.push(roundXY(p.x, p.y));
|
|
9494
9576
|
});
|
|
9495
9577
|
});
|
|
9578
|
+
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
9496
9579
|
for (const { points, attrs } of groups.values()) {
|
|
9497
9580
|
for (const [x, y] of points) {
|
|
9498
9581
|
this.#svg.push(`<circle cx="${formatNum(x)}" cy="${formatNum(y)}" ${attrs} />`);
|
|
9499
9582
|
}
|
|
9500
9583
|
}
|
|
9584
|
+
this.#svg.push('</g>');
|
|
9585
|
+
}
|
|
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>');
|
|
9501
9678
|
}
|
|
9502
|
-
drawRasterTiles(tiles, style) {
|
|
9679
|
+
drawRasterTiles(id, tiles, style) {
|
|
9503
9680
|
if (tiles.length === 0)
|
|
9504
9681
|
return;
|
|
9505
9682
|
if (style.opacity <= 0)
|
|
@@ -9515,15 +9692,14 @@ class SVGRenderer {
|
|
|
9515
9692
|
const brightness = (style.brightnessMin + style.brightnessMax) / 2;
|
|
9516
9693
|
filters.push(`brightness(${String(brightness)})`);
|
|
9517
9694
|
}
|
|
9518
|
-
let gAttrs = `opacity="${String(style.opacity)}"`;
|
|
9695
|
+
let gAttrs = `id="${escapeXml(id)}" opacity="${String(style.opacity)}"`;
|
|
9519
9696
|
if (filters.length > 0)
|
|
9520
9697
|
gAttrs += ` filter="${filters.join(' ')}"`;
|
|
9521
9698
|
this.#svg.push(`<g ${gAttrs}>`);
|
|
9522
9699
|
const pixelated = style.resampling === 'nearest';
|
|
9523
9700
|
for (const tile of tiles) {
|
|
9524
9701
|
const overlap = Math.min(tile.width, tile.height) / 10000; // slight overlap to prevent sub-pixel gaps between tiles
|
|
9525
|
-
|
|
9526
|
-
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}"`;
|
|
9702
|
+
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}"`;
|
|
9527
9703
|
if (pixelated)
|
|
9528
9704
|
attrs += ' style="image-rendering:pixelated"';
|
|
9529
9705
|
this.#svg.push(`<image ${attrs} />`);
|
|
@@ -9557,16 +9733,67 @@ function strokeAttr(color, width) {
|
|
|
9557
9733
|
attr += ` stroke-opacity="${color.opacity.toFixed(3)}"`;
|
|
9558
9734
|
return attr;
|
|
9559
9735
|
}
|
|
9560
|
-
function formatScaled(v
|
|
9561
|
-
return formatNum(Math.round(v *
|
|
9736
|
+
function formatScaled(v) {
|
|
9737
|
+
return formatNum(Math.round(v * 10));
|
|
9562
9738
|
}
|
|
9563
|
-
function roundXY(x, y
|
|
9564
|
-
return [Math.round(x *
|
|
9739
|
+
function roundXY(x, y) {
|
|
9740
|
+
return [Math.round(x * 10), Math.round(y * 10)];
|
|
9565
9741
|
}
|
|
9566
|
-
function formatPoint(p
|
|
9567
|
-
const [x, y] = roundXY(p[0], p[1]
|
|
9742
|
+
function formatPoint(p) {
|
|
9743
|
+
const [x, y] = roundXY(p[0], p[1]);
|
|
9568
9744
|
return formatNum(x) + ',' + formatNum(y);
|
|
9569
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
|
+
}
|
|
9570
9797
|
|
|
9571
9798
|
/*
|
|
9572
9799
|
* bignumber.js v9.3.1
|
|
@@ -16115,6 +16342,65 @@ async function getRasterTiles(job, sourceName) {
|
|
|
16115
16342
|
return rasterTiles.filter((tile) => tile !== null);
|
|
16116
16343
|
}
|
|
16117
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
|
+
|
|
16118
16404
|
async function getLayerFeatures(job) {
|
|
16119
16405
|
const { width, height } = job.renderer;
|
|
16120
16406
|
const { zoom, center } = job.view;
|
|
@@ -16275,6 +16561,12 @@ function getLayerStyles(layers) {
|
|
|
16275
16561
|
return layers.map(createStyleLayer);
|
|
16276
16562
|
}
|
|
16277
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
|
+
}
|
|
16278
16570
|
async function renderMap(job) {
|
|
16279
16571
|
await render(job);
|
|
16280
16572
|
return job.renderer.getString();
|
|
@@ -16285,9 +16577,12 @@ function getFeatures(layerFeatures, layerStyle) {
|
|
|
16285
16577
|
async function render(job) {
|
|
16286
16578
|
const { renderer } = job;
|
|
16287
16579
|
const { zoom } = job.view;
|
|
16288
|
-
const layerFeatures = await
|
|
16580
|
+
const [layerFeatures, spriteAtlas] = await Promise.all([
|
|
16581
|
+
getLayerFeatures(job),
|
|
16582
|
+
job.renderLabels ? loadSpriteAtlas(job.style) : Promise.resolve(new Map()),
|
|
16583
|
+
]);
|
|
16289
16584
|
const layerStyles = getLayerStyles(job.style.layers);
|
|
16290
|
-
const availableImages = [];
|
|
16585
|
+
const availableImages = [...spriteAtlas.keys()];
|
|
16291
16586
|
const featureState = {};
|
|
16292
16587
|
for (const layerStyle of layerStyles) {
|
|
16293
16588
|
if (layerStyle.isHidden(zoom))
|
|
@@ -16308,6 +16603,7 @@ async function render(job) {
|
|
|
16308
16603
|
function getLayout(key, feature) {
|
|
16309
16604
|
return getStyleValue(layerStyle.layout, key, feature);
|
|
16310
16605
|
}
|
|
16606
|
+
const layerId = layerStyle.id;
|
|
16311
16607
|
switch (layerStyle.type) {
|
|
16312
16608
|
case 'background':
|
|
16313
16609
|
{
|
|
@@ -16327,7 +16623,7 @@ async function render(job) {
|
|
|
16327
16623
|
: polygons;
|
|
16328
16624
|
if (polygonFeatures.length === 0)
|
|
16329
16625
|
continue;
|
|
16330
|
-
renderer.drawPolygons(polygonFeatures.map((feature) => [
|
|
16626
|
+
renderer.drawPolygons(layerId, polygonFeatures.map((feature) => [
|
|
16331
16627
|
feature,
|
|
16332
16628
|
{
|
|
16333
16629
|
color: getPaint('fill-color', feature),
|
|
@@ -16347,7 +16643,7 @@ async function render(job) {
|
|
|
16347
16643
|
: lineStrings;
|
|
16348
16644
|
if (lineStringFeatures.length === 0)
|
|
16349
16645
|
continue;
|
|
16350
|
-
renderer.drawLineStrings(lineStringFeatures.map((feature) => [
|
|
16646
|
+
renderer.drawLineStrings(layerId, lineStringFeatures.map((feature) => [
|
|
16351
16647
|
feature,
|
|
16352
16648
|
{
|
|
16353
16649
|
color: getPaint('line-color', feature),
|
|
@@ -16366,7 +16662,7 @@ async function render(job) {
|
|
|
16366
16662
|
case 'raster':
|
|
16367
16663
|
{
|
|
16368
16664
|
const tiles = await getRasterTiles(job, layerStyle.source);
|
|
16369
|
-
renderer.drawRasterTiles(tiles, {
|
|
16665
|
+
renderer.drawRasterTiles(layerId, tiles, {
|
|
16370
16666
|
opacity: getPaint('raster-opacity'),
|
|
16371
16667
|
hueRotate: getPaint('raster-hue-rotate'),
|
|
16372
16668
|
brightnessMin: getPaint('raster-brightness-min'),
|
|
@@ -16387,7 +16683,7 @@ async function render(job) {
|
|
|
16387
16683
|
: points;
|
|
16388
16684
|
if (pointFeatures.length === 0)
|
|
16389
16685
|
continue;
|
|
16390
|
-
renderer.drawCircles(pointFeatures.map((feature) => [
|
|
16686
|
+
renderer.drawCircles(layerId, pointFeatures.map((feature) => [
|
|
16391
16687
|
feature,
|
|
16392
16688
|
{
|
|
16393
16689
|
color: getPaint('circle-color', feature),
|
|
@@ -16400,11 +16696,76 @@ async function render(job) {
|
|
|
16400
16696
|
]));
|
|
16401
16697
|
}
|
|
16402
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;
|
|
16403
16765
|
case 'color-relief':
|
|
16404
16766
|
case 'fill-extrusion':
|
|
16405
16767
|
case 'heatmap':
|
|
16406
16768
|
case 'hillshade':
|
|
16407
|
-
case 'symbol':
|
|
16408
16769
|
continue;
|
|
16409
16770
|
default:
|
|
16410
16771
|
throw Error('layerStyle.type: ' + String(layerStyle.type));
|
|
@@ -16415,20 +16776,18 @@ async function render(job) {
|
|
|
16415
16776
|
async function renderToSVG(options) {
|
|
16416
16777
|
const width = options.width ?? 1024;
|
|
16417
16778
|
const height = options.height ?? 1024;
|
|
16418
|
-
const scale = options.scale ?? 1;
|
|
16419
16779
|
if (width <= 0)
|
|
16420
16780
|
throw new Error('width must be positive');
|
|
16421
16781
|
if (height <= 0)
|
|
16422
16782
|
throw new Error('height must be positive');
|
|
16423
|
-
if (scale <= 0)
|
|
16424
|
-
throw new Error('scale must be positive');
|
|
16425
16783
|
return await renderMap({
|
|
16426
|
-
renderer: new SVGRenderer({ width, height
|
|
16784
|
+
renderer: new SVGRenderer({ width, height }),
|
|
16427
16785
|
style: options.style,
|
|
16428
16786
|
view: {
|
|
16429
16787
|
center: [options.lon ?? 0, options.lat ?? 0],
|
|
16430
16788
|
zoom: options.zoom ?? 2,
|
|
16431
16789
|
},
|
|
16790
|
+
renderLabels: options.renderLabels ?? false,
|
|
16432
16791
|
});
|
|
16433
16792
|
}
|
|
16434
16793
|
|
|
@@ -16484,7 +16843,6 @@ class SVGExportControl {
|
|
|
16484
16843
|
this.options = {
|
|
16485
16844
|
defaultWidth: options?.defaultWidth ?? 1024,
|
|
16486
16845
|
defaultHeight: options?.defaultHeight ?? 1024,
|
|
16487
|
-
defaultScale: options?.defaultScale ?? 1,
|
|
16488
16846
|
};
|
|
16489
16847
|
}
|
|
16490
16848
|
onAdd(map) {
|
|
@@ -16525,12 +16883,14 @@ class SVGExportControl {
|
|
|
16525
16883
|
<div class="panel-notice">
|
|
16526
16884
|
Note:<br>
|
|
16527
16885
|
<span class="panel-attribution"></span><br>
|
|
16528
|
-
|
|
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>
|
|
16529
16887
|
</div>
|
|
16530
16888
|
<div class="panel-inputs">
|
|
16531
|
-
<
|
|
16532
|
-
|
|
16533
|
-
|
|
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>
|
|
16534
16894
|
</div>
|
|
16535
16895
|
<div class="preview-container">
|
|
16536
16896
|
<span class="preview-loading">Rendering preview\u2026</span>
|
|
@@ -16563,6 +16923,9 @@ class SVGExportControl {
|
|
|
16563
16923
|
input.addEventListener('input', () => {
|
|
16564
16924
|
this.schedulePreview();
|
|
16565
16925
|
});
|
|
16926
|
+
input.addEventListener('change', () => {
|
|
16927
|
+
this.schedulePreview();
|
|
16928
|
+
});
|
|
16566
16929
|
});
|
|
16567
16930
|
querySelector(this.panel, '.btn-download').addEventListener('click', () => {
|
|
16568
16931
|
this.downloadSVG();
|
|
@@ -16628,8 +16991,8 @@ class SVGExportControl {
|
|
|
16628
16991
|
openBtn.disabled = true;
|
|
16629
16992
|
const width = Number(querySelector(panel, '.input-width').value);
|
|
16630
16993
|
const height = Number(querySelector(panel, '.input-height').value);
|
|
16631
|
-
const
|
|
16632
|
-
if (!width || !height ||
|
|
16994
|
+
const renderLabels = querySelector(panel, '.input-labels').checked;
|
|
16995
|
+
if (!width || !height || width < 1 || height < 1) {
|
|
16633
16996
|
previewContainer.innerHTML = '<span class="preview-loading">Invalid input values</span>';
|
|
16634
16997
|
return;
|
|
16635
16998
|
}
|
|
@@ -16640,11 +17003,11 @@ class SVGExportControl {
|
|
|
16640
17003
|
const svg = await renderToSVG({
|
|
16641
17004
|
width,
|
|
16642
17005
|
height,
|
|
16643
|
-
scale,
|
|
16644
17006
|
style,
|
|
16645
17007
|
lon: center.lng,
|
|
16646
17008
|
lat: center.lat,
|
|
16647
17009
|
zoom,
|
|
17010
|
+
renderLabels,
|
|
16648
17011
|
});
|
|
16649
17012
|
if (this.renderGeneration !== generation)
|
|
16650
17013
|
return;
|