@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.
@@ -69,6 +69,10 @@
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
77
  grid-template-columns: 1fr 1fr;
74
78
  gap: 8px;
@@ -76,11 +80,15 @@
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
- font-size: 12px;
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],
@@ -9377,7 +9459,7 @@
9377
9459
  color.alpha *= style.opacity;
9378
9460
  this.#backgroundColor = color;
9379
9461
  }
9380
- drawPolygons(features) {
9462
+ drawPolygons(id, features) {
9381
9463
  if (features.length === 0)
9382
9464
  return;
9383
9465
  const groups = new Map();
@@ -9401,12 +9483,14 @@
9401
9483
  group.segments.push(ring.map((p) => roundXY(p.x, p.y)));
9402
9484
  });
9403
9485
  });
9486
+ this.#svg.push(`<g id="${escapeXml(id)}">`);
9404
9487
  for (const { segments, attrs } of groups.values()) {
9405
9488
  const d = segmentsToPath(segments, true);
9406
9489
  this.#svg.push(`<path d="${d}" ${attrs} />`);
9407
9490
  }
9491
+ this.#svg.push('</g>');
9408
9492
  }
9409
- drawLineStrings(features) {
9493
+ drawLineStrings(id, features) {
9410
9494
  if (features.length === 0)
9411
9495
  return;
9412
9496
  const groups = new Map();
@@ -9455,13 +9539,15 @@
9455
9539
  group.segments.push(line.map((p) => roundXY(p.x, p.y)));
9456
9540
  });
9457
9541
  });
9542
+ this.#svg.push(`<g id="${escapeXml(id)}">`);
9458
9543
  for (const { segments, attrs } of groups.values()) {
9459
9544
  const chains = chainSegments(segments);
9460
9545
  const d = segmentsToPath(chains);
9461
9546
  this.#svg.push(`<path d="${d}" ${attrs} />`);
9462
9547
  }
9548
+ this.#svg.push('</g>');
9463
9549
  }
9464
- drawCircles(features) {
9550
+ drawCircles(id, features) {
9465
9551
  if (features.length === 0)
9466
9552
  return;
9467
9553
  const groups = new Map();
@@ -9493,13 +9579,108 @@
9493
9579
  group.points.push(roundXY(p.x, p.y));
9494
9580
  });
9495
9581
  });
9582
+ this.#svg.push(`<g id="${escapeXml(id)}">`);
9496
9583
  for (const { points, attrs } of groups.values()) {
9497
9584
  for (const [x, y] of points) {
9498
9585
  this.#svg.push(`<circle cx="${formatNum(x)}" cy="${formatNum(y)}" ${attrs} />`);
9499
9586
  }
9500
9587
  }
9588
+ this.#svg.push('</g>');
9501
9589
  }
9502
- drawRasterTiles(tiles, style) {
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>');
9682
+ }
9683
+ drawRasterTiles(id, tiles, style) {
9503
9684
  if (tiles.length === 0)
9504
9685
  return;
9505
9686
  if (style.opacity <= 0)
@@ -9515,7 +9696,7 @@
9515
9696
  const brightness = (style.brightnessMin + style.brightnessMax) / 2;
9516
9697
  filters.push(`brightness(${String(brightness)})`);
9517
9698
  }
9518
- let gAttrs = `opacity="${String(style.opacity)}"`;
9699
+ let gAttrs = `id="${escapeXml(id)}" opacity="${String(style.opacity)}"`;
9519
9700
  if (filters.length > 0)
9520
9701
  gAttrs += ` filter="${filters.join(' ')}"`;
9521
9702
  this.#svg.push(`<g ${gAttrs}>`);
@@ -9566,6 +9747,57 @@
9566
9747
  const [x, y] = roundXY(p[0], p[1]);
9567
9748
  return formatNum(x) + ',' + formatNum(y);
9568
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, '&amp;')
9797
+ .replace(/</g, '&lt;')
9798
+ .replace(/>/g, '&gt;')
9799
+ .replace(/"/g, '&quot;');
9800
+ }
9569
9801
 
9570
9802
  /*
9571
9803
  * bignumber.js v9.3.1
@@ -16114,6 +16346,65 @@
16114
16346
  return rasterTiles.filter((tile) => tile !== null);
16115
16347
  }
16116
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
+
16117
16408
  async function getLayerFeatures(job) {
16118
16409
  const { width, height } = job.renderer;
16119
16410
  const { zoom, center } = job.view;
@@ -16274,6 +16565,12 @@
16274
16565
  return layers.map(createStyleLayer);
16275
16566
  }
16276
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
+ }
16277
16574
  async function renderMap(job) {
16278
16575
  await render(job);
16279
16576
  return job.renderer.getString();
@@ -16284,9 +16581,12 @@
16284
16581
  async function render(job) {
16285
16582
  const { renderer } = job;
16286
16583
  const { zoom } = job.view;
16287
- const layerFeatures = await getLayerFeatures(job);
16584
+ const [layerFeatures, spriteAtlas] = await Promise.all([
16585
+ getLayerFeatures(job),
16586
+ job.renderLabels ? loadSpriteAtlas(job.style) : Promise.resolve(new Map()),
16587
+ ]);
16288
16588
  const layerStyles = getLayerStyles(job.style.layers);
16289
- const availableImages = [];
16589
+ const availableImages = [...spriteAtlas.keys()];
16290
16590
  const featureState = {};
16291
16591
  for (const layerStyle of layerStyles) {
16292
16592
  if (layerStyle.isHidden(zoom))
@@ -16307,6 +16607,7 @@
16307
16607
  function getLayout(key, feature) {
16308
16608
  return getStyleValue(layerStyle.layout, key, feature);
16309
16609
  }
16610
+ const layerId = layerStyle.id;
16310
16611
  switch (layerStyle.type) {
16311
16612
  case 'background':
16312
16613
  {
@@ -16326,7 +16627,7 @@
16326
16627
  : polygons;
16327
16628
  if (polygonFeatures.length === 0)
16328
16629
  continue;
16329
- renderer.drawPolygons(polygonFeatures.map((feature) => [
16630
+ renderer.drawPolygons(layerId, polygonFeatures.map((feature) => [
16330
16631
  feature,
16331
16632
  {
16332
16633
  color: getPaint('fill-color', feature),
@@ -16346,7 +16647,7 @@
16346
16647
  : lineStrings;
16347
16648
  if (lineStringFeatures.length === 0)
16348
16649
  continue;
16349
- renderer.drawLineStrings(lineStringFeatures.map((feature) => [
16650
+ renderer.drawLineStrings(layerId, lineStringFeatures.map((feature) => [
16350
16651
  feature,
16351
16652
  {
16352
16653
  color: getPaint('line-color', feature),
@@ -16365,7 +16666,7 @@
16365
16666
  case 'raster':
16366
16667
  {
16367
16668
  const tiles = await getRasterTiles(job, layerStyle.source);
16368
- renderer.drawRasterTiles(tiles, {
16669
+ renderer.drawRasterTiles(layerId, tiles, {
16369
16670
  opacity: getPaint('raster-opacity'),
16370
16671
  hueRotate: getPaint('raster-hue-rotate'),
16371
16672
  brightnessMin: getPaint('raster-brightness-min'),
@@ -16386,7 +16687,7 @@
16386
16687
  : points;
16387
16688
  if (pointFeatures.length === 0)
16388
16689
  continue;
16389
- renderer.drawCircles(pointFeatures.map((feature) => [
16690
+ renderer.drawCircles(layerId, pointFeatures.map((feature) => [
16390
16691
  feature,
16391
16692
  {
16392
16693
  color: getPaint('circle-color', feature),
@@ -16399,11 +16700,76 @@
16399
16700
  ]));
16400
16701
  }
16401
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;
16402
16769
  case 'color-relief':
16403
16770
  case 'fill-extrusion':
16404
16771
  case 'heatmap':
16405
16772
  case 'hillshade':
16406
- case 'symbol':
16407
16773
  continue;
16408
16774
  default:
16409
16775
  throw Error('layerStyle.type: ' + String(layerStyle.type));
@@ -16425,6 +16791,7 @@
16425
16791
  center: [options.lon ?? 0, options.lat ?? 0],
16426
16792
  zoom: options.zoom ?? 2,
16427
16793
  },
16794
+ renderLabels: options.renderLabels ?? false,
16428
16795
  });
16429
16796
  }
16430
16797
 
@@ -16520,11 +16887,14 @@
16520
16887
  <div class="panel-notice">
16521
16888
  Note:<br>
16522
16889
  <span class="panel-attribution"></span><br>
16523
- 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>
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>
16524
16891
  </div>
16525
16892
  <div class="panel-inputs">
16526
- <label>Width<input type="number" class="input-width" value="${String(this.options.defaultWidth)}" min="1" max="8192"></label>
16527
- <label>Height<input type="number" class="input-height" value="${String(this.options.defaultHeight)}" min="1" max="8192"></label>
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>
16528
16898
  </div>
16529
16899
  <div class="preview-container">
16530
16900
  <span class="preview-loading">Rendering preview\u2026</span>
@@ -16557,6 +16927,9 @@
16557
16927
  input.addEventListener('input', () => {
16558
16928
  this.schedulePreview();
16559
16929
  });
16930
+ input.addEventListener('change', () => {
16931
+ this.schedulePreview();
16932
+ });
16560
16933
  });
16561
16934
  querySelector(this.panel, '.btn-download').addEventListener('click', () => {
16562
16935
  this.downloadSVG();
@@ -16622,6 +16995,7 @@
16622
16995
  openBtn.disabled = true;
16623
16996
  const width = Number(querySelector(panel, '.input-width').value);
16624
16997
  const height = Number(querySelector(panel, '.input-height').value);
16998
+ const renderLabels = querySelector(panel, '.input-labels').checked;
16625
16999
  if (!width || !height || width < 1 || height < 1) {
16626
17000
  previewContainer.innerHTML = '<span class="preview-loading">Invalid input values</span>';
16627
17001
  return;
@@ -16637,6 +17011,7 @@
16637
17011
  lon: center.lng,
16638
17012
  lat: center.lat,
16639
17013
  zoom,
17014
+ renderLabels,
16640
17015
  });
16641
17016
  if (this.renderGeneration !== generation)
16642
17017
  return;