@versatiles/svg-renderer 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/maplibre.cjs CHANGED
@@ -65,6 +65,10 @@ 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
73
  grid-template-columns: 1fr 1fr;
70
74
  gap: 8px;
@@ -72,11 +76,15 @@ const PANEL_CSS = `
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
- font-size: 12px;
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],
@@ -9373,7 +9455,7 @@ class SVGRenderer {
9373
9455
  color.alpha *= style.opacity;
9374
9456
  this.#backgroundColor = color;
9375
9457
  }
9376
- drawPolygons(features) {
9458
+ drawPolygons(id, features) {
9377
9459
  if (features.length === 0)
9378
9460
  return;
9379
9461
  const groups = new Map();
@@ -9397,12 +9479,14 @@ class SVGRenderer {
9397
9479
  group.segments.push(ring.map((p) => roundXY(p.x, p.y)));
9398
9480
  });
9399
9481
  });
9482
+ this.#svg.push(`<g id="${escapeXml(id)}">`);
9400
9483
  for (const { segments, attrs } of groups.values()) {
9401
9484
  const d = segmentsToPath(segments, true);
9402
9485
  this.#svg.push(`<path d="${d}" ${attrs} />`);
9403
9486
  }
9487
+ this.#svg.push('</g>');
9404
9488
  }
9405
- drawLineStrings(features) {
9489
+ drawLineStrings(id, features) {
9406
9490
  if (features.length === 0)
9407
9491
  return;
9408
9492
  const groups = new Map();
@@ -9451,13 +9535,15 @@ class SVGRenderer {
9451
9535
  group.segments.push(line.map((p) => roundXY(p.x, p.y)));
9452
9536
  });
9453
9537
  });
9538
+ this.#svg.push(`<g id="${escapeXml(id)}">`);
9454
9539
  for (const { segments, attrs } of groups.values()) {
9455
9540
  const chains = chainSegments(segments);
9456
9541
  const d = segmentsToPath(chains);
9457
9542
  this.#svg.push(`<path d="${d}" ${attrs} />`);
9458
9543
  }
9544
+ this.#svg.push('</g>');
9459
9545
  }
9460
- drawCircles(features) {
9546
+ drawCircles(id, features) {
9461
9547
  if (features.length === 0)
9462
9548
  return;
9463
9549
  const groups = new Map();
@@ -9489,13 +9575,108 @@ class SVGRenderer {
9489
9575
  group.points.push(roundXY(p.x, p.y));
9490
9576
  });
9491
9577
  });
9578
+ this.#svg.push(`<g id="${escapeXml(id)}">`);
9492
9579
  for (const { points, attrs } of groups.values()) {
9493
9580
  for (const [x, y] of points) {
9494
9581
  this.#svg.push(`<circle cx="${formatNum(x)}" cy="${formatNum(y)}" ${attrs} />`);
9495
9582
  }
9496
9583
  }
9584
+ this.#svg.push('</g>');
9497
9585
  }
9498
- drawRasterTiles(tiles, style) {
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>');
9678
+ }
9679
+ drawRasterTiles(id, tiles, style) {
9499
9680
  if (tiles.length === 0)
9500
9681
  return;
9501
9682
  if (style.opacity <= 0)
@@ -9511,7 +9692,7 @@ class SVGRenderer {
9511
9692
  const brightness = (style.brightnessMin + style.brightnessMax) / 2;
9512
9693
  filters.push(`brightness(${String(brightness)})`);
9513
9694
  }
9514
- let gAttrs = `opacity="${String(style.opacity)}"`;
9695
+ let gAttrs = `id="${escapeXml(id)}" opacity="${String(style.opacity)}"`;
9515
9696
  if (filters.length > 0)
9516
9697
  gAttrs += ` filter="${filters.join(' ')}"`;
9517
9698
  this.#svg.push(`<g ${gAttrs}>`);
@@ -9562,6 +9743,57 @@ function formatPoint(p) {
9562
9743
  const [x, y] = roundXY(p[0], p[1]);
9563
9744
  return formatNum(x) + ',' + formatNum(y);
9564
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, '&amp;')
9793
+ .replace(/</g, '&lt;')
9794
+ .replace(/>/g, '&gt;')
9795
+ .replace(/"/g, '&quot;');
9796
+ }
9565
9797
 
9566
9798
  /*
9567
9799
  * bignumber.js v9.3.1
@@ -16110,6 +16342,65 @@ async function getRasterTiles(job, sourceName) {
16110
16342
  return rasterTiles.filter((tile) => tile !== null);
16111
16343
  }
16112
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
+
16113
16404
  async function getLayerFeatures(job) {
16114
16405
  const { width, height } = job.renderer;
16115
16406
  const { zoom, center } = job.view;
@@ -16270,6 +16561,12 @@ function getLayerStyles(layers) {
16270
16561
  return layers.map(createStyleLayer);
16271
16562
  }
16272
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
+ }
16273
16570
  async function renderMap(job) {
16274
16571
  await render(job);
16275
16572
  return job.renderer.getString();
@@ -16280,9 +16577,12 @@ function getFeatures(layerFeatures, layerStyle) {
16280
16577
  async function render(job) {
16281
16578
  const { renderer } = job;
16282
16579
  const { zoom } = job.view;
16283
- const layerFeatures = await getLayerFeatures(job);
16580
+ const [layerFeatures, spriteAtlas] = await Promise.all([
16581
+ getLayerFeatures(job),
16582
+ job.renderLabels ? loadSpriteAtlas(job.style) : Promise.resolve(new Map()),
16583
+ ]);
16284
16584
  const layerStyles = getLayerStyles(job.style.layers);
16285
- const availableImages = [];
16585
+ const availableImages = [...spriteAtlas.keys()];
16286
16586
  const featureState = {};
16287
16587
  for (const layerStyle of layerStyles) {
16288
16588
  if (layerStyle.isHidden(zoom))
@@ -16303,6 +16603,7 @@ async function render(job) {
16303
16603
  function getLayout(key, feature) {
16304
16604
  return getStyleValue(layerStyle.layout, key, feature);
16305
16605
  }
16606
+ const layerId = layerStyle.id;
16306
16607
  switch (layerStyle.type) {
16307
16608
  case 'background':
16308
16609
  {
@@ -16322,7 +16623,7 @@ async function render(job) {
16322
16623
  : polygons;
16323
16624
  if (polygonFeatures.length === 0)
16324
16625
  continue;
16325
- renderer.drawPolygons(polygonFeatures.map((feature) => [
16626
+ renderer.drawPolygons(layerId, polygonFeatures.map((feature) => [
16326
16627
  feature,
16327
16628
  {
16328
16629
  color: getPaint('fill-color', feature),
@@ -16342,7 +16643,7 @@ async function render(job) {
16342
16643
  : lineStrings;
16343
16644
  if (lineStringFeatures.length === 0)
16344
16645
  continue;
16345
- renderer.drawLineStrings(lineStringFeatures.map((feature) => [
16646
+ renderer.drawLineStrings(layerId, lineStringFeatures.map((feature) => [
16346
16647
  feature,
16347
16648
  {
16348
16649
  color: getPaint('line-color', feature),
@@ -16361,7 +16662,7 @@ async function render(job) {
16361
16662
  case 'raster':
16362
16663
  {
16363
16664
  const tiles = await getRasterTiles(job, layerStyle.source);
16364
- renderer.drawRasterTiles(tiles, {
16665
+ renderer.drawRasterTiles(layerId, tiles, {
16365
16666
  opacity: getPaint('raster-opacity'),
16366
16667
  hueRotate: getPaint('raster-hue-rotate'),
16367
16668
  brightnessMin: getPaint('raster-brightness-min'),
@@ -16382,7 +16683,7 @@ async function render(job) {
16382
16683
  : points;
16383
16684
  if (pointFeatures.length === 0)
16384
16685
  continue;
16385
- renderer.drawCircles(pointFeatures.map((feature) => [
16686
+ renderer.drawCircles(layerId, pointFeatures.map((feature) => [
16386
16687
  feature,
16387
16688
  {
16388
16689
  color: getPaint('circle-color', feature),
@@ -16395,11 +16696,76 @@ async function render(job) {
16395
16696
  ]));
16396
16697
  }
16397
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;
16398
16765
  case 'color-relief':
16399
16766
  case 'fill-extrusion':
16400
16767
  case 'heatmap':
16401
16768
  case 'hillshade':
16402
- case 'symbol':
16403
16769
  continue;
16404
16770
  default:
16405
16771
  throw Error('layerStyle.type: ' + String(layerStyle.type));
@@ -16421,6 +16787,7 @@ async function renderToSVG(options) {
16421
16787
  center: [options.lon ?? 0, options.lat ?? 0],
16422
16788
  zoom: options.zoom ?? 2,
16423
16789
  },
16790
+ renderLabels: options.renderLabels ?? false,
16424
16791
  });
16425
16792
  }
16426
16793
 
@@ -16516,11 +16883,14 @@ class SVGExportControl {
16516
16883
  <div class="panel-notice">
16517
16884
  Note:<br>
16518
16885
  <span class="panel-attribution"></span><br>
16519
- Also export of symbols and texts is not supported yet, but you can improve me on <a href="https://github.com/versatiles-org/versatiles-svg-renderer" target="_blank" rel="noopener noreferrer">GitHub</a>.<br>
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>
16520
16887
  </div>
16521
16888
  <div class="panel-inputs">
16522
- <label>Width<input type="number" class="input-width" value="${String(this.options.defaultWidth)}" min="1" max="8192"></label>
16523
- <label>Height<input type="number" class="input-height" value="${String(this.options.defaultHeight)}" min="1" max="8192"></label>
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>
16524
16894
  </div>
16525
16895
  <div class="preview-container">
16526
16896
  <span class="preview-loading">Rendering preview\u2026</span>
@@ -16553,6 +16923,9 @@ class SVGExportControl {
16553
16923
  input.addEventListener('input', () => {
16554
16924
  this.schedulePreview();
16555
16925
  });
16926
+ input.addEventListener('change', () => {
16927
+ this.schedulePreview();
16928
+ });
16556
16929
  });
16557
16930
  querySelector(this.panel, '.btn-download').addEventListener('click', () => {
16558
16931
  this.downloadSVG();
@@ -16618,6 +16991,7 @@ class SVGExportControl {
16618
16991
  openBtn.disabled = true;
16619
16992
  const width = Number(querySelector(panel, '.input-width').value);
16620
16993
  const height = Number(querySelector(panel, '.input-height').value);
16994
+ const renderLabels = querySelector(panel, '.input-labels').checked;
16621
16995
  if (!width || !height || width < 1 || height < 1) {
16622
16996
  previewContainer.innerHTML = '<span class="preview-loading">Invalid input values</span>';
16623
16997
  return;
@@ -16633,6 +17007,7 @@ class SVGExportControl {
16633
17007
  lon: center.lng,
16634
17008
  lat: center.lat,
16635
17009
  zoom,
17010
+ renderLabels,
16636
17011
  });
16637
17012
  if (this.renderGeneration !== generation)
16638
17013
  return;