@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.umd.js
CHANGED
|
@@ -69,18 +69,26 @@
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
.svg-export-panel .panel-inputs {
|
|
72
|
+
margin-bottom: 12px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.svg-export-panel .panel-inputs .grid {
|
|
72
76
|
display: grid;
|
|
73
|
-
grid-template-columns: 1fr 1fr
|
|
77
|
+
grid-template-columns: 1fr 1fr;
|
|
74
78
|
gap: 8px;
|
|
75
79
|
margin-bottom: 12px;
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
.svg-export-panel .panel-inputs label {
|
|
83
|
+
font-size: 12px;
|
|
84
|
+
color: #666;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.svg-export-panel .panel-inputs .grid label {
|
|
79
88
|
display: flex;
|
|
80
89
|
flex-direction: column;
|
|
81
90
|
gap: 4px;
|
|
82
|
-
|
|
83
|
-
color: #666;
|
|
91
|
+
width: 100%;
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
.svg-export-panel .panel-inputs input {
|
|
@@ -88,7 +96,6 @@
|
|
|
88
96
|
border: 1px solid #ccc;
|
|
89
97
|
border-radius: 4px;
|
|
90
98
|
font-size: 13px;
|
|
91
|
-
width: 100%;
|
|
92
99
|
box-sizing: border-box;
|
|
93
100
|
}
|
|
94
101
|
|
|
@@ -3006,6 +3013,23 @@
|
|
|
3006
3013
|
},
|
|
3007
3014
|
"property-type": "data-constant"
|
|
3008
3015
|
},
|
|
3016
|
+
resampling: {
|
|
3017
|
+
type: "enum",
|
|
3018
|
+
values: {
|
|
3019
|
+
linear: {
|
|
3020
|
+
},
|
|
3021
|
+
nearest: {
|
|
3022
|
+
}
|
|
3023
|
+
},
|
|
3024
|
+
"default": "linear",
|
|
3025
|
+
expression: {
|
|
3026
|
+
interpolated: false,
|
|
3027
|
+
parameters: [
|
|
3028
|
+
"zoom"
|
|
3029
|
+
]
|
|
3030
|
+
},
|
|
3031
|
+
"property-type": "data-constant"
|
|
3032
|
+
},
|
|
3009
3033
|
"raster-resampling": {
|
|
3010
3034
|
type: "enum",
|
|
3011
3035
|
values: {
|
|
@@ -3156,6 +3180,23 @@
|
|
|
3156
3180
|
]
|
|
3157
3181
|
},
|
|
3158
3182
|
"property-type": "data-constant"
|
|
3183
|
+
},
|
|
3184
|
+
resampling: {
|
|
3185
|
+
type: "enum",
|
|
3186
|
+
values: {
|
|
3187
|
+
linear: {
|
|
3188
|
+
},
|
|
3189
|
+
nearest: {
|
|
3190
|
+
}
|
|
3191
|
+
},
|
|
3192
|
+
"default": "linear",
|
|
3193
|
+
expression: {
|
|
3194
|
+
interpolated: false,
|
|
3195
|
+
parameters: [
|
|
3196
|
+
"zoom"
|
|
3197
|
+
]
|
|
3198
|
+
},
|
|
3199
|
+
"property-type": "data-constant"
|
|
3159
3200
|
}
|
|
3160
3201
|
};
|
|
3161
3202
|
var paint_background = {
|
|
@@ -3576,6 +3617,23 @@
|
|
|
3576
3617
|
]
|
|
3577
3618
|
},
|
|
3578
3619
|
"property-type": "color-ramp"
|
|
3620
|
+
},
|
|
3621
|
+
resampling: {
|
|
3622
|
+
type: "enum",
|
|
3623
|
+
values: {
|
|
3624
|
+
linear: {
|
|
3625
|
+
},
|
|
3626
|
+
nearest: {
|
|
3627
|
+
}
|
|
3628
|
+
},
|
|
3629
|
+
"default": "linear",
|
|
3630
|
+
expression: {
|
|
3631
|
+
interpolated: false,
|
|
3632
|
+
parameters: [
|
|
3633
|
+
"zoom"
|
|
3634
|
+
]
|
|
3635
|
+
},
|
|
3636
|
+
"property-type": "data-constant"
|
|
3579
3637
|
}
|
|
3580
3638
|
},
|
|
3581
3639
|
paint_background: paint_background,
|
|
@@ -6420,11 +6478,12 @@
|
|
|
6420
6478
|
}
|
|
6421
6479
|
|
|
6422
6480
|
class NumberFormat {
|
|
6423
|
-
constructor(number, locale, currency, minFractionDigits, maxFractionDigits) {
|
|
6481
|
+
constructor(number, locale, currency, unit, minFractionDigits, maxFractionDigits) {
|
|
6424
6482
|
this.type = StringType;
|
|
6425
6483
|
this.number = number;
|
|
6426
6484
|
this.locale = locale;
|
|
6427
6485
|
this.currency = currency;
|
|
6486
|
+
this.unit = unit;
|
|
6428
6487
|
this.minFractionDigits = minFractionDigits;
|
|
6429
6488
|
this.maxFractionDigits = maxFractionDigits;
|
|
6430
6489
|
}
|
|
@@ -6449,6 +6508,15 @@
|
|
|
6449
6508
|
if (!currency)
|
|
6450
6509
|
return null;
|
|
6451
6510
|
}
|
|
6511
|
+
let unit = null;
|
|
6512
|
+
if (options['unit']) {
|
|
6513
|
+
unit = context.parse(options['unit'], 1, StringType);
|
|
6514
|
+
if (!unit)
|
|
6515
|
+
return null;
|
|
6516
|
+
}
|
|
6517
|
+
if (currency && unit) {
|
|
6518
|
+
return context.error('NumberFormat options `currency` and `unit` are mutually exclusive');
|
|
6519
|
+
}
|
|
6452
6520
|
let minFractionDigits = null;
|
|
6453
6521
|
if (options['min-fraction-digits']) {
|
|
6454
6522
|
minFractionDigits = context.parse(options['min-fraction-digits'], 1, NumberType);
|
|
@@ -6461,12 +6529,13 @@
|
|
|
6461
6529
|
if (!maxFractionDigits)
|
|
6462
6530
|
return null;
|
|
6463
6531
|
}
|
|
6464
|
-
return new NumberFormat(number, locale, currency, minFractionDigits, maxFractionDigits);
|
|
6532
|
+
return new NumberFormat(number, locale, currency, unit, minFractionDigits, maxFractionDigits);
|
|
6465
6533
|
}
|
|
6466
6534
|
evaluate(ctx) {
|
|
6467
6535
|
return new Intl.NumberFormat(this.locale ? this.locale.evaluate(ctx) : [], {
|
|
6468
|
-
style: this.currency ? 'currency' : 'decimal',
|
|
6536
|
+
style: this.currency ? 'currency' : this.unit ? 'unit' : 'decimal',
|
|
6469
6537
|
currency: this.currency ? this.currency.evaluate(ctx) : undefined,
|
|
6538
|
+
unit: this.unit ? this.unit.evaluate(ctx) : undefined,
|
|
6470
6539
|
minimumFractionDigits: this.minFractionDigits
|
|
6471
6540
|
? this.minFractionDigits.evaluate(ctx)
|
|
6472
6541
|
: undefined,
|
|
@@ -6483,6 +6552,9 @@
|
|
|
6483
6552
|
if (this.currency) {
|
|
6484
6553
|
fn(this.currency);
|
|
6485
6554
|
}
|
|
6555
|
+
if (this.unit) {
|
|
6556
|
+
fn(this.unit);
|
|
6557
|
+
}
|
|
6486
6558
|
if (this.minFractionDigits) {
|
|
6487
6559
|
fn(this.minFractionDigits);
|
|
6488
6560
|
}
|
|
@@ -8221,6 +8293,16 @@
|
|
|
8221
8293
|
varargs(ValueType),
|
|
8222
8294
|
(ctx, args) => args.map((arg) => valueToString(arg.evaluate(ctx))).join('')
|
|
8223
8295
|
],
|
|
8296
|
+
split: [
|
|
8297
|
+
array(StringType),
|
|
8298
|
+
[StringType, StringType],
|
|
8299
|
+
(ctx, [s, delim]) => s.evaluate(ctx).split(delim.evaluate(ctx))
|
|
8300
|
+
],
|
|
8301
|
+
join: [
|
|
8302
|
+
StringType,
|
|
8303
|
+
[array(StringType), StringType],
|
|
8304
|
+
(ctx, [arr, delim]) => arr.value.join(delim.evaluate(ctx))
|
|
8305
|
+
],
|
|
8224
8306
|
'resolved-locale': [
|
|
8225
8307
|
StringType,
|
|
8226
8308
|
[CollatorType],
|
|
@@ -9365,13 +9447,11 @@
|
|
|
9365
9447
|
width;
|
|
9366
9448
|
height;
|
|
9367
9449
|
#svg;
|
|
9368
|
-
#scale;
|
|
9369
9450
|
#backgroundColor;
|
|
9370
9451
|
constructor(opt) {
|
|
9371
9452
|
this.width = opt.width;
|
|
9372
9453
|
this.height = opt.height;
|
|
9373
9454
|
this.#svg = [];
|
|
9374
|
-
this.#scale = opt.scale;
|
|
9375
9455
|
this.#backgroundColor = Color.transparent;
|
|
9376
9456
|
}
|
|
9377
9457
|
drawBackgroundFill(style) {
|
|
@@ -9379,7 +9459,7 @@
|
|
|
9379
9459
|
color.alpha *= style.opacity;
|
|
9380
9460
|
this.#backgroundColor = color;
|
|
9381
9461
|
}
|
|
9382
|
-
drawPolygons(features) {
|
|
9462
|
+
drawPolygons(id, features) {
|
|
9383
9463
|
if (features.length === 0)
|
|
9384
9464
|
return;
|
|
9385
9465
|
const groups = new Map();
|
|
@@ -9391,7 +9471,7 @@
|
|
|
9391
9471
|
return;
|
|
9392
9472
|
const translate = style.translate[0] === 0 && style.translate[1] === 0
|
|
9393
9473
|
? ''
|
|
9394
|
-
: ` transform="translate(${formatPoint(style.translate
|
|
9474
|
+
: ` transform="translate(${formatPoint(style.translate)})"`;
|
|
9395
9475
|
const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
|
|
9396
9476
|
const key = color.hex + translate + opacityAttr;
|
|
9397
9477
|
let group = groups.get(key);
|
|
@@ -9400,15 +9480,17 @@
|
|
|
9400
9480
|
groups.set(key, group);
|
|
9401
9481
|
}
|
|
9402
9482
|
feature.geometry.forEach((ring) => {
|
|
9403
|
-
group.segments.push(ring.map((p) => roundXY(p.x, p.y
|
|
9483
|
+
group.segments.push(ring.map((p) => roundXY(p.x, p.y)));
|
|
9404
9484
|
});
|
|
9405
9485
|
});
|
|
9486
|
+
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
9406
9487
|
for (const { segments, attrs } of groups.values()) {
|
|
9407
9488
|
const d = segmentsToPath(segments, true);
|
|
9408
9489
|
this.#svg.push(`<path d="${d}" ${attrs} />`);
|
|
9409
9490
|
}
|
|
9491
|
+
this.#svg.push('</g>');
|
|
9410
9492
|
}
|
|
9411
|
-
drawLineStrings(features) {
|
|
9493
|
+
drawLineStrings(id, features) {
|
|
9412
9494
|
if (features.length === 0)
|
|
9413
9495
|
return;
|
|
9414
9496
|
const groups = new Map();
|
|
@@ -9420,10 +9502,10 @@
|
|
|
9420
9502
|
return;
|
|
9421
9503
|
const translate = style.translate[0] === 0 && style.translate[1] === 0
|
|
9422
9504
|
? ''
|
|
9423
|
-
: ` transform="translate(${formatPoint(style.translate
|
|
9424
|
-
const roundedWidth = formatScaled(style.width
|
|
9505
|
+
: ` transform="translate(${formatPoint(style.translate)})"`;
|
|
9506
|
+
const roundedWidth = formatScaled(style.width);
|
|
9425
9507
|
const dasharrayStr = style.dasharray
|
|
9426
|
-
? style.dasharray.map((v) => formatScaled(v * style.width
|
|
9508
|
+
? style.dasharray.map((v) => formatScaled(v * style.width)).join(',')
|
|
9427
9509
|
: '';
|
|
9428
9510
|
const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
|
|
9429
9511
|
const key = [
|
|
@@ -9454,16 +9536,18 @@
|
|
|
9454
9536
|
groups.set(key, group);
|
|
9455
9537
|
}
|
|
9456
9538
|
feature.geometry.forEach((line) => {
|
|
9457
|
-
group.segments.push(line.map((p) => roundXY(p.x, p.y
|
|
9539
|
+
group.segments.push(line.map((p) => roundXY(p.x, p.y)));
|
|
9458
9540
|
});
|
|
9459
9541
|
});
|
|
9542
|
+
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
9460
9543
|
for (const { segments, attrs } of groups.values()) {
|
|
9461
9544
|
const chains = chainSegments(segments);
|
|
9462
9545
|
const d = segmentsToPath(chains);
|
|
9463
9546
|
this.#svg.push(`<path d="${d}" ${attrs} />`);
|
|
9464
9547
|
}
|
|
9548
|
+
this.#svg.push('</g>');
|
|
9465
9549
|
}
|
|
9466
|
-
drawCircles(features) {
|
|
9550
|
+
drawCircles(id, features) {
|
|
9467
9551
|
if (features.length === 0)
|
|
9468
9552
|
return;
|
|
9469
9553
|
const groups = new Map();
|
|
@@ -9475,12 +9559,10 @@
|
|
|
9475
9559
|
return;
|
|
9476
9560
|
const translate = style.translate[0] === 0 && style.translate[1] === 0
|
|
9477
9561
|
? ''
|
|
9478
|
-
: ` transform="translate(${formatPoint(style.translate
|
|
9479
|
-
const roundedRadius = formatScaled(style.radius
|
|
9562
|
+
: ` transform="translate(${formatPoint(style.translate)})"`;
|
|
9563
|
+
const roundedRadius = formatScaled(style.radius);
|
|
9480
9564
|
const strokeColor = new Color(style.strokeColor);
|
|
9481
|
-
const strokeAttrs = style.strokeWidth > 0
|
|
9482
|
-
? ` ${strokeAttr(strokeColor, formatScaled(style.strokeWidth, this.#scale))}`
|
|
9483
|
-
: '';
|
|
9565
|
+
const strokeAttrs = style.strokeWidth > 0 ? ` ${strokeAttr(strokeColor, formatScaled(style.strokeWidth))}` : '';
|
|
9484
9566
|
const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
|
|
9485
9567
|
const key = [color.hex, roundedRadius, strokeAttrs, opacityAttr, translate].join('\0');
|
|
9486
9568
|
let group = groups.get(key);
|
|
@@ -9494,16 +9576,111 @@
|
|
|
9494
9576
|
feature.geometry.forEach((ring) => {
|
|
9495
9577
|
const p = ring[0];
|
|
9496
9578
|
if (p)
|
|
9497
|
-
group.points.push(roundXY(p.x, p.y
|
|
9579
|
+
group.points.push(roundXY(p.x, p.y));
|
|
9498
9580
|
});
|
|
9499
9581
|
});
|
|
9582
|
+
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
9500
9583
|
for (const { points, attrs } of groups.values()) {
|
|
9501
9584
|
for (const [x, y] of points) {
|
|
9502
9585
|
this.#svg.push(`<circle cx="${formatNum(x)}" cy="${formatNum(y)}" ${attrs} />`);
|
|
9503
9586
|
}
|
|
9504
9587
|
}
|
|
9588
|
+
this.#svg.push('</g>');
|
|
9589
|
+
}
|
|
9590
|
+
drawLabels(id, features) {
|
|
9591
|
+
if (features.length === 0)
|
|
9592
|
+
return;
|
|
9593
|
+
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
9594
|
+
for (const [feature, style] of features) {
|
|
9595
|
+
if (style.opacity <= 0 || !style.text)
|
|
9596
|
+
continue;
|
|
9597
|
+
const color = new Color(style.color);
|
|
9598
|
+
if (color.alpha <= 0)
|
|
9599
|
+
continue;
|
|
9600
|
+
const ring = feature.geometry[0];
|
|
9601
|
+
if (!ring || ring.length === 0)
|
|
9602
|
+
continue;
|
|
9603
|
+
const point = ring[Math.floor(ring.length / 2)];
|
|
9604
|
+
const [px, py] = roundXY(point.x, point.y);
|
|
9605
|
+
const fontSize = formatScaled(style.size);
|
|
9606
|
+
const fontFamily = style.font.join(', ') + ', Helvetica, Arial, sans-serif';
|
|
9607
|
+
const [svgAnchor, baseline] = mapTextAnchor(style.anchor);
|
|
9608
|
+
const offsetX = style.offset[0] * style.size;
|
|
9609
|
+
const offsetY = style.offset[1] * style.size;
|
|
9610
|
+
const [dx, dy] = roundXY(offsetX, offsetY);
|
|
9611
|
+
const attrs = [
|
|
9612
|
+
`x="${formatNum(px)}"`,
|
|
9613
|
+
`y="${formatNum(py)}"`,
|
|
9614
|
+
`font-family="${escapeXml(fontFamily)}"`,
|
|
9615
|
+
`font-size="${fontSize}"`,
|
|
9616
|
+
`text-anchor="${svgAnchor}"`,
|
|
9617
|
+
`dominant-baseline="${baseline}"`,
|
|
9618
|
+
];
|
|
9619
|
+
if (dx !== 0)
|
|
9620
|
+
attrs.push(`dx="${formatNum(dx)}"`);
|
|
9621
|
+
if (dy !== 0)
|
|
9622
|
+
attrs.push(`dy="${formatNum(dy)}"`);
|
|
9623
|
+
if (style.rotate !== 0) {
|
|
9624
|
+
attrs.push(`transform="rotate(${String(style.rotate)},${formatNum(px)},${formatNum(py)})"`);
|
|
9625
|
+
}
|
|
9626
|
+
const haloColor = new Color(style.haloColor);
|
|
9627
|
+
if (style.haloWidth > 0 && haloColor.alpha > 0) {
|
|
9628
|
+
const haloWidth = formatScaled(style.haloWidth);
|
|
9629
|
+
attrs.push('paint-order="stroke fill"', `stroke="${haloColor.rgb}"`, `stroke-width="${haloWidth}"`, 'stroke-linejoin="round"');
|
|
9630
|
+
if (haloColor.alpha < 255)
|
|
9631
|
+
attrs.push(`stroke-opacity="${haloColor.opacity.toFixed(3)}"`);
|
|
9632
|
+
}
|
|
9633
|
+
attrs.push(fillAttr(color));
|
|
9634
|
+
if (style.opacity < 1)
|
|
9635
|
+
attrs.push(`opacity="${style.opacity.toFixed(3)}"`);
|
|
9636
|
+
this.#svg.push(`<text ${attrs.join(' ')}>${escapeXml(style.text)}</text>`);
|
|
9637
|
+
}
|
|
9638
|
+
this.#svg.push('</g>');
|
|
9639
|
+
}
|
|
9640
|
+
drawIcons(id, features, spriteAtlas) {
|
|
9641
|
+
if (features.length === 0)
|
|
9642
|
+
return;
|
|
9643
|
+
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
9644
|
+
for (const [feature, style] of features) {
|
|
9645
|
+
if (style.opacity <= 0)
|
|
9646
|
+
continue;
|
|
9647
|
+
const sprite = spriteAtlas.get(style.image);
|
|
9648
|
+
if (!sprite)
|
|
9649
|
+
continue;
|
|
9650
|
+
const ring = feature.geometry[0];
|
|
9651
|
+
if (!ring || ring.length === 0)
|
|
9652
|
+
continue;
|
|
9653
|
+
const point = ring[Math.floor(ring.length / 2)];
|
|
9654
|
+
const scale = style.size / sprite.pixelRatio;
|
|
9655
|
+
const iconW = sprite.width * scale;
|
|
9656
|
+
const iconH = sprite.height * scale;
|
|
9657
|
+
const [anchorDx, anchorDy] = mapIconAnchor(style.anchor, iconW, iconH);
|
|
9658
|
+
const ox = style.offset[0] * style.size + anchorDx;
|
|
9659
|
+
const oy = style.offset[1] * style.size + anchorDy;
|
|
9660
|
+
const x = point.x + ox;
|
|
9661
|
+
const y = point.y + oy;
|
|
9662
|
+
const [sx, sy] = roundXY(x, y);
|
|
9663
|
+
const [sw, sh] = roundXY(iconW, iconH);
|
|
9664
|
+
const viewBox = `${String(sprite.x)} ${String(sprite.y)} ${String(sprite.width)} ${String(sprite.height)}`;
|
|
9665
|
+
const attrs = [
|
|
9666
|
+
`x="${formatNum(sx)}"`,
|
|
9667
|
+
`y="${formatNum(sy)}"`,
|
|
9668
|
+
`width="${formatNum(sw)}"`,
|
|
9669
|
+
`height="${formatNum(sh)}"`,
|
|
9670
|
+
];
|
|
9671
|
+
if (style.opacity < 1)
|
|
9672
|
+
attrs.push(`opacity="${style.opacity.toFixed(3)}"`);
|
|
9673
|
+
if (style.rotate !== 0) {
|
|
9674
|
+
const [cx, cy] = roundXY(point.x + style.offset[0] * style.size, point.y + style.offset[1] * style.size);
|
|
9675
|
+
attrs.push(`transform="rotate(${String(style.rotate)},${formatNum(cx)},${formatNum(cy)})"`);
|
|
9676
|
+
}
|
|
9677
|
+
this.#svg.push(`<svg ${attrs.join(' ')} viewBox="${viewBox}" xmlns="http://www.w3.org/2000/svg">` +
|
|
9678
|
+
`<image width="${String(sprite.sheetWidth)}" height="${String(sprite.sheetHeight)}" href="${sprite.sheetDataUri}" />` +
|
|
9679
|
+
`</svg>`);
|
|
9680
|
+
}
|
|
9681
|
+
this.#svg.push('</g>');
|
|
9505
9682
|
}
|
|
9506
|
-
drawRasterTiles(tiles, style) {
|
|
9683
|
+
drawRasterTiles(id, tiles, style) {
|
|
9507
9684
|
if (tiles.length === 0)
|
|
9508
9685
|
return;
|
|
9509
9686
|
if (style.opacity <= 0)
|
|
@@ -9519,15 +9696,14 @@
|
|
|
9519
9696
|
const brightness = (style.brightnessMin + style.brightnessMax) / 2;
|
|
9520
9697
|
filters.push(`brightness(${String(brightness)})`);
|
|
9521
9698
|
}
|
|
9522
|
-
let gAttrs = `opacity="${String(style.opacity)}"`;
|
|
9699
|
+
let gAttrs = `id="${escapeXml(id)}" opacity="${String(style.opacity)}"`;
|
|
9523
9700
|
if (filters.length > 0)
|
|
9524
9701
|
gAttrs += ` filter="${filters.join(' ')}"`;
|
|
9525
9702
|
this.#svg.push(`<g ${gAttrs}>`);
|
|
9526
9703
|
const pixelated = style.resampling === 'nearest';
|
|
9527
9704
|
for (const tile of tiles) {
|
|
9528
9705
|
const overlap = Math.min(tile.width, tile.height) / 10000; // slight overlap to prevent sub-pixel gaps between tiles
|
|
9529
|
-
|
|
9530
|
-
let attrs = `x="${formatScaled(tile.x - overlap, s)}" y="${formatScaled(tile.y - overlap, s)}" width="${formatScaled(tile.width + overlap * 2, s)}" height="${formatScaled(tile.height + overlap * 2, s)}" href="${tile.dataUri}"`;
|
|
9706
|
+
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}"`;
|
|
9531
9707
|
if (pixelated)
|
|
9532
9708
|
attrs += ' style="image-rendering:pixelated"';
|
|
9533
9709
|
this.#svg.push(`<image ${attrs} />`);
|
|
@@ -9561,16 +9737,67 @@
|
|
|
9561
9737
|
attr += ` stroke-opacity="${color.opacity.toFixed(3)}"`;
|
|
9562
9738
|
return attr;
|
|
9563
9739
|
}
|
|
9564
|
-
function formatScaled(v
|
|
9565
|
-
return formatNum(Math.round(v *
|
|
9740
|
+
function formatScaled(v) {
|
|
9741
|
+
return formatNum(Math.round(v * 10));
|
|
9566
9742
|
}
|
|
9567
|
-
function roundXY(x, y
|
|
9568
|
-
return [Math.round(x *
|
|
9743
|
+
function roundXY(x, y) {
|
|
9744
|
+
return [Math.round(x * 10), Math.round(y * 10)];
|
|
9569
9745
|
}
|
|
9570
|
-
function formatPoint(p
|
|
9571
|
-
const [x, y] = roundXY(p[0], p[1]
|
|
9746
|
+
function formatPoint(p) {
|
|
9747
|
+
const [x, y] = roundXY(p[0], p[1]);
|
|
9572
9748
|
return formatNum(x) + ',' + formatNum(y);
|
|
9573
9749
|
}
|
|
9750
|
+
function mapTextAnchor(anchor) {
|
|
9751
|
+
switch (anchor) {
|
|
9752
|
+
case 'left':
|
|
9753
|
+
return ['start', 'central'];
|
|
9754
|
+
case 'right':
|
|
9755
|
+
return ['end', 'central'];
|
|
9756
|
+
case 'top':
|
|
9757
|
+
return ['middle', 'text-before-edge'];
|
|
9758
|
+
case 'bottom':
|
|
9759
|
+
return ['middle', 'text-after-edge'];
|
|
9760
|
+
case 'top-left':
|
|
9761
|
+
return ['start', 'text-before-edge'];
|
|
9762
|
+
case 'top-right':
|
|
9763
|
+
return ['end', 'text-before-edge'];
|
|
9764
|
+
case 'bottom-left':
|
|
9765
|
+
return ['start', 'text-after-edge'];
|
|
9766
|
+
case 'bottom-right':
|
|
9767
|
+
return ['end', 'text-after-edge'];
|
|
9768
|
+
default:
|
|
9769
|
+
return ['middle', 'central'];
|
|
9770
|
+
}
|
|
9771
|
+
}
|
|
9772
|
+
function mapIconAnchor(anchor, w, h) {
|
|
9773
|
+
switch (anchor) {
|
|
9774
|
+
case 'left':
|
|
9775
|
+
return [0, -h / 2];
|
|
9776
|
+
case 'right':
|
|
9777
|
+
return [-w, -h / 2];
|
|
9778
|
+
case 'top':
|
|
9779
|
+
return [-w / 2, 0];
|
|
9780
|
+
case 'bottom':
|
|
9781
|
+
return [-w / 2, -h];
|
|
9782
|
+
case 'top-left':
|
|
9783
|
+
return [0, 0];
|
|
9784
|
+
case 'top-right':
|
|
9785
|
+
return [-w, 0];
|
|
9786
|
+
case 'bottom-left':
|
|
9787
|
+
return [0, -h];
|
|
9788
|
+
case 'bottom-right':
|
|
9789
|
+
return [-w, -h];
|
|
9790
|
+
default:
|
|
9791
|
+
return [-w / 2, -h / 2];
|
|
9792
|
+
}
|
|
9793
|
+
}
|
|
9794
|
+
function escapeXml(s) {
|
|
9795
|
+
return s
|
|
9796
|
+
.replace(/&/g, '&')
|
|
9797
|
+
.replace(/</g, '<')
|
|
9798
|
+
.replace(/>/g, '>')
|
|
9799
|
+
.replace(/"/g, '"');
|
|
9800
|
+
}
|
|
9574
9801
|
|
|
9575
9802
|
/*
|
|
9576
9803
|
* bignumber.js v9.3.1
|
|
@@ -16119,6 +16346,65 @@
|
|
|
16119
16346
|
return rasterTiles.filter((tile) => tile !== null);
|
|
16120
16347
|
}
|
|
16121
16348
|
|
|
16349
|
+
async function loadSpriteAtlas(style) {
|
|
16350
|
+
const atlas = new Map();
|
|
16351
|
+
const sprite = style.sprite;
|
|
16352
|
+
if (!sprite)
|
|
16353
|
+
return atlas;
|
|
16354
|
+
const sources = [];
|
|
16355
|
+
if (typeof sprite === 'string') {
|
|
16356
|
+
sources.push({ id: 'default', url: sprite });
|
|
16357
|
+
}
|
|
16358
|
+
else if (Array.isArray(sprite)) {
|
|
16359
|
+
for (const s of sprite) {
|
|
16360
|
+
sources.push({
|
|
16361
|
+
id: s.id,
|
|
16362
|
+
url: s.url,
|
|
16363
|
+
});
|
|
16364
|
+
}
|
|
16365
|
+
}
|
|
16366
|
+
await Promise.all(sources.map(async ({ id, url }) => {
|
|
16367
|
+
try {
|
|
16368
|
+
const [jsonResponse, imageResponse] = await Promise.all([
|
|
16369
|
+
fetch(`${url}.json`),
|
|
16370
|
+
fetch(`${url}.png`),
|
|
16371
|
+
]);
|
|
16372
|
+
if (!jsonResponse.ok || !imageResponse.ok)
|
|
16373
|
+
return;
|
|
16374
|
+
const json = (await jsonResponse.json());
|
|
16375
|
+
const imageBuffer = await imageResponse.arrayBuffer();
|
|
16376
|
+
const base64 = typeof Buffer !== 'undefined'
|
|
16377
|
+
? Buffer.from(imageBuffer).toString('base64')
|
|
16378
|
+
: btoa(String.fromCharCode(...new Uint8Array(imageBuffer)));
|
|
16379
|
+
const sheetDataUri = `data:image/png;base64,${base64}`;
|
|
16380
|
+
// Estimate sheet dimensions from sprite entries
|
|
16381
|
+
let sheetWidth = 0;
|
|
16382
|
+
let sheetHeight = 0;
|
|
16383
|
+
for (const entry of Object.values(json)) {
|
|
16384
|
+
sheetWidth = Math.max(sheetWidth, entry.x + entry.width);
|
|
16385
|
+
sheetHeight = Math.max(sheetHeight, entry.y + entry.height);
|
|
16386
|
+
}
|
|
16387
|
+
const prefix = id === 'default' ? '' : `${id}:`;
|
|
16388
|
+
for (const [name, entry] of Object.entries(json)) {
|
|
16389
|
+
atlas.set(`${prefix}${name}`, {
|
|
16390
|
+
width: entry.width,
|
|
16391
|
+
height: entry.height,
|
|
16392
|
+
x: entry.x,
|
|
16393
|
+
y: entry.y,
|
|
16394
|
+
pixelRatio: entry.pixelRatio ?? 1,
|
|
16395
|
+
sheetDataUri,
|
|
16396
|
+
sheetWidth,
|
|
16397
|
+
sheetHeight,
|
|
16398
|
+
});
|
|
16399
|
+
}
|
|
16400
|
+
}
|
|
16401
|
+
catch {
|
|
16402
|
+
// Silently skip failed sprite loads
|
|
16403
|
+
}
|
|
16404
|
+
}));
|
|
16405
|
+
return atlas;
|
|
16406
|
+
}
|
|
16407
|
+
|
|
16122
16408
|
async function getLayerFeatures(job) {
|
|
16123
16409
|
const { width, height } = job.renderer;
|
|
16124
16410
|
const { zoom, center } = job.view;
|
|
@@ -16279,6 +16565,12 @@
|
|
|
16279
16565
|
return layers.map(createStyleLayer);
|
|
16280
16566
|
}
|
|
16281
16567
|
|
|
16568
|
+
function resolveTokens(text, properties) {
|
|
16569
|
+
return text.replace(/\{([^}]+)\}/g, (_, key) => {
|
|
16570
|
+
const value = properties[key];
|
|
16571
|
+
return value != null ? String(value) : '';
|
|
16572
|
+
});
|
|
16573
|
+
}
|
|
16282
16574
|
async function renderMap(job) {
|
|
16283
16575
|
await render(job);
|
|
16284
16576
|
return job.renderer.getString();
|
|
@@ -16289,9 +16581,12 @@
|
|
|
16289
16581
|
async function render(job) {
|
|
16290
16582
|
const { renderer } = job;
|
|
16291
16583
|
const { zoom } = job.view;
|
|
16292
|
-
const layerFeatures = await
|
|
16584
|
+
const [layerFeatures, spriteAtlas] = await Promise.all([
|
|
16585
|
+
getLayerFeatures(job),
|
|
16586
|
+
job.renderLabels ? loadSpriteAtlas(job.style) : Promise.resolve(new Map()),
|
|
16587
|
+
]);
|
|
16293
16588
|
const layerStyles = getLayerStyles(job.style.layers);
|
|
16294
|
-
const availableImages = [];
|
|
16589
|
+
const availableImages = [...spriteAtlas.keys()];
|
|
16295
16590
|
const featureState = {};
|
|
16296
16591
|
for (const layerStyle of layerStyles) {
|
|
16297
16592
|
if (layerStyle.isHidden(zoom))
|
|
@@ -16312,6 +16607,7 @@
|
|
|
16312
16607
|
function getLayout(key, feature) {
|
|
16313
16608
|
return getStyleValue(layerStyle.layout, key, feature);
|
|
16314
16609
|
}
|
|
16610
|
+
const layerId = layerStyle.id;
|
|
16315
16611
|
switch (layerStyle.type) {
|
|
16316
16612
|
case 'background':
|
|
16317
16613
|
{
|
|
@@ -16331,7 +16627,7 @@
|
|
|
16331
16627
|
: polygons;
|
|
16332
16628
|
if (polygonFeatures.length === 0)
|
|
16333
16629
|
continue;
|
|
16334
|
-
renderer.drawPolygons(polygonFeatures.map((feature) => [
|
|
16630
|
+
renderer.drawPolygons(layerId, polygonFeatures.map((feature) => [
|
|
16335
16631
|
feature,
|
|
16336
16632
|
{
|
|
16337
16633
|
color: getPaint('fill-color', feature),
|
|
@@ -16351,7 +16647,7 @@
|
|
|
16351
16647
|
: lineStrings;
|
|
16352
16648
|
if (lineStringFeatures.length === 0)
|
|
16353
16649
|
continue;
|
|
16354
|
-
renderer.drawLineStrings(lineStringFeatures.map((feature) => [
|
|
16650
|
+
renderer.drawLineStrings(layerId, lineStringFeatures.map((feature) => [
|
|
16355
16651
|
feature,
|
|
16356
16652
|
{
|
|
16357
16653
|
color: getPaint('line-color', feature),
|
|
@@ -16370,7 +16666,7 @@
|
|
|
16370
16666
|
case 'raster':
|
|
16371
16667
|
{
|
|
16372
16668
|
const tiles = await getRasterTiles(job, layerStyle.source);
|
|
16373
|
-
renderer.drawRasterTiles(tiles, {
|
|
16669
|
+
renderer.drawRasterTiles(layerId, tiles, {
|
|
16374
16670
|
opacity: getPaint('raster-opacity'),
|
|
16375
16671
|
hueRotate: getPaint('raster-hue-rotate'),
|
|
16376
16672
|
brightnessMin: getPaint('raster-brightness-min'),
|
|
@@ -16391,7 +16687,7 @@
|
|
|
16391
16687
|
: points;
|
|
16392
16688
|
if (pointFeatures.length === 0)
|
|
16393
16689
|
continue;
|
|
16394
|
-
renderer.drawCircles(pointFeatures.map((feature) => [
|
|
16690
|
+
renderer.drawCircles(layerId, pointFeatures.map((feature) => [
|
|
16395
16691
|
feature,
|
|
16396
16692
|
{
|
|
16397
16693
|
color: getPaint('circle-color', feature),
|
|
@@ -16404,11 +16700,76 @@
|
|
|
16404
16700
|
]));
|
|
16405
16701
|
}
|
|
16406
16702
|
continue;
|
|
16703
|
+
case 'symbol':
|
|
16704
|
+
{
|
|
16705
|
+
if (!job.renderLabels)
|
|
16706
|
+
continue;
|
|
16707
|
+
const features = getFeatures(layerFeatures, layerStyle);
|
|
16708
|
+
const allFeatures = [
|
|
16709
|
+
...(features?.points ?? []),
|
|
16710
|
+
...(features?.linestrings ?? []),
|
|
16711
|
+
...(features?.polygons ?? []),
|
|
16712
|
+
];
|
|
16713
|
+
if (allFeatures.length === 0)
|
|
16714
|
+
continue;
|
|
16715
|
+
const symbolFeatures = layerStyle.filterFn
|
|
16716
|
+
? allFeatures.filter((feature) => layerStyle.filterFn.filter({ zoom }, feature))
|
|
16717
|
+
: allFeatures;
|
|
16718
|
+
if (symbolFeatures.length === 0)
|
|
16719
|
+
continue;
|
|
16720
|
+
// Render icons first (underneath text)
|
|
16721
|
+
renderer.drawIcons(`${layerId}-icons`, symbolFeatures.flatMap((feature) => {
|
|
16722
|
+
const iconImage = getLayout('icon-image', feature);
|
|
16723
|
+
const iconName = iconImage != null
|
|
16724
|
+
? resolveTokens(iconImage.toString(), feature.properties)
|
|
16725
|
+
: '';
|
|
16726
|
+
if (!iconName || !spriteAtlas.has(iconName))
|
|
16727
|
+
return [];
|
|
16728
|
+
return [
|
|
16729
|
+
[
|
|
16730
|
+
feature,
|
|
16731
|
+
{
|
|
16732
|
+
image: iconName,
|
|
16733
|
+
size: getLayout('icon-size', feature),
|
|
16734
|
+
anchor: getLayout('icon-anchor', feature),
|
|
16735
|
+
offset: getLayout('icon-offset', feature),
|
|
16736
|
+
rotate: getLayout('icon-rotate', feature),
|
|
16737
|
+
opacity: getPaint('icon-opacity', feature),
|
|
16738
|
+
},
|
|
16739
|
+
],
|
|
16740
|
+
];
|
|
16741
|
+
}), spriteAtlas);
|
|
16742
|
+
// Render text labels on top
|
|
16743
|
+
renderer.drawLabels(`${layerId}-labels`, symbolFeatures.flatMap((feature) => {
|
|
16744
|
+
const textField = getLayout('text-field', feature);
|
|
16745
|
+
const textRaw = textField != null ? textField.toString() : '';
|
|
16746
|
+
const text = resolveTokens(textRaw, feature.properties);
|
|
16747
|
+
if (!text)
|
|
16748
|
+
return [];
|
|
16749
|
+
return [
|
|
16750
|
+
[
|
|
16751
|
+
feature,
|
|
16752
|
+
{
|
|
16753
|
+
text,
|
|
16754
|
+
size: getLayout('text-size', feature),
|
|
16755
|
+
font: getLayout('text-font', feature),
|
|
16756
|
+
anchor: getLayout('text-anchor', feature),
|
|
16757
|
+
offset: getLayout('text-offset', feature),
|
|
16758
|
+
rotate: getLayout('text-rotate', feature),
|
|
16759
|
+
color: getPaint('text-color', feature),
|
|
16760
|
+
opacity: getPaint('text-opacity', feature),
|
|
16761
|
+
haloColor: getPaint('text-halo-color', feature),
|
|
16762
|
+
haloWidth: getPaint('text-halo-width', feature),
|
|
16763
|
+
},
|
|
16764
|
+
],
|
|
16765
|
+
];
|
|
16766
|
+
}));
|
|
16767
|
+
}
|
|
16768
|
+
continue;
|
|
16407
16769
|
case 'color-relief':
|
|
16408
16770
|
case 'fill-extrusion':
|
|
16409
16771
|
case 'heatmap':
|
|
16410
16772
|
case 'hillshade':
|
|
16411
|
-
case 'symbol':
|
|
16412
16773
|
continue;
|
|
16413
16774
|
default:
|
|
16414
16775
|
throw Error('layerStyle.type: ' + String(layerStyle.type));
|
|
@@ -16419,20 +16780,18 @@
|
|
|
16419
16780
|
async function renderToSVG(options) {
|
|
16420
16781
|
const width = options.width ?? 1024;
|
|
16421
16782
|
const height = options.height ?? 1024;
|
|
16422
|
-
const scale = options.scale ?? 1;
|
|
16423
16783
|
if (width <= 0)
|
|
16424
16784
|
throw new Error('width must be positive');
|
|
16425
16785
|
if (height <= 0)
|
|
16426
16786
|
throw new Error('height must be positive');
|
|
16427
|
-
if (scale <= 0)
|
|
16428
|
-
throw new Error('scale must be positive');
|
|
16429
16787
|
return await renderMap({
|
|
16430
|
-
renderer: new SVGRenderer({ width, height
|
|
16788
|
+
renderer: new SVGRenderer({ width, height }),
|
|
16431
16789
|
style: options.style,
|
|
16432
16790
|
view: {
|
|
16433
16791
|
center: [options.lon ?? 0, options.lat ?? 0],
|
|
16434
16792
|
zoom: options.zoom ?? 2,
|
|
16435
16793
|
},
|
|
16794
|
+
renderLabels: options.renderLabels ?? false,
|
|
16436
16795
|
});
|
|
16437
16796
|
}
|
|
16438
16797
|
|
|
@@ -16488,7 +16847,6 @@
|
|
|
16488
16847
|
this.options = {
|
|
16489
16848
|
defaultWidth: options?.defaultWidth ?? 1024,
|
|
16490
16849
|
defaultHeight: options?.defaultHeight ?? 1024,
|
|
16491
|
-
defaultScale: options?.defaultScale ?? 1,
|
|
16492
16850
|
};
|
|
16493
16851
|
}
|
|
16494
16852
|
onAdd(map) {
|
|
@@ -16529,12 +16887,14 @@
|
|
|
16529
16887
|
<div class="panel-notice">
|
|
16530
16888
|
Note:<br>
|
|
16531
16889
|
<span class="panel-attribution"></span><br>
|
|
16532
|
-
|
|
16890
|
+
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>
|
|
16533
16891
|
</div>
|
|
16534
16892
|
<div class="panel-inputs">
|
|
16535
|
-
<
|
|
16536
|
-
|
|
16537
|
-
|
|
16893
|
+
<div class="grid">
|
|
16894
|
+
<label>Width<input type="number" class="input-width" value="${String(this.options.defaultWidth)}" min="1" max="8192"></label>
|
|
16895
|
+
<label>Height<input type="number" class="input-height" value="${String(this.options.defaultHeight)}" min="1" max="8192"></label>
|
|
16896
|
+
</div>
|
|
16897
|
+
<label class="label-checkbox"><input type="checkbox" class="input-labels"> Include labels and icons (buggy)</label>
|
|
16538
16898
|
</div>
|
|
16539
16899
|
<div class="preview-container">
|
|
16540
16900
|
<span class="preview-loading">Rendering preview\u2026</span>
|
|
@@ -16567,6 +16927,9 @@
|
|
|
16567
16927
|
input.addEventListener('input', () => {
|
|
16568
16928
|
this.schedulePreview();
|
|
16569
16929
|
});
|
|
16930
|
+
input.addEventListener('change', () => {
|
|
16931
|
+
this.schedulePreview();
|
|
16932
|
+
});
|
|
16570
16933
|
});
|
|
16571
16934
|
querySelector(this.panel, '.btn-download').addEventListener('click', () => {
|
|
16572
16935
|
this.downloadSVG();
|
|
@@ -16632,8 +16995,8 @@
|
|
|
16632
16995
|
openBtn.disabled = true;
|
|
16633
16996
|
const width = Number(querySelector(panel, '.input-width').value);
|
|
16634
16997
|
const height = Number(querySelector(panel, '.input-height').value);
|
|
16635
|
-
const
|
|
16636
|
-
if (!width || !height ||
|
|
16998
|
+
const renderLabels = querySelector(panel, '.input-labels').checked;
|
|
16999
|
+
if (!width || !height || width < 1 || height < 1) {
|
|
16637
17000
|
previewContainer.innerHTML = '<span class="preview-loading">Invalid input values</span>';
|
|
16638
17001
|
return;
|
|
16639
17002
|
}
|
|
@@ -16644,11 +17007,11 @@
|
|
|
16644
17007
|
const svg = await renderToSVG({
|
|
16645
17008
|
width,
|
|
16646
17009
|
height,
|
|
16647
|
-
scale,
|
|
16648
17010
|
style,
|
|
16649
17011
|
lon: center.lng,
|
|
16650
17012
|
lat: center.lat,
|
|
16651
17013
|
zoom,
|
|
17014
|
+
renderLabels,
|
|
16652
17015
|
});
|
|
16653
17016
|
if (this.renderGeneration !== generation)
|
|
16654
17017
|
return;
|