@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.
@@ -33,6 +33,7 @@ declare function renderToSVG(options: {
33
33
  lon?: number;
34
34
  lat?: number;
35
35
  zoom?: number;
36
+ renderLabels?: boolean;
36
37
  }): Promise<string>;
37
38
 
38
39
  export { SVGExportControl, renderToSVG };
package/dist/maplibre.js CHANGED
@@ -63,6 +63,10 @@ const PANEL_CSS = `
63
63
  }
64
64
 
65
65
  .svg-export-panel .panel-inputs {
66
+ margin-bottom: 12px;
67
+ }
68
+
69
+ .svg-export-panel .panel-inputs .grid {
66
70
  display: grid;
67
71
  grid-template-columns: 1fr 1fr;
68
72
  gap: 8px;
@@ -70,11 +74,15 @@ const PANEL_CSS = `
70
74
  }
71
75
 
72
76
  .svg-export-panel .panel-inputs label {
77
+ font-size: 12px;
78
+ color: #666;
79
+ }
80
+
81
+ .svg-export-panel .panel-inputs .grid label {
73
82
  display: flex;
74
83
  flex-direction: column;
75
84
  gap: 4px;
76
- font-size: 12px;
77
- color: #666;
85
+ width: 100%;
78
86
  }
79
87
 
80
88
  .svg-export-panel .panel-inputs input {
@@ -82,7 +90,6 @@ const PANEL_CSS = `
82
90
  border: 1px solid #ccc;
83
91
  border-radius: 4px;
84
92
  font-size: 13px;
85
- width: 100%;
86
93
  box-sizing: border-box;
87
94
  }
88
95
 
@@ -3000,6 +3007,23 @@ var paint_raster = {
3000
3007
  },
3001
3008
  "property-type": "data-constant"
3002
3009
  },
3010
+ resampling: {
3011
+ type: "enum",
3012
+ values: {
3013
+ linear: {
3014
+ },
3015
+ nearest: {
3016
+ }
3017
+ },
3018
+ "default": "linear",
3019
+ expression: {
3020
+ interpolated: false,
3021
+ parameters: [
3022
+ "zoom"
3023
+ ]
3024
+ },
3025
+ "property-type": "data-constant"
3026
+ },
3003
3027
  "raster-resampling": {
3004
3028
  type: "enum",
3005
3029
  values: {
@@ -3150,6 +3174,23 @@ var paint_hillshade = {
3150
3174
  ]
3151
3175
  },
3152
3176
  "property-type": "data-constant"
3177
+ },
3178
+ resampling: {
3179
+ type: "enum",
3180
+ values: {
3181
+ linear: {
3182
+ },
3183
+ nearest: {
3184
+ }
3185
+ },
3186
+ "default": "linear",
3187
+ expression: {
3188
+ interpolated: false,
3189
+ parameters: [
3190
+ "zoom"
3191
+ ]
3192
+ },
3193
+ "property-type": "data-constant"
3153
3194
  }
3154
3195
  };
3155
3196
  var paint_background = {
@@ -3570,6 +3611,23 @@ var v8Spec = {
3570
3611
  ]
3571
3612
  },
3572
3613
  "property-type": "color-ramp"
3614
+ },
3615
+ resampling: {
3616
+ type: "enum",
3617
+ values: {
3618
+ linear: {
3619
+ },
3620
+ nearest: {
3621
+ }
3622
+ },
3623
+ "default": "linear",
3624
+ expression: {
3625
+ interpolated: false,
3626
+ parameters: [
3627
+ "zoom"
3628
+ ]
3629
+ },
3630
+ "property-type": "data-constant"
3573
3631
  }
3574
3632
  },
3575
3633
  paint_background: paint_background,
@@ -6414,11 +6472,12 @@ class CollatorExpression {
6414
6472
  }
6415
6473
 
6416
6474
  class NumberFormat {
6417
- constructor(number, locale, currency, minFractionDigits, maxFractionDigits) {
6475
+ constructor(number, locale, currency, unit, minFractionDigits, maxFractionDigits) {
6418
6476
  this.type = StringType;
6419
6477
  this.number = number;
6420
6478
  this.locale = locale;
6421
6479
  this.currency = currency;
6480
+ this.unit = unit;
6422
6481
  this.minFractionDigits = minFractionDigits;
6423
6482
  this.maxFractionDigits = maxFractionDigits;
6424
6483
  }
@@ -6443,6 +6502,15 @@ class NumberFormat {
6443
6502
  if (!currency)
6444
6503
  return null;
6445
6504
  }
6505
+ let unit = null;
6506
+ if (options['unit']) {
6507
+ unit = context.parse(options['unit'], 1, StringType);
6508
+ if (!unit)
6509
+ return null;
6510
+ }
6511
+ if (currency && unit) {
6512
+ return context.error('NumberFormat options `currency` and `unit` are mutually exclusive');
6513
+ }
6446
6514
  let minFractionDigits = null;
6447
6515
  if (options['min-fraction-digits']) {
6448
6516
  minFractionDigits = context.parse(options['min-fraction-digits'], 1, NumberType);
@@ -6455,12 +6523,13 @@ class NumberFormat {
6455
6523
  if (!maxFractionDigits)
6456
6524
  return null;
6457
6525
  }
6458
- return new NumberFormat(number, locale, currency, minFractionDigits, maxFractionDigits);
6526
+ return new NumberFormat(number, locale, currency, unit, minFractionDigits, maxFractionDigits);
6459
6527
  }
6460
6528
  evaluate(ctx) {
6461
6529
  return new Intl.NumberFormat(this.locale ? this.locale.evaluate(ctx) : [], {
6462
- style: this.currency ? 'currency' : 'decimal',
6530
+ style: this.currency ? 'currency' : this.unit ? 'unit' : 'decimal',
6463
6531
  currency: this.currency ? this.currency.evaluate(ctx) : undefined,
6532
+ unit: this.unit ? this.unit.evaluate(ctx) : undefined,
6464
6533
  minimumFractionDigits: this.minFractionDigits
6465
6534
  ? this.minFractionDigits.evaluate(ctx)
6466
6535
  : undefined,
@@ -6477,6 +6546,9 @@ class NumberFormat {
6477
6546
  if (this.currency) {
6478
6547
  fn(this.currency);
6479
6548
  }
6549
+ if (this.unit) {
6550
+ fn(this.unit);
6551
+ }
6480
6552
  if (this.minFractionDigits) {
6481
6553
  fn(this.minFractionDigits);
6482
6554
  }
@@ -8215,6 +8287,16 @@ CompoundExpression.register(expressions$1, {
8215
8287
  varargs(ValueType),
8216
8288
  (ctx, args) => args.map((arg) => valueToString(arg.evaluate(ctx))).join('')
8217
8289
  ],
8290
+ split: [
8291
+ array(StringType),
8292
+ [StringType, StringType],
8293
+ (ctx, [s, delim]) => s.evaluate(ctx).split(delim.evaluate(ctx))
8294
+ ],
8295
+ join: [
8296
+ StringType,
8297
+ [array(StringType), StringType],
8298
+ (ctx, [arr, delim]) => arr.value.join(delim.evaluate(ctx))
8299
+ ],
8218
8300
  'resolved-locale': [
8219
8301
  StringType,
8220
8302
  [CollatorType],
@@ -9371,7 +9453,7 @@ class SVGRenderer {
9371
9453
  color.alpha *= style.opacity;
9372
9454
  this.#backgroundColor = color;
9373
9455
  }
9374
- drawPolygons(features) {
9456
+ drawPolygons(id, features) {
9375
9457
  if (features.length === 0)
9376
9458
  return;
9377
9459
  const groups = new Map();
@@ -9395,12 +9477,14 @@ class SVGRenderer {
9395
9477
  group.segments.push(ring.map((p) => roundXY(p.x, p.y)));
9396
9478
  });
9397
9479
  });
9480
+ this.#svg.push(`<g id="${escapeXml(id)}">`);
9398
9481
  for (const { segments, attrs } of groups.values()) {
9399
9482
  const d = segmentsToPath(segments, true);
9400
9483
  this.#svg.push(`<path d="${d}" ${attrs} />`);
9401
9484
  }
9485
+ this.#svg.push('</g>');
9402
9486
  }
9403
- drawLineStrings(features) {
9487
+ drawLineStrings(id, features) {
9404
9488
  if (features.length === 0)
9405
9489
  return;
9406
9490
  const groups = new Map();
@@ -9449,13 +9533,15 @@ class SVGRenderer {
9449
9533
  group.segments.push(line.map((p) => roundXY(p.x, p.y)));
9450
9534
  });
9451
9535
  });
9536
+ this.#svg.push(`<g id="${escapeXml(id)}">`);
9452
9537
  for (const { segments, attrs } of groups.values()) {
9453
9538
  const chains = chainSegments(segments);
9454
9539
  const d = segmentsToPath(chains);
9455
9540
  this.#svg.push(`<path d="${d}" ${attrs} />`);
9456
9541
  }
9542
+ this.#svg.push('</g>');
9457
9543
  }
9458
- drawCircles(features) {
9544
+ drawCircles(id, features) {
9459
9545
  if (features.length === 0)
9460
9546
  return;
9461
9547
  const groups = new Map();
@@ -9487,13 +9573,108 @@ class SVGRenderer {
9487
9573
  group.points.push(roundXY(p.x, p.y));
9488
9574
  });
9489
9575
  });
9576
+ this.#svg.push(`<g id="${escapeXml(id)}">`);
9490
9577
  for (const { points, attrs } of groups.values()) {
9491
9578
  for (const [x, y] of points) {
9492
9579
  this.#svg.push(`<circle cx="${formatNum(x)}" cy="${formatNum(y)}" ${attrs} />`);
9493
9580
  }
9494
9581
  }
9582
+ this.#svg.push('</g>');
9495
9583
  }
9496
- drawRasterTiles(tiles, style) {
9584
+ drawLabels(id, features) {
9585
+ if (features.length === 0)
9586
+ return;
9587
+ this.#svg.push(`<g id="${escapeXml(id)}">`);
9588
+ for (const [feature, style] of features) {
9589
+ if (style.opacity <= 0 || !style.text)
9590
+ continue;
9591
+ const color = new Color(style.color);
9592
+ if (color.alpha <= 0)
9593
+ continue;
9594
+ const ring = feature.geometry[0];
9595
+ if (!ring || ring.length === 0)
9596
+ continue;
9597
+ const point = ring[Math.floor(ring.length / 2)];
9598
+ const [px, py] = roundXY(point.x, point.y);
9599
+ const fontSize = formatScaled(style.size);
9600
+ const fontFamily = style.font.join(', ') + ', Helvetica, Arial, sans-serif';
9601
+ const [svgAnchor, baseline] = mapTextAnchor(style.anchor);
9602
+ const offsetX = style.offset[0] * style.size;
9603
+ const offsetY = style.offset[1] * style.size;
9604
+ const [dx, dy] = roundXY(offsetX, offsetY);
9605
+ const attrs = [
9606
+ `x="${formatNum(px)}"`,
9607
+ `y="${formatNum(py)}"`,
9608
+ `font-family="${escapeXml(fontFamily)}"`,
9609
+ `font-size="${fontSize}"`,
9610
+ `text-anchor="${svgAnchor}"`,
9611
+ `dominant-baseline="${baseline}"`,
9612
+ ];
9613
+ if (dx !== 0)
9614
+ attrs.push(`dx="${formatNum(dx)}"`);
9615
+ if (dy !== 0)
9616
+ attrs.push(`dy="${formatNum(dy)}"`);
9617
+ if (style.rotate !== 0) {
9618
+ attrs.push(`transform="rotate(${String(style.rotate)},${formatNum(px)},${formatNum(py)})"`);
9619
+ }
9620
+ const haloColor = new Color(style.haloColor);
9621
+ if (style.haloWidth > 0 && haloColor.alpha > 0) {
9622
+ const haloWidth = formatScaled(style.haloWidth);
9623
+ attrs.push('paint-order="stroke fill"', `stroke="${haloColor.rgb}"`, `stroke-width="${haloWidth}"`, 'stroke-linejoin="round"');
9624
+ if (haloColor.alpha < 255)
9625
+ attrs.push(`stroke-opacity="${haloColor.opacity.toFixed(3)}"`);
9626
+ }
9627
+ attrs.push(fillAttr(color));
9628
+ if (style.opacity < 1)
9629
+ attrs.push(`opacity="${style.opacity.toFixed(3)}"`);
9630
+ this.#svg.push(`<text ${attrs.join(' ')}>${escapeXml(style.text)}</text>`);
9631
+ }
9632
+ this.#svg.push('</g>');
9633
+ }
9634
+ drawIcons(id, features, spriteAtlas) {
9635
+ if (features.length === 0)
9636
+ return;
9637
+ this.#svg.push(`<g id="${escapeXml(id)}">`);
9638
+ for (const [feature, style] of features) {
9639
+ if (style.opacity <= 0)
9640
+ continue;
9641
+ const sprite = spriteAtlas.get(style.image);
9642
+ if (!sprite)
9643
+ continue;
9644
+ const ring = feature.geometry[0];
9645
+ if (!ring || ring.length === 0)
9646
+ continue;
9647
+ const point = ring[Math.floor(ring.length / 2)];
9648
+ const scale = style.size / sprite.pixelRatio;
9649
+ const iconW = sprite.width * scale;
9650
+ const iconH = sprite.height * scale;
9651
+ const [anchorDx, anchorDy] = mapIconAnchor(style.anchor, iconW, iconH);
9652
+ const ox = style.offset[0] * style.size + anchorDx;
9653
+ const oy = style.offset[1] * style.size + anchorDy;
9654
+ const x = point.x + ox;
9655
+ const y = point.y + oy;
9656
+ const [sx, sy] = roundXY(x, y);
9657
+ const [sw, sh] = roundXY(iconW, iconH);
9658
+ const viewBox = `${String(sprite.x)} ${String(sprite.y)} ${String(sprite.width)} ${String(sprite.height)}`;
9659
+ const attrs = [
9660
+ `x="${formatNum(sx)}"`,
9661
+ `y="${formatNum(sy)}"`,
9662
+ `width="${formatNum(sw)}"`,
9663
+ `height="${formatNum(sh)}"`,
9664
+ ];
9665
+ if (style.opacity < 1)
9666
+ attrs.push(`opacity="${style.opacity.toFixed(3)}"`);
9667
+ if (style.rotate !== 0) {
9668
+ const [cx, cy] = roundXY(point.x + style.offset[0] * style.size, point.y + style.offset[1] * style.size);
9669
+ attrs.push(`transform="rotate(${String(style.rotate)},${formatNum(cx)},${formatNum(cy)})"`);
9670
+ }
9671
+ this.#svg.push(`<svg ${attrs.join(' ')} viewBox="${viewBox}" xmlns="http://www.w3.org/2000/svg">` +
9672
+ `<image width="${String(sprite.sheetWidth)}" height="${String(sprite.sheetHeight)}" href="${sprite.sheetDataUri}" />` +
9673
+ `</svg>`);
9674
+ }
9675
+ this.#svg.push('</g>');
9676
+ }
9677
+ drawRasterTiles(id, tiles, style) {
9497
9678
  if (tiles.length === 0)
9498
9679
  return;
9499
9680
  if (style.opacity <= 0)
@@ -9509,7 +9690,7 @@ class SVGRenderer {
9509
9690
  const brightness = (style.brightnessMin + style.brightnessMax) / 2;
9510
9691
  filters.push(`brightness(${String(brightness)})`);
9511
9692
  }
9512
- let gAttrs = `opacity="${String(style.opacity)}"`;
9693
+ let gAttrs = `id="${escapeXml(id)}" opacity="${String(style.opacity)}"`;
9513
9694
  if (filters.length > 0)
9514
9695
  gAttrs += ` filter="${filters.join(' ')}"`;
9515
9696
  this.#svg.push(`<g ${gAttrs}>`);
@@ -9560,6 +9741,57 @@ function formatPoint(p) {
9560
9741
  const [x, y] = roundXY(p[0], p[1]);
9561
9742
  return formatNum(x) + ',' + formatNum(y);
9562
9743
  }
9744
+ function mapTextAnchor(anchor) {
9745
+ switch (anchor) {
9746
+ case 'left':
9747
+ return ['start', 'central'];
9748
+ case 'right':
9749
+ return ['end', 'central'];
9750
+ case 'top':
9751
+ return ['middle', 'text-before-edge'];
9752
+ case 'bottom':
9753
+ return ['middle', 'text-after-edge'];
9754
+ case 'top-left':
9755
+ return ['start', 'text-before-edge'];
9756
+ case 'top-right':
9757
+ return ['end', 'text-before-edge'];
9758
+ case 'bottom-left':
9759
+ return ['start', 'text-after-edge'];
9760
+ case 'bottom-right':
9761
+ return ['end', 'text-after-edge'];
9762
+ default:
9763
+ return ['middle', 'central'];
9764
+ }
9765
+ }
9766
+ function mapIconAnchor(anchor, w, h) {
9767
+ switch (anchor) {
9768
+ case 'left':
9769
+ return [0, -h / 2];
9770
+ case 'right':
9771
+ return [-w, -h / 2];
9772
+ case 'top':
9773
+ return [-w / 2, 0];
9774
+ case 'bottom':
9775
+ return [-w / 2, -h];
9776
+ case 'top-left':
9777
+ return [0, 0];
9778
+ case 'top-right':
9779
+ return [-w, 0];
9780
+ case 'bottom-left':
9781
+ return [0, -h];
9782
+ case 'bottom-right':
9783
+ return [-w, -h];
9784
+ default:
9785
+ return [-w / 2, -h / 2];
9786
+ }
9787
+ }
9788
+ function escapeXml(s) {
9789
+ return s
9790
+ .replace(/&/g, '&amp;')
9791
+ .replace(/</g, '&lt;')
9792
+ .replace(/>/g, '&gt;')
9793
+ .replace(/"/g, '&quot;');
9794
+ }
9563
9795
 
9564
9796
  /*
9565
9797
  * bignumber.js v9.3.1
@@ -16108,6 +16340,65 @@ async function getRasterTiles(job, sourceName) {
16108
16340
  return rasterTiles.filter((tile) => tile !== null);
16109
16341
  }
16110
16342
 
16343
+ async function loadSpriteAtlas(style) {
16344
+ const atlas = new Map();
16345
+ const sprite = style.sprite;
16346
+ if (!sprite)
16347
+ return atlas;
16348
+ const sources = [];
16349
+ if (typeof sprite === 'string') {
16350
+ sources.push({ id: 'default', url: sprite });
16351
+ }
16352
+ else if (Array.isArray(sprite)) {
16353
+ for (const s of sprite) {
16354
+ sources.push({
16355
+ id: s.id,
16356
+ url: s.url,
16357
+ });
16358
+ }
16359
+ }
16360
+ await Promise.all(sources.map(async ({ id, url }) => {
16361
+ try {
16362
+ const [jsonResponse, imageResponse] = await Promise.all([
16363
+ fetch(`${url}.json`),
16364
+ fetch(`${url}.png`),
16365
+ ]);
16366
+ if (!jsonResponse.ok || !imageResponse.ok)
16367
+ return;
16368
+ const json = (await jsonResponse.json());
16369
+ const imageBuffer = await imageResponse.arrayBuffer();
16370
+ const base64 = typeof Buffer !== 'undefined'
16371
+ ? Buffer.from(imageBuffer).toString('base64')
16372
+ : btoa(String.fromCharCode(...new Uint8Array(imageBuffer)));
16373
+ const sheetDataUri = `data:image/png;base64,${base64}`;
16374
+ // Estimate sheet dimensions from sprite entries
16375
+ let sheetWidth = 0;
16376
+ let sheetHeight = 0;
16377
+ for (const entry of Object.values(json)) {
16378
+ sheetWidth = Math.max(sheetWidth, entry.x + entry.width);
16379
+ sheetHeight = Math.max(sheetHeight, entry.y + entry.height);
16380
+ }
16381
+ const prefix = id === 'default' ? '' : `${id}:`;
16382
+ for (const [name, entry] of Object.entries(json)) {
16383
+ atlas.set(`${prefix}${name}`, {
16384
+ width: entry.width,
16385
+ height: entry.height,
16386
+ x: entry.x,
16387
+ y: entry.y,
16388
+ pixelRatio: entry.pixelRatio ?? 1,
16389
+ sheetDataUri,
16390
+ sheetWidth,
16391
+ sheetHeight,
16392
+ });
16393
+ }
16394
+ }
16395
+ catch {
16396
+ // Silently skip failed sprite loads
16397
+ }
16398
+ }));
16399
+ return atlas;
16400
+ }
16401
+
16111
16402
  async function getLayerFeatures(job) {
16112
16403
  const { width, height } = job.renderer;
16113
16404
  const { zoom, center } = job.view;
@@ -16268,6 +16559,12 @@ function getLayerStyles(layers) {
16268
16559
  return layers.map(createStyleLayer);
16269
16560
  }
16270
16561
 
16562
+ function resolveTokens(text, properties) {
16563
+ return text.replace(/\{([^}]+)\}/g, (_, key) => {
16564
+ const value = properties[key];
16565
+ return value != null ? String(value) : '';
16566
+ });
16567
+ }
16271
16568
  async function renderMap(job) {
16272
16569
  await render(job);
16273
16570
  return job.renderer.getString();
@@ -16278,9 +16575,12 @@ function getFeatures(layerFeatures, layerStyle) {
16278
16575
  async function render(job) {
16279
16576
  const { renderer } = job;
16280
16577
  const { zoom } = job.view;
16281
- const layerFeatures = await getLayerFeatures(job);
16578
+ const [layerFeatures, spriteAtlas] = await Promise.all([
16579
+ getLayerFeatures(job),
16580
+ job.renderLabels ? loadSpriteAtlas(job.style) : Promise.resolve(new Map()),
16581
+ ]);
16282
16582
  const layerStyles = getLayerStyles(job.style.layers);
16283
- const availableImages = [];
16583
+ const availableImages = [...spriteAtlas.keys()];
16284
16584
  const featureState = {};
16285
16585
  for (const layerStyle of layerStyles) {
16286
16586
  if (layerStyle.isHidden(zoom))
@@ -16301,6 +16601,7 @@ async function render(job) {
16301
16601
  function getLayout(key, feature) {
16302
16602
  return getStyleValue(layerStyle.layout, key, feature);
16303
16603
  }
16604
+ const layerId = layerStyle.id;
16304
16605
  switch (layerStyle.type) {
16305
16606
  case 'background':
16306
16607
  {
@@ -16320,7 +16621,7 @@ async function render(job) {
16320
16621
  : polygons;
16321
16622
  if (polygonFeatures.length === 0)
16322
16623
  continue;
16323
- renderer.drawPolygons(polygonFeatures.map((feature) => [
16624
+ renderer.drawPolygons(layerId, polygonFeatures.map((feature) => [
16324
16625
  feature,
16325
16626
  {
16326
16627
  color: getPaint('fill-color', feature),
@@ -16340,7 +16641,7 @@ async function render(job) {
16340
16641
  : lineStrings;
16341
16642
  if (lineStringFeatures.length === 0)
16342
16643
  continue;
16343
- renderer.drawLineStrings(lineStringFeatures.map((feature) => [
16644
+ renderer.drawLineStrings(layerId, lineStringFeatures.map((feature) => [
16344
16645
  feature,
16345
16646
  {
16346
16647
  color: getPaint('line-color', feature),
@@ -16359,7 +16660,7 @@ async function render(job) {
16359
16660
  case 'raster':
16360
16661
  {
16361
16662
  const tiles = await getRasterTiles(job, layerStyle.source);
16362
- renderer.drawRasterTiles(tiles, {
16663
+ renderer.drawRasterTiles(layerId, tiles, {
16363
16664
  opacity: getPaint('raster-opacity'),
16364
16665
  hueRotate: getPaint('raster-hue-rotate'),
16365
16666
  brightnessMin: getPaint('raster-brightness-min'),
@@ -16380,7 +16681,7 @@ async function render(job) {
16380
16681
  : points;
16381
16682
  if (pointFeatures.length === 0)
16382
16683
  continue;
16383
- renderer.drawCircles(pointFeatures.map((feature) => [
16684
+ renderer.drawCircles(layerId, pointFeatures.map((feature) => [
16384
16685
  feature,
16385
16686
  {
16386
16687
  color: getPaint('circle-color', feature),
@@ -16393,11 +16694,76 @@ async function render(job) {
16393
16694
  ]));
16394
16695
  }
16395
16696
  continue;
16697
+ case 'symbol':
16698
+ {
16699
+ if (!job.renderLabels)
16700
+ continue;
16701
+ const features = getFeatures(layerFeatures, layerStyle);
16702
+ const allFeatures = [
16703
+ ...(features?.points ?? []),
16704
+ ...(features?.linestrings ?? []),
16705
+ ...(features?.polygons ?? []),
16706
+ ];
16707
+ if (allFeatures.length === 0)
16708
+ continue;
16709
+ const symbolFeatures = layerStyle.filterFn
16710
+ ? allFeatures.filter((feature) => layerStyle.filterFn.filter({ zoom }, feature))
16711
+ : allFeatures;
16712
+ if (symbolFeatures.length === 0)
16713
+ continue;
16714
+ // Render icons first (underneath text)
16715
+ renderer.drawIcons(`${layerId}-icons`, symbolFeatures.flatMap((feature) => {
16716
+ const iconImage = getLayout('icon-image', feature);
16717
+ const iconName = iconImage != null
16718
+ ? resolveTokens(iconImage.toString(), feature.properties)
16719
+ : '';
16720
+ if (!iconName || !spriteAtlas.has(iconName))
16721
+ return [];
16722
+ return [
16723
+ [
16724
+ feature,
16725
+ {
16726
+ image: iconName,
16727
+ size: getLayout('icon-size', feature),
16728
+ anchor: getLayout('icon-anchor', feature),
16729
+ offset: getLayout('icon-offset', feature),
16730
+ rotate: getLayout('icon-rotate', feature),
16731
+ opacity: getPaint('icon-opacity', feature),
16732
+ },
16733
+ ],
16734
+ ];
16735
+ }), spriteAtlas);
16736
+ // Render text labels on top
16737
+ renderer.drawLabels(`${layerId}-labels`, symbolFeatures.flatMap((feature) => {
16738
+ const textField = getLayout('text-field', feature);
16739
+ const textRaw = textField != null ? textField.toString() : '';
16740
+ const text = resolveTokens(textRaw, feature.properties);
16741
+ if (!text)
16742
+ return [];
16743
+ return [
16744
+ [
16745
+ feature,
16746
+ {
16747
+ text,
16748
+ size: getLayout('text-size', feature),
16749
+ font: getLayout('text-font', feature),
16750
+ anchor: getLayout('text-anchor', feature),
16751
+ offset: getLayout('text-offset', feature),
16752
+ rotate: getLayout('text-rotate', feature),
16753
+ color: getPaint('text-color', feature),
16754
+ opacity: getPaint('text-opacity', feature),
16755
+ haloColor: getPaint('text-halo-color', feature),
16756
+ haloWidth: getPaint('text-halo-width', feature),
16757
+ },
16758
+ ],
16759
+ ];
16760
+ }));
16761
+ }
16762
+ continue;
16396
16763
  case 'color-relief':
16397
16764
  case 'fill-extrusion':
16398
16765
  case 'heatmap':
16399
16766
  case 'hillshade':
16400
- case 'symbol':
16401
16767
  continue;
16402
16768
  default:
16403
16769
  throw Error('layerStyle.type: ' + String(layerStyle.type));
@@ -16419,6 +16785,7 @@ async function renderToSVG(options) {
16419
16785
  center: [options.lon ?? 0, options.lat ?? 0],
16420
16786
  zoom: options.zoom ?? 2,
16421
16787
  },
16788
+ renderLabels: options.renderLabels ?? false,
16422
16789
  });
16423
16790
  }
16424
16791
 
@@ -16514,11 +16881,14 @@ class SVGExportControl {
16514
16881
  <div class="panel-notice">
16515
16882
  Note:<br>
16516
16883
  <span class="panel-attribution"></span><br>
16517
- 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>
16884
+ Text labels are rendered without collision detection, so labels may overlap. You can improve me on <a href="https://github.com/versatiles-org/versatiles-svg-renderer" target="_blank" rel="noopener noreferrer">GitHub</a>.<br>
16518
16885
  </div>
16519
16886
  <div class="panel-inputs">
16520
- <label>Width<input type="number" class="input-width" value="${String(this.options.defaultWidth)}" min="1" max="8192"></label>
16521
- <label>Height<input type="number" class="input-height" value="${String(this.options.defaultHeight)}" min="1" max="8192"></label>
16887
+ <div class="grid">
16888
+ <label>Width<input type="number" class="input-width" value="${String(this.options.defaultWidth)}" min="1" max="8192"></label>
16889
+ <label>Height<input type="number" class="input-height" value="${String(this.options.defaultHeight)}" min="1" max="8192"></label>
16890
+ </div>
16891
+ <label class="label-checkbox"><input type="checkbox" class="input-labels"> Include labels and icons (buggy)</label>
16522
16892
  </div>
16523
16893
  <div class="preview-container">
16524
16894
  <span class="preview-loading">Rendering preview\u2026</span>
@@ -16551,6 +16921,9 @@ class SVGExportControl {
16551
16921
  input.addEventListener('input', () => {
16552
16922
  this.schedulePreview();
16553
16923
  });
16924
+ input.addEventListener('change', () => {
16925
+ this.schedulePreview();
16926
+ });
16554
16927
  });
16555
16928
  querySelector(this.panel, '.btn-download').addEventListener('click', () => {
16556
16929
  this.downloadSVG();
@@ -16616,6 +16989,7 @@ class SVGExportControl {
16616
16989
  openBtn.disabled = true;
16617
16990
  const width = Number(querySelector(panel, '.input-width').value);
16618
16991
  const height = Number(querySelector(panel, '.input-height').value);
16992
+ const renderLabels = querySelector(panel, '.input-labels').checked;
16619
16993
  if (!width || !height || width < 1 || height < 1) {
16620
16994
  previewContainer.innerHTML = '<span class="preview-loading">Invalid input values</span>';
16621
16995
  return;
@@ -16631,6 +17005,7 @@ class SVGExportControl {
16631
17005
  lon: center.lng,
16632
17006
  lat: center.lat,
16633
17007
  zoom,
17008
+ renderLabels,
16634
17009
  });
16635
17010
  if (this.renderGeneration !== generation)
16636
17011
  return;