@versatiles/svg-renderer 0.5.2 → 0.7.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 +505 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +505 -24
- package/dist/index.js.map +1 -1
- package/dist/maplibre.cjs +526 -30
- package/dist/maplibre.cjs.map +1 -1
- package/dist/maplibre.d.ts +1 -0
- package/dist/maplibre.js +526 -30
- package/dist/maplibre.js.map +1 -1
- package/dist/maplibre.umd.js +526 -30
- package/dist/maplibre.umd.js.map +1 -1
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -2826,6 +2826,23 @@ var paint_raster = {
|
|
|
2826
2826
|
},
|
|
2827
2827
|
"property-type": "data-constant"
|
|
2828
2828
|
},
|
|
2829
|
+
resampling: {
|
|
2830
|
+
type: "enum",
|
|
2831
|
+
values: {
|
|
2832
|
+
linear: {
|
|
2833
|
+
},
|
|
2834
|
+
nearest: {
|
|
2835
|
+
}
|
|
2836
|
+
},
|
|
2837
|
+
"default": "linear",
|
|
2838
|
+
expression: {
|
|
2839
|
+
interpolated: false,
|
|
2840
|
+
parameters: [
|
|
2841
|
+
"zoom"
|
|
2842
|
+
]
|
|
2843
|
+
},
|
|
2844
|
+
"property-type": "data-constant"
|
|
2845
|
+
},
|
|
2829
2846
|
"raster-resampling": {
|
|
2830
2847
|
type: "enum",
|
|
2831
2848
|
values: {
|
|
@@ -2976,6 +2993,23 @@ var paint_hillshade = {
|
|
|
2976
2993
|
]
|
|
2977
2994
|
},
|
|
2978
2995
|
"property-type": "data-constant"
|
|
2996
|
+
},
|
|
2997
|
+
resampling: {
|
|
2998
|
+
type: "enum",
|
|
2999
|
+
values: {
|
|
3000
|
+
linear: {
|
|
3001
|
+
},
|
|
3002
|
+
nearest: {
|
|
3003
|
+
}
|
|
3004
|
+
},
|
|
3005
|
+
"default": "linear",
|
|
3006
|
+
expression: {
|
|
3007
|
+
interpolated: false,
|
|
3008
|
+
parameters: [
|
|
3009
|
+
"zoom"
|
|
3010
|
+
]
|
|
3011
|
+
},
|
|
3012
|
+
"property-type": "data-constant"
|
|
2979
3013
|
}
|
|
2980
3014
|
};
|
|
2981
3015
|
var paint_background = {
|
|
@@ -3396,6 +3430,23 @@ var v8Spec = {
|
|
|
3396
3430
|
]
|
|
3397
3431
|
},
|
|
3398
3432
|
"property-type": "color-ramp"
|
|
3433
|
+
},
|
|
3434
|
+
resampling: {
|
|
3435
|
+
type: "enum",
|
|
3436
|
+
values: {
|
|
3437
|
+
linear: {
|
|
3438
|
+
},
|
|
3439
|
+
nearest: {
|
|
3440
|
+
}
|
|
3441
|
+
},
|
|
3442
|
+
"default": "linear",
|
|
3443
|
+
expression: {
|
|
3444
|
+
interpolated: false,
|
|
3445
|
+
parameters: [
|
|
3446
|
+
"zoom"
|
|
3447
|
+
]
|
|
3448
|
+
},
|
|
3449
|
+
"property-type": "data-constant"
|
|
3399
3450
|
}
|
|
3400
3451
|
},
|
|
3401
3452
|
paint_background: paint_background,
|
|
@@ -6240,11 +6291,12 @@ class CollatorExpression {
|
|
|
6240
6291
|
}
|
|
6241
6292
|
|
|
6242
6293
|
class NumberFormat {
|
|
6243
|
-
constructor(number, locale, currency, minFractionDigits, maxFractionDigits) {
|
|
6294
|
+
constructor(number, locale, currency, unit, minFractionDigits, maxFractionDigits) {
|
|
6244
6295
|
this.type = StringType;
|
|
6245
6296
|
this.number = number;
|
|
6246
6297
|
this.locale = locale;
|
|
6247
6298
|
this.currency = currency;
|
|
6299
|
+
this.unit = unit;
|
|
6248
6300
|
this.minFractionDigits = minFractionDigits;
|
|
6249
6301
|
this.maxFractionDigits = maxFractionDigits;
|
|
6250
6302
|
}
|
|
@@ -6269,6 +6321,15 @@ class NumberFormat {
|
|
|
6269
6321
|
if (!currency)
|
|
6270
6322
|
return null;
|
|
6271
6323
|
}
|
|
6324
|
+
let unit = null;
|
|
6325
|
+
if (options['unit']) {
|
|
6326
|
+
unit = context.parse(options['unit'], 1, StringType);
|
|
6327
|
+
if (!unit)
|
|
6328
|
+
return null;
|
|
6329
|
+
}
|
|
6330
|
+
if (currency && unit) {
|
|
6331
|
+
return context.error('NumberFormat options `currency` and `unit` are mutually exclusive');
|
|
6332
|
+
}
|
|
6272
6333
|
let minFractionDigits = null;
|
|
6273
6334
|
if (options['min-fraction-digits']) {
|
|
6274
6335
|
minFractionDigits = context.parse(options['min-fraction-digits'], 1, NumberType);
|
|
@@ -6281,12 +6342,13 @@ class NumberFormat {
|
|
|
6281
6342
|
if (!maxFractionDigits)
|
|
6282
6343
|
return null;
|
|
6283
6344
|
}
|
|
6284
|
-
return new NumberFormat(number, locale, currency, minFractionDigits, maxFractionDigits);
|
|
6345
|
+
return new NumberFormat(number, locale, currency, unit, minFractionDigits, maxFractionDigits);
|
|
6285
6346
|
}
|
|
6286
6347
|
evaluate(ctx) {
|
|
6287
6348
|
return new Intl.NumberFormat(this.locale ? this.locale.evaluate(ctx) : [], {
|
|
6288
|
-
style: this.currency ? 'currency' : 'decimal',
|
|
6349
|
+
style: this.currency ? 'currency' : this.unit ? 'unit' : 'decimal',
|
|
6289
6350
|
currency: this.currency ? this.currency.evaluate(ctx) : undefined,
|
|
6351
|
+
unit: this.unit ? this.unit.evaluate(ctx) : undefined,
|
|
6290
6352
|
minimumFractionDigits: this.minFractionDigits
|
|
6291
6353
|
? this.minFractionDigits.evaluate(ctx)
|
|
6292
6354
|
: undefined,
|
|
@@ -6303,6 +6365,9 @@ class NumberFormat {
|
|
|
6303
6365
|
if (this.currency) {
|
|
6304
6366
|
fn(this.currency);
|
|
6305
6367
|
}
|
|
6368
|
+
if (this.unit) {
|
|
6369
|
+
fn(this.unit);
|
|
6370
|
+
}
|
|
6306
6371
|
if (this.minFractionDigits) {
|
|
6307
6372
|
fn(this.minFractionDigits);
|
|
6308
6373
|
}
|
|
@@ -8041,6 +8106,16 @@ CompoundExpression.register(expressions$1, {
|
|
|
8041
8106
|
varargs(ValueType),
|
|
8042
8107
|
(ctx, args) => args.map((arg) => valueToString(arg.evaluate(ctx))).join('')
|
|
8043
8108
|
],
|
|
8109
|
+
split: [
|
|
8110
|
+
array(StringType),
|
|
8111
|
+
[StringType, StringType],
|
|
8112
|
+
(ctx, [s, delim]) => s.evaluate(ctx).split(delim.evaluate(ctx))
|
|
8113
|
+
],
|
|
8114
|
+
join: [
|
|
8115
|
+
StringType,
|
|
8116
|
+
[array(StringType), StringType],
|
|
8117
|
+
(ctx, [arr, delim]) => arr.value.join(delim.evaluate(ctx))
|
|
8118
|
+
],
|
|
8044
8119
|
'resolved-locale': [
|
|
8045
8120
|
StringType,
|
|
8046
8121
|
[CollatorType],
|
|
@@ -9186,6 +9261,9 @@ class SVGRenderer {
|
|
|
9186
9261
|
height;
|
|
9187
9262
|
#svg;
|
|
9188
9263
|
#backgroundColor;
|
|
9264
|
+
#spriteSheetDefs = new Map();
|
|
9265
|
+
#spriteSymbolDefs = new Map();
|
|
9266
|
+
#sdfFilterDefs = new Map();
|
|
9189
9267
|
constructor(opt) {
|
|
9190
9268
|
this.width = opt.width;
|
|
9191
9269
|
this.height = opt.height;
|
|
@@ -9197,7 +9275,7 @@ class SVGRenderer {
|
|
|
9197
9275
|
color.alpha *= style.opacity;
|
|
9198
9276
|
this.#backgroundColor = color;
|
|
9199
9277
|
}
|
|
9200
|
-
drawPolygons(features) {
|
|
9278
|
+
drawPolygons(id, features) {
|
|
9201
9279
|
if (features.length === 0)
|
|
9202
9280
|
return;
|
|
9203
9281
|
const groups = new Map();
|
|
@@ -9221,12 +9299,14 @@ class SVGRenderer {
|
|
|
9221
9299
|
group.segments.push(ring.map((p) => roundXY(p.x, p.y)));
|
|
9222
9300
|
});
|
|
9223
9301
|
});
|
|
9302
|
+
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
9224
9303
|
for (const { segments, attrs } of groups.values()) {
|
|
9225
9304
|
const d = segmentsToPath(segments, true);
|
|
9226
9305
|
this.#svg.push(`<path d="${d}" ${attrs} />`);
|
|
9227
9306
|
}
|
|
9307
|
+
this.#svg.push('</g>');
|
|
9228
9308
|
}
|
|
9229
|
-
drawLineStrings(features) {
|
|
9309
|
+
drawLineStrings(id, features) {
|
|
9230
9310
|
if (features.length === 0)
|
|
9231
9311
|
return;
|
|
9232
9312
|
const groups = new Map();
|
|
@@ -9275,13 +9355,15 @@ class SVGRenderer {
|
|
|
9275
9355
|
group.segments.push(line.map((p) => roundXY(p.x, p.y)));
|
|
9276
9356
|
});
|
|
9277
9357
|
});
|
|
9358
|
+
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
9278
9359
|
for (const { segments, attrs } of groups.values()) {
|
|
9279
9360
|
const chains = chainSegments(segments);
|
|
9280
9361
|
const d = segmentsToPath(chains);
|
|
9281
9362
|
this.#svg.push(`<path d="${d}" ${attrs} />`);
|
|
9282
9363
|
}
|
|
9364
|
+
this.#svg.push('</g>');
|
|
9283
9365
|
}
|
|
9284
|
-
drawCircles(features) {
|
|
9366
|
+
drawCircles(id, features) {
|
|
9285
9367
|
if (features.length === 0)
|
|
9286
9368
|
return;
|
|
9287
9369
|
const groups = new Map();
|
|
@@ -9313,13 +9395,183 @@ class SVGRenderer {
|
|
|
9313
9395
|
group.points.push(roundXY(p.x, p.y));
|
|
9314
9396
|
});
|
|
9315
9397
|
});
|
|
9398
|
+
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
9316
9399
|
for (const { points, attrs } of groups.values()) {
|
|
9317
9400
|
for (const [x, y] of points) {
|
|
9318
9401
|
this.#svg.push(`<circle cx="${formatNum(x)}" cy="${formatNum(y)}" ${attrs} />`);
|
|
9319
9402
|
}
|
|
9320
9403
|
}
|
|
9404
|
+
this.#svg.push('</g>');
|
|
9405
|
+
}
|
|
9406
|
+
drawLabels(id, features) {
|
|
9407
|
+
if (features.length === 0)
|
|
9408
|
+
return;
|
|
9409
|
+
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
9410
|
+
for (const [feature, style] of features) {
|
|
9411
|
+
if (style.opacity <= 0 || !style.text)
|
|
9412
|
+
continue;
|
|
9413
|
+
const color = new Color(style.color);
|
|
9414
|
+
if (color.alpha <= 0)
|
|
9415
|
+
continue;
|
|
9416
|
+
const ring = feature.geometry[0];
|
|
9417
|
+
if (!ring || ring.length === 0)
|
|
9418
|
+
continue;
|
|
9419
|
+
const point = ring[Math.floor(ring.length / 2)];
|
|
9420
|
+
const [px, py] = roundXY(point.x, point.y);
|
|
9421
|
+
const fontSize = formatScaled(style.size);
|
|
9422
|
+
const fontFamily = style.font.join(', ') + ', Helvetica, Arial, sans-serif';
|
|
9423
|
+
const [svgAnchor, baseline] = mapTextAnchor(style.anchor);
|
|
9424
|
+
const offsetX = style.offset[0] * style.size;
|
|
9425
|
+
const offsetY = style.offset[1] * style.size;
|
|
9426
|
+
const [dx, dy] = roundXY(offsetX, offsetY);
|
|
9427
|
+
const attrs = [
|
|
9428
|
+
`x="${formatNum(px)}"`,
|
|
9429
|
+
`y="${formatNum(py)}"`,
|
|
9430
|
+
`font-family="${escapeXml(fontFamily)}"`,
|
|
9431
|
+
`font-size="${fontSize}"`,
|
|
9432
|
+
`text-anchor="${svgAnchor}"`,
|
|
9433
|
+
`dominant-baseline="${baseline}"`,
|
|
9434
|
+
];
|
|
9435
|
+
if (dx !== 0)
|
|
9436
|
+
attrs.push(`dx="${formatNum(dx)}"`);
|
|
9437
|
+
if (dy !== 0)
|
|
9438
|
+
attrs.push(`dy="${formatNum(dy)}"`);
|
|
9439
|
+
if (style.rotate !== 0) {
|
|
9440
|
+
attrs.push(`transform="rotate(${String(style.rotate)},${formatNum(px)},${formatNum(py)})"`);
|
|
9441
|
+
}
|
|
9442
|
+
const haloColor = new Color(style.haloColor);
|
|
9443
|
+
if (style.haloWidth > 0 && haloColor.alpha > 0) {
|
|
9444
|
+
const haloWidth = formatScaled(style.haloWidth);
|
|
9445
|
+
attrs.push('paint-order="stroke fill"', `stroke="${haloColor.rgb}"`, `stroke-width="${haloWidth}"`, 'stroke-linejoin="round"');
|
|
9446
|
+
if (haloColor.alpha < 255)
|
|
9447
|
+
attrs.push(`stroke-opacity="${haloColor.opacity.toFixed(3)}"`);
|
|
9448
|
+
}
|
|
9449
|
+
attrs.push(fillAttr(color));
|
|
9450
|
+
if (style.opacity < 1)
|
|
9451
|
+
attrs.push(`opacity="${style.opacity.toFixed(3)}"`);
|
|
9452
|
+
this.#svg.push(`<text ${attrs.join(' ')}>${escapeXml(style.text)}</text>`);
|
|
9453
|
+
}
|
|
9454
|
+
this.#svg.push('</g>');
|
|
9321
9455
|
}
|
|
9322
|
-
|
|
9456
|
+
drawIcons(id, features, spriteAtlas) {
|
|
9457
|
+
if (features.length === 0)
|
|
9458
|
+
return;
|
|
9459
|
+
const elements = [];
|
|
9460
|
+
for (const [feature, style] of features) {
|
|
9461
|
+
if (style.opacity <= 0)
|
|
9462
|
+
continue;
|
|
9463
|
+
const sprite = spriteAtlas.get(style.image);
|
|
9464
|
+
if (!sprite)
|
|
9465
|
+
continue;
|
|
9466
|
+
const ring = feature.geometry[0];
|
|
9467
|
+
if (!ring || ring.length === 0)
|
|
9468
|
+
continue;
|
|
9469
|
+
const point = ring[Math.floor(ring.length / 2)];
|
|
9470
|
+
const scale = style.size / sprite.pixelRatio;
|
|
9471
|
+
const iconW = sprite.width * scale;
|
|
9472
|
+
const iconH = sprite.height * scale;
|
|
9473
|
+
const [anchorDx, anchorDy] = mapIconAnchor(style.anchor, iconW, iconH);
|
|
9474
|
+
const ox = style.offset[0] * style.size + anchorDx;
|
|
9475
|
+
const oy = style.offset[1] * style.size + anchorDy;
|
|
9476
|
+
const [iconXr, iconYr] = roundXY(point.x + ox, point.y + oy);
|
|
9477
|
+
// Register sprite sheet in global defs (once per unique data URI)
|
|
9478
|
+
const imgW = Math.round(sprite.sheetWidth * 10);
|
|
9479
|
+
const imgH = Math.round(sprite.sheetHeight * 10);
|
|
9480
|
+
const sheetKey = sprite.sheetDataUri;
|
|
9481
|
+
if (!this.#spriteSheetDefs.has(sheetKey)) {
|
|
9482
|
+
this.#spriteSheetDefs.set(sheetKey, {
|
|
9483
|
+
defId: `sprite-sheet-${String(this.#spriteSheetDefs.size)}`,
|
|
9484
|
+
width: imgW,
|
|
9485
|
+
height: imgH,
|
|
9486
|
+
href: sprite.sheetDataUri,
|
|
9487
|
+
});
|
|
9488
|
+
}
|
|
9489
|
+
const sheetDef = this.#spriteSheetDefs.get(sheetKey);
|
|
9490
|
+
// Register symbol for this sprite (once per sprite name + sheet)
|
|
9491
|
+
const sprX = Math.round(sprite.x * 10);
|
|
9492
|
+
const sprY = Math.round(sprite.y * 10);
|
|
9493
|
+
const sprW = Math.round(sprite.width * 10);
|
|
9494
|
+
const sprH = Math.round(sprite.height * 10);
|
|
9495
|
+
const symKey = `${style.image}\0${sheetKey}`;
|
|
9496
|
+
if (!this.#spriteSymbolDefs.has(symKey)) {
|
|
9497
|
+
this.#spriteSymbolDefs.set(symKey, {
|
|
9498
|
+
symbolId: `sprite-${escapeXml(style.image)}`,
|
|
9499
|
+
sheetDefId: sheetDef.defId,
|
|
9500
|
+
x: sprX,
|
|
9501
|
+
y: sprY,
|
|
9502
|
+
width: sprW,
|
|
9503
|
+
height: sprH,
|
|
9504
|
+
});
|
|
9505
|
+
}
|
|
9506
|
+
const symDef = this.#spriteSymbolDefs.get(symKey);
|
|
9507
|
+
// Build instance: translate to position, scale from native to desired size
|
|
9508
|
+
const scaleStr = scale === 1 ? '' : ` scale(${formatScale(scale)})`;
|
|
9509
|
+
const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
|
|
9510
|
+
// SDF filter for colorable icons
|
|
9511
|
+
let filterAttr = '';
|
|
9512
|
+
if (style.sdf) {
|
|
9513
|
+
const iconColor = new Color(style.color);
|
|
9514
|
+
const haloColor = new Color(style.haloColor);
|
|
9515
|
+
const hasHalo = style.haloWidth > 0 && haloColor.alpha > 0;
|
|
9516
|
+
const filterKey = hasHalo
|
|
9517
|
+
? `sdf\0${iconColor.hex}\0${haloColor.hex}\0${String(style.haloWidth)}`
|
|
9518
|
+
: `sdf\0${iconColor.hex}`;
|
|
9519
|
+
if (!this.#sdfFilterDefs.has(filterKey)) {
|
|
9520
|
+
const filterId = `sdf-${String(this.#sdfFilterDefs.size)}`;
|
|
9521
|
+
const iconFloodOpacity = iconColor.alpha < 255 ? ` flood-opacity="${iconColor.opacity.toFixed(3)}"` : '';
|
|
9522
|
+
let content;
|
|
9523
|
+
if (hasHalo) {
|
|
9524
|
+
const haloRadius = formatScale(style.haloWidth);
|
|
9525
|
+
const haloFloodOpacity = haloColor.alpha < 255 ? ` flood-opacity="${haloColor.opacity.toFixed(3)}"` : '';
|
|
9526
|
+
content =
|
|
9527
|
+
`<filter id="${filterId}" color-interpolation-filters="sRGB">` +
|
|
9528
|
+
// Threshold alpha at 0.75 (MapLibre SDF edge) to get sharp icon mask
|
|
9529
|
+
`<feComponentTransfer in="SourceGraphic" result="sharp"><feFuncA type="discrete" tableValues="0 0 0 1" /></feComponentTransfer>` +
|
|
9530
|
+
// Dilate sharp mask for halo
|
|
9531
|
+
`<feMorphology in="sharp" operator="dilate" radius="${haloRadius}" result="dilated" />` +
|
|
9532
|
+
`<feFlood flood-color="${haloColor.rgb}"${haloFloodOpacity} result="haloColor" />` +
|
|
9533
|
+
`<feComposite in="haloColor" in2="dilated" operator="in" result="halo" />` +
|
|
9534
|
+
// Color the sharp icon
|
|
9535
|
+
`<feFlood flood-color="${iconColor.rgb}"${iconFloodOpacity} result="iconColor" />` +
|
|
9536
|
+
`<feComposite in="iconColor" in2="sharp" operator="in" result="colored" />` +
|
|
9537
|
+
`<feComposite in="colored" in2="halo" operator="over" />` +
|
|
9538
|
+
`</filter>`;
|
|
9539
|
+
}
|
|
9540
|
+
else {
|
|
9541
|
+
content =
|
|
9542
|
+
`<filter id="${filterId}" x="0" y="0" width="1" height="1" color-interpolation-filters="sRGB">` +
|
|
9543
|
+
// Threshold alpha at 0.75 (MapLibre SDF edge) to get sharp mask
|
|
9544
|
+
`<feComponentTransfer in="SourceGraphic" result="sharp"><feFuncA type="discrete" tableValues="0 0 0 1" /></feComponentTransfer>` +
|
|
9545
|
+
// Replace color while keeping sharp alpha
|
|
9546
|
+
`<feFlood flood-color="${iconColor.rgb}"${iconFloodOpacity} result="color" />` +
|
|
9547
|
+
`<feComposite in="color" in2="sharp" operator="in" />` +
|
|
9548
|
+
`</filter>`;
|
|
9549
|
+
}
|
|
9550
|
+
this.#sdfFilterDefs.set(filterKey, { filterId, content });
|
|
9551
|
+
}
|
|
9552
|
+
const { filterId } = this.#sdfFilterDefs.get(filterKey);
|
|
9553
|
+
filterAttr = ` filter="url(#${filterId})"`;
|
|
9554
|
+
}
|
|
9555
|
+
if (style.rotate !== 0) {
|
|
9556
|
+
const [cx, cy] = roundXY(point.x + style.offset[0] * style.size, point.y + style.offset[1] * style.size);
|
|
9557
|
+
elements.push(`<g transform="rotate(${String(style.rotate)},${formatNum(cx)},${formatNum(cy)})">` +
|
|
9558
|
+
`<g transform="translate(${formatNum(iconXr)},${formatNum(iconYr)})${scaleStr}"${opacityAttr}${filterAttr}>` +
|
|
9559
|
+
`<use href="#${escapeXml(symDef.symbolId)}" />` +
|
|
9560
|
+
`</g></g>`);
|
|
9561
|
+
}
|
|
9562
|
+
else {
|
|
9563
|
+
elements.push(`<g transform="translate(${formatNum(iconXr)},${formatNum(iconYr)})${scaleStr}"${opacityAttr}${filterAttr}>` +
|
|
9564
|
+
`<use href="#${escapeXml(symDef.symbolId)}" />` +
|
|
9565
|
+
`</g>`);
|
|
9566
|
+
}
|
|
9567
|
+
}
|
|
9568
|
+
if (elements.length === 0)
|
|
9569
|
+
return;
|
|
9570
|
+
this.#svg.push(`<g id="${escapeXml(id)}">`);
|
|
9571
|
+
this.#svg.push(...elements);
|
|
9572
|
+
this.#svg.push('</g>');
|
|
9573
|
+
}
|
|
9574
|
+
drawRasterTiles(id, tiles, style) {
|
|
9323
9575
|
if (tiles.length === 0)
|
|
9324
9576
|
return;
|
|
9325
9577
|
if (style.opacity <= 0)
|
|
@@ -9335,7 +9587,7 @@ class SVGRenderer {
|
|
|
9335
9587
|
const brightness = (style.brightnessMin + style.brightnessMax) / 2;
|
|
9336
9588
|
filters.push(`brightness(${String(brightness)})`);
|
|
9337
9589
|
}
|
|
9338
|
-
let gAttrs = `opacity="${String(style.opacity)}"`;
|
|
9590
|
+
let gAttrs = `id="${escapeXml(id)}" opacity="${String(style.opacity)}"`;
|
|
9339
9591
|
if (filters.length > 0)
|
|
9340
9592
|
gAttrs += ` filter="${filters.join(' ')}"`;
|
|
9341
9593
|
this.#svg.push(`<g ${gAttrs}>`);
|
|
@@ -9352,9 +9604,21 @@ class SVGRenderer {
|
|
|
9352
9604
|
getString() {
|
|
9353
9605
|
const w = this.width.toFixed(0);
|
|
9354
9606
|
const h = this.height.toFixed(0);
|
|
9607
|
+
// Build defs content
|
|
9608
|
+
const defsContent = [`<clipPath id="vb"><rect width="${w}" height="${h}"/></clipPath>`];
|
|
9609
|
+
for (const sheet of this.#spriteSheetDefs.values()) {
|
|
9610
|
+
defsContent.push(`<image id="${escapeXml(sheet.defId)}" width="${formatNum(sheet.width)}" height="${formatNum(sheet.height)}" href="${escapeXml(sheet.href)}" />`);
|
|
9611
|
+
}
|
|
9612
|
+
for (const sym of this.#spriteSymbolDefs.values()) {
|
|
9613
|
+
const clipId = `${sym.symbolId}-clip`;
|
|
9614
|
+
defsContent.push(`<clipPath id="${escapeXml(clipId)}"><rect width="${formatNum(sym.width)}" height="${formatNum(sym.height)}" /></clipPath>`, `<symbol id="${escapeXml(sym.symbolId)}"><g clip-path="url(#${escapeXml(clipId)})"><use href="#${escapeXml(sym.sheetDefId)}" x="${formatNum(-sym.x)}" y="${formatNum(-sym.y)}" /></g></symbol>`);
|
|
9615
|
+
}
|
|
9616
|
+
for (const { content } of this.#sdfFilterDefs.values()) {
|
|
9617
|
+
defsContent.push(content);
|
|
9618
|
+
}
|
|
9355
9619
|
const parts = [
|
|
9356
9620
|
`<svg viewBox="0 0 ${w} ${h}" width="${w}" height="${h}" xmlns="http://www.w3.org/2000/svg">`,
|
|
9357
|
-
`<defs
|
|
9621
|
+
`<defs>\n ${defsContent.join('\n ')}\n</defs>`,
|
|
9358
9622
|
`<g clip-path="url(#vb)">`,
|
|
9359
9623
|
];
|
|
9360
9624
|
if (this.#backgroundColor.alpha > 0) {
|
|
@@ -9379,6 +9643,9 @@ function strokeAttr(color, width) {
|
|
|
9379
9643
|
function formatScaled(v) {
|
|
9380
9644
|
return formatNum(Math.round(v * 10));
|
|
9381
9645
|
}
|
|
9646
|
+
function formatScale(v) {
|
|
9647
|
+
return (Math.round(v * 10000) / 10000).toString();
|
|
9648
|
+
}
|
|
9382
9649
|
function roundXY(x, y) {
|
|
9383
9650
|
return [Math.round(x * 10), Math.round(y * 10)];
|
|
9384
9651
|
}
|
|
@@ -9386,6 +9653,57 @@ function formatPoint(p) {
|
|
|
9386
9653
|
const [x, y] = roundXY(p[0], p[1]);
|
|
9387
9654
|
return formatNum(x) + ',' + formatNum(y);
|
|
9388
9655
|
}
|
|
9656
|
+
function mapTextAnchor(anchor) {
|
|
9657
|
+
switch (anchor) {
|
|
9658
|
+
case 'left':
|
|
9659
|
+
return ['start', 'central'];
|
|
9660
|
+
case 'right':
|
|
9661
|
+
return ['end', 'central'];
|
|
9662
|
+
case 'top':
|
|
9663
|
+
return ['middle', 'text-before-edge'];
|
|
9664
|
+
case 'bottom':
|
|
9665
|
+
return ['middle', 'text-after-edge'];
|
|
9666
|
+
case 'top-left':
|
|
9667
|
+
return ['start', 'text-before-edge'];
|
|
9668
|
+
case 'top-right':
|
|
9669
|
+
return ['end', 'text-before-edge'];
|
|
9670
|
+
case 'bottom-left':
|
|
9671
|
+
return ['start', 'text-after-edge'];
|
|
9672
|
+
case 'bottom-right':
|
|
9673
|
+
return ['end', 'text-after-edge'];
|
|
9674
|
+
default:
|
|
9675
|
+
return ['middle', 'central'];
|
|
9676
|
+
}
|
|
9677
|
+
}
|
|
9678
|
+
function mapIconAnchor(anchor, w, h) {
|
|
9679
|
+
switch (anchor) {
|
|
9680
|
+
case 'left':
|
|
9681
|
+
return [0, -h / 2];
|
|
9682
|
+
case 'right':
|
|
9683
|
+
return [-w, -h / 2];
|
|
9684
|
+
case 'top':
|
|
9685
|
+
return [-w / 2, 0];
|
|
9686
|
+
case 'bottom':
|
|
9687
|
+
return [-w / 2, -h];
|
|
9688
|
+
case 'top-left':
|
|
9689
|
+
return [0, 0];
|
|
9690
|
+
case 'top-right':
|
|
9691
|
+
return [-w, 0];
|
|
9692
|
+
case 'bottom-left':
|
|
9693
|
+
return [0, -h];
|
|
9694
|
+
case 'bottom-right':
|
|
9695
|
+
return [-w, -h];
|
|
9696
|
+
default:
|
|
9697
|
+
return [-w / 2, -h / 2];
|
|
9698
|
+
}
|
|
9699
|
+
}
|
|
9700
|
+
function escapeXml(s) {
|
|
9701
|
+
return s
|
|
9702
|
+
.replace(/&/g, '&')
|
|
9703
|
+
.replace(/</g, '<')
|
|
9704
|
+
.replace(/>/g, '>')
|
|
9705
|
+
.replace(/"/g, '"');
|
|
9706
|
+
}
|
|
9389
9707
|
|
|
9390
9708
|
/*
|
|
9391
9709
|
* bignumber.js v9.3.1
|
|
@@ -15790,14 +16108,29 @@ async function loadVectorSource(source, job, layerFeatures) {
|
|
|
15790
16108
|
list = features.polygons;
|
|
15791
16109
|
break;
|
|
15792
16110
|
}
|
|
15793
|
-
|
|
15794
|
-
|
|
15795
|
-
geometry
|
|
15796
|
-
|
|
15797
|
-
|
|
15798
|
-
|
|
15799
|
-
|
|
15800
|
-
|
|
16111
|
+
// Split MultiPoint into individual Point features
|
|
16112
|
+
if (type === 'Point' && geometry.length > 1) {
|
|
16113
|
+
for (const ring of geometry) {
|
|
16114
|
+
const feature = new Feature({
|
|
16115
|
+
type,
|
|
16116
|
+
geometry: [ring],
|
|
16117
|
+
id: featureSrc.id,
|
|
16118
|
+
properties: featureSrc.properties,
|
|
16119
|
+
});
|
|
16120
|
+
if (feature.doesOverlap([0, 0, width, height]))
|
|
16121
|
+
list.push(feature);
|
|
16122
|
+
}
|
|
16123
|
+
}
|
|
16124
|
+
else {
|
|
16125
|
+
const feature = new Feature({
|
|
16126
|
+
type,
|
|
16127
|
+
geometry,
|
|
16128
|
+
id: featureSrc.id,
|
|
16129
|
+
properties: featureSrc.properties,
|
|
16130
|
+
});
|
|
16131
|
+
if (feature.doesOverlap([0, 0, width, height]))
|
|
16132
|
+
list.push(feature);
|
|
16133
|
+
}
|
|
15801
16134
|
}
|
|
15802
16135
|
}
|
|
15803
16136
|
}));
|
|
@@ -15934,6 +16267,73 @@ async function getRasterTiles(job, sourceName) {
|
|
|
15934
16267
|
return rasterTiles.filter((tile) => tile !== null);
|
|
15935
16268
|
}
|
|
15936
16269
|
|
|
16270
|
+
async function fetchSpritePair(url) {
|
|
16271
|
+
const [jsonResponse, imageResponse] = await Promise.all([
|
|
16272
|
+
fetch(`${url}.json`),
|
|
16273
|
+
fetch(`${url}.png`),
|
|
16274
|
+
]);
|
|
16275
|
+
if (jsonResponse.ok && imageResponse.ok)
|
|
16276
|
+
return { jsonResponse, imageResponse };
|
|
16277
|
+
}
|
|
16278
|
+
async function loadSpriteAtlas(style) {
|
|
16279
|
+
const atlas = new Map();
|
|
16280
|
+
const sprite = style.sprite;
|
|
16281
|
+
if (!sprite)
|
|
16282
|
+
return atlas;
|
|
16283
|
+
const sources = [];
|
|
16284
|
+
if (typeof sprite === 'string') {
|
|
16285
|
+
sources.push({ id: 'default', url: sprite });
|
|
16286
|
+
}
|
|
16287
|
+
else if (Array.isArray(sprite)) {
|
|
16288
|
+
for (const s of sprite) {
|
|
16289
|
+
sources.push({
|
|
16290
|
+
id: s.id,
|
|
16291
|
+
url: s.url,
|
|
16292
|
+
});
|
|
16293
|
+
}
|
|
16294
|
+
}
|
|
16295
|
+
await Promise.all(sources.map(async ({ id, url }) => {
|
|
16296
|
+
try {
|
|
16297
|
+
// Try @2x retina sprites first, fall back to 1x
|
|
16298
|
+
const spritePair = (await fetchSpritePair(`${url}@2x`)) ?? (await fetchSpritePair(url));
|
|
16299
|
+
if (!spritePair)
|
|
16300
|
+
return;
|
|
16301
|
+
const { jsonResponse, imageResponse } = spritePair;
|
|
16302
|
+
const json = (await jsonResponse.json());
|
|
16303
|
+
const imageBuffer = await imageResponse.arrayBuffer();
|
|
16304
|
+
const base64 = typeof Buffer !== 'undefined'
|
|
16305
|
+
? Buffer.from(imageBuffer).toString('base64')
|
|
16306
|
+
: btoa(String.fromCharCode(...new Uint8Array(imageBuffer)));
|
|
16307
|
+
const sheetDataUri = `data:image/png;base64,${base64}`;
|
|
16308
|
+
// Estimate sheet dimensions from sprite entries
|
|
16309
|
+
let sheetWidth = 0;
|
|
16310
|
+
let sheetHeight = 0;
|
|
16311
|
+
for (const entry of Object.values(json)) {
|
|
16312
|
+
sheetWidth = Math.max(sheetWidth, entry.x + entry.width);
|
|
16313
|
+
sheetHeight = Math.max(sheetHeight, entry.y + entry.height);
|
|
16314
|
+
}
|
|
16315
|
+
const prefix = id === 'default' ? '' : `${id}:`;
|
|
16316
|
+
for (const [name, entry] of Object.entries(json)) {
|
|
16317
|
+
atlas.set(`${prefix}${name}`, {
|
|
16318
|
+
width: entry.width,
|
|
16319
|
+
height: entry.height,
|
|
16320
|
+
x: entry.x,
|
|
16321
|
+
y: entry.y,
|
|
16322
|
+
pixelRatio: entry.pixelRatio ?? 1,
|
|
16323
|
+
sdf: entry.sdf ?? false,
|
|
16324
|
+
sheetDataUri,
|
|
16325
|
+
sheetWidth,
|
|
16326
|
+
sheetHeight,
|
|
16327
|
+
});
|
|
16328
|
+
}
|
|
16329
|
+
}
|
|
16330
|
+
catch {
|
|
16331
|
+
// Silently skip failed sprite loads
|
|
16332
|
+
}
|
|
16333
|
+
}));
|
|
16334
|
+
return atlas;
|
|
16335
|
+
}
|
|
16336
|
+
|
|
15937
16337
|
async function getLayerFeatures(job) {
|
|
15938
16338
|
const { width, height } = job.renderer;
|
|
15939
16339
|
const { zoom, center } = job.view;
|
|
@@ -16094,6 +16494,12 @@ function getLayerStyles(layers) {
|
|
|
16094
16494
|
return layers.map(createStyleLayer);
|
|
16095
16495
|
}
|
|
16096
16496
|
|
|
16497
|
+
function resolveTokens(text, properties) {
|
|
16498
|
+
return text.replace(/\{([^}]+)\}/g, (_, key) => {
|
|
16499
|
+
const value = properties[key];
|
|
16500
|
+
return value != null ? String(value) : '';
|
|
16501
|
+
});
|
|
16502
|
+
}
|
|
16097
16503
|
async function renderMap(job) {
|
|
16098
16504
|
await render(job);
|
|
16099
16505
|
return job.renderer.getString();
|
|
@@ -16104,9 +16510,12 @@ function getFeatures(layerFeatures, layerStyle) {
|
|
|
16104
16510
|
async function render(job) {
|
|
16105
16511
|
const { renderer } = job;
|
|
16106
16512
|
const { zoom } = job.view;
|
|
16107
|
-
const layerFeatures = await
|
|
16513
|
+
const [layerFeatures, spriteAtlas] = await Promise.all([
|
|
16514
|
+
getLayerFeatures(job),
|
|
16515
|
+
job.renderLabels ? loadSpriteAtlas(job.style) : Promise.resolve(new Map()),
|
|
16516
|
+
]);
|
|
16108
16517
|
const layerStyles = getLayerStyles(job.style.layers);
|
|
16109
|
-
const availableImages = [];
|
|
16518
|
+
const availableImages = [...spriteAtlas.keys()];
|
|
16110
16519
|
const featureState = {};
|
|
16111
16520
|
for (const layerStyle of layerStyles) {
|
|
16112
16521
|
if (layerStyle.isHidden(zoom))
|
|
@@ -16127,6 +16536,7 @@ async function render(job) {
|
|
|
16127
16536
|
function getLayout(key, feature) {
|
|
16128
16537
|
return getStyleValue(layerStyle.layout, key, feature);
|
|
16129
16538
|
}
|
|
16539
|
+
const layerId = layerStyle.id;
|
|
16130
16540
|
switch (layerStyle.type) {
|
|
16131
16541
|
case 'background':
|
|
16132
16542
|
{
|
|
@@ -16146,7 +16556,7 @@ async function render(job) {
|
|
|
16146
16556
|
: polygons;
|
|
16147
16557
|
if (polygonFeatures.length === 0)
|
|
16148
16558
|
continue;
|
|
16149
|
-
renderer.drawPolygons(polygonFeatures.map((feature) => [
|
|
16559
|
+
renderer.drawPolygons(layerId, polygonFeatures.map((feature) => [
|
|
16150
16560
|
feature,
|
|
16151
16561
|
{
|
|
16152
16562
|
color: getPaint('fill-color', feature),
|
|
@@ -16166,7 +16576,7 @@ async function render(job) {
|
|
|
16166
16576
|
: lineStrings;
|
|
16167
16577
|
if (lineStringFeatures.length === 0)
|
|
16168
16578
|
continue;
|
|
16169
|
-
renderer.drawLineStrings(lineStringFeatures.map((feature) => [
|
|
16579
|
+
renderer.drawLineStrings(layerId, lineStringFeatures.map((feature) => [
|
|
16170
16580
|
feature,
|
|
16171
16581
|
{
|
|
16172
16582
|
color: getPaint('line-color', feature),
|
|
@@ -16185,7 +16595,7 @@ async function render(job) {
|
|
|
16185
16595
|
case 'raster':
|
|
16186
16596
|
{
|
|
16187
16597
|
const tiles = await getRasterTiles(job, layerStyle.source);
|
|
16188
|
-
renderer.drawRasterTiles(tiles, {
|
|
16598
|
+
renderer.drawRasterTiles(layerId, tiles, {
|
|
16189
16599
|
opacity: getPaint('raster-opacity'),
|
|
16190
16600
|
hueRotate: getPaint('raster-hue-rotate'),
|
|
16191
16601
|
brightnessMin: getPaint('raster-brightness-min'),
|
|
@@ -16206,7 +16616,7 @@ async function render(job) {
|
|
|
16206
16616
|
: points;
|
|
16207
16617
|
if (pointFeatures.length === 0)
|
|
16208
16618
|
continue;
|
|
16209
|
-
renderer.drawCircles(pointFeatures.map((feature) => [
|
|
16619
|
+
renderer.drawCircles(layerId, pointFeatures.map((feature) => [
|
|
16210
16620
|
feature,
|
|
16211
16621
|
{
|
|
16212
16622
|
color: getPaint('circle-color', feature),
|
|
@@ -16219,11 +16629,81 @@ async function render(job) {
|
|
|
16219
16629
|
]));
|
|
16220
16630
|
}
|
|
16221
16631
|
continue;
|
|
16632
|
+
case 'symbol':
|
|
16633
|
+
{
|
|
16634
|
+
if (!job.renderLabels)
|
|
16635
|
+
continue;
|
|
16636
|
+
const features = getFeatures(layerFeatures, layerStyle);
|
|
16637
|
+
const allFeatures = [
|
|
16638
|
+
...(features?.points ?? []),
|
|
16639
|
+
...(features?.linestrings ?? []),
|
|
16640
|
+
...(features?.polygons ?? []),
|
|
16641
|
+
];
|
|
16642
|
+
if (allFeatures.length === 0)
|
|
16643
|
+
continue;
|
|
16644
|
+
const symbolFeatures = layerStyle.filterFn
|
|
16645
|
+
? allFeatures.filter((feature) => layerStyle.filterFn.filter({ zoom }, feature))
|
|
16646
|
+
: allFeatures;
|
|
16647
|
+
if (symbolFeatures.length === 0)
|
|
16648
|
+
continue;
|
|
16649
|
+
// Render icons first (underneath text)
|
|
16650
|
+
renderer.drawIcons(`${layerId}-icons`, symbolFeatures.flatMap((feature) => {
|
|
16651
|
+
const iconImage = getLayout('icon-image', feature);
|
|
16652
|
+
const iconName = iconImage != null
|
|
16653
|
+
? resolveTokens(iconImage.toString(), feature.properties)
|
|
16654
|
+
: '';
|
|
16655
|
+
if (!iconName || !spriteAtlas.has(iconName))
|
|
16656
|
+
return [];
|
|
16657
|
+
const spriteEntry = spriteAtlas.get(iconName);
|
|
16658
|
+
return [
|
|
16659
|
+
[
|
|
16660
|
+
feature,
|
|
16661
|
+
{
|
|
16662
|
+
image: iconName,
|
|
16663
|
+
size: getLayout('icon-size', feature),
|
|
16664
|
+
anchor: getLayout('icon-anchor', feature),
|
|
16665
|
+
offset: getLayout('icon-offset', feature),
|
|
16666
|
+
rotate: getLayout('icon-rotate', feature),
|
|
16667
|
+
opacity: getPaint('icon-opacity', feature),
|
|
16668
|
+
sdf: spriteEntry.sdf,
|
|
16669
|
+
color: getPaint('icon-color', feature),
|
|
16670
|
+
haloColor: getPaint('icon-halo-color', feature),
|
|
16671
|
+
haloWidth: getPaint('icon-halo-width', feature),
|
|
16672
|
+
},
|
|
16673
|
+
],
|
|
16674
|
+
];
|
|
16675
|
+
}), spriteAtlas);
|
|
16676
|
+
// Render text labels on top
|
|
16677
|
+
renderer.drawLabels(`${layerId}-labels`, symbolFeatures.flatMap((feature) => {
|
|
16678
|
+
const textField = getLayout('text-field', feature);
|
|
16679
|
+
const textRaw = textField != null ? textField.toString() : '';
|
|
16680
|
+
const text = resolveTokens(textRaw, feature.properties);
|
|
16681
|
+
if (!text)
|
|
16682
|
+
return [];
|
|
16683
|
+
return [
|
|
16684
|
+
[
|
|
16685
|
+
feature,
|
|
16686
|
+
{
|
|
16687
|
+
text,
|
|
16688
|
+
size: getLayout('text-size', feature),
|
|
16689
|
+
font: getLayout('text-font', feature),
|
|
16690
|
+
anchor: getLayout('text-anchor', feature),
|
|
16691
|
+
offset: getLayout('text-offset', feature),
|
|
16692
|
+
rotate: getLayout('text-rotate', feature),
|
|
16693
|
+
color: getPaint('text-color', feature),
|
|
16694
|
+
opacity: getPaint('text-opacity', feature),
|
|
16695
|
+
haloColor: getPaint('text-halo-color', feature),
|
|
16696
|
+
haloWidth: getPaint('text-halo-width', feature),
|
|
16697
|
+
},
|
|
16698
|
+
],
|
|
16699
|
+
];
|
|
16700
|
+
}));
|
|
16701
|
+
}
|
|
16702
|
+
continue;
|
|
16222
16703
|
case 'color-relief':
|
|
16223
16704
|
case 'fill-extrusion':
|
|
16224
16705
|
case 'heatmap':
|
|
16225
16706
|
case 'hillshade':
|
|
16226
|
-
case 'symbol':
|
|
16227
16707
|
continue;
|
|
16228
16708
|
default:
|
|
16229
16709
|
throw Error('layerStyle.type: ' + String(layerStyle.type));
|
|
@@ -16245,6 +16725,7 @@ async function renderToSVG(options) {
|
|
|
16245
16725
|
center: [options.lon ?? 0, options.lat ?? 0],
|
|
16246
16726
|
zoom: options.zoom ?? 2,
|
|
16247
16727
|
},
|
|
16728
|
+
renderLabels: options.renderLabels ?? false,
|
|
16248
16729
|
});
|
|
16249
16730
|
}
|
|
16250
16731
|
|