@versatiles/svg-renderer 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2828,6 +2828,23 @@ var paint_raster = {
2828
2828
  },
2829
2829
  "property-type": "data-constant"
2830
2830
  },
2831
+ resampling: {
2832
+ type: "enum",
2833
+ values: {
2834
+ linear: {
2835
+ },
2836
+ nearest: {
2837
+ }
2838
+ },
2839
+ "default": "linear",
2840
+ expression: {
2841
+ interpolated: false,
2842
+ parameters: [
2843
+ "zoom"
2844
+ ]
2845
+ },
2846
+ "property-type": "data-constant"
2847
+ },
2831
2848
  "raster-resampling": {
2832
2849
  type: "enum",
2833
2850
  values: {
@@ -2978,6 +2995,23 @@ var paint_hillshade = {
2978
2995
  ]
2979
2996
  },
2980
2997
  "property-type": "data-constant"
2998
+ },
2999
+ resampling: {
3000
+ type: "enum",
3001
+ values: {
3002
+ linear: {
3003
+ },
3004
+ nearest: {
3005
+ }
3006
+ },
3007
+ "default": "linear",
3008
+ expression: {
3009
+ interpolated: false,
3010
+ parameters: [
3011
+ "zoom"
3012
+ ]
3013
+ },
3014
+ "property-type": "data-constant"
2981
3015
  }
2982
3016
  };
2983
3017
  var paint_background = {
@@ -3398,6 +3432,23 @@ var v8Spec = {
3398
3432
  ]
3399
3433
  },
3400
3434
  "property-type": "color-ramp"
3435
+ },
3436
+ resampling: {
3437
+ type: "enum",
3438
+ values: {
3439
+ linear: {
3440
+ },
3441
+ nearest: {
3442
+ }
3443
+ },
3444
+ "default": "linear",
3445
+ expression: {
3446
+ interpolated: false,
3447
+ parameters: [
3448
+ "zoom"
3449
+ ]
3450
+ },
3451
+ "property-type": "data-constant"
3401
3452
  }
3402
3453
  },
3403
3454
  paint_background: paint_background,
@@ -6242,11 +6293,12 @@ class CollatorExpression {
6242
6293
  }
6243
6294
 
6244
6295
  class NumberFormat {
6245
- constructor(number, locale, currency, minFractionDigits, maxFractionDigits) {
6296
+ constructor(number, locale, currency, unit, minFractionDigits, maxFractionDigits) {
6246
6297
  this.type = StringType;
6247
6298
  this.number = number;
6248
6299
  this.locale = locale;
6249
6300
  this.currency = currency;
6301
+ this.unit = unit;
6250
6302
  this.minFractionDigits = minFractionDigits;
6251
6303
  this.maxFractionDigits = maxFractionDigits;
6252
6304
  }
@@ -6271,6 +6323,15 @@ class NumberFormat {
6271
6323
  if (!currency)
6272
6324
  return null;
6273
6325
  }
6326
+ let unit = null;
6327
+ if (options['unit']) {
6328
+ unit = context.parse(options['unit'], 1, StringType);
6329
+ if (!unit)
6330
+ return null;
6331
+ }
6332
+ if (currency && unit) {
6333
+ return context.error('NumberFormat options `currency` and `unit` are mutually exclusive');
6334
+ }
6274
6335
  let minFractionDigits = null;
6275
6336
  if (options['min-fraction-digits']) {
6276
6337
  minFractionDigits = context.parse(options['min-fraction-digits'], 1, NumberType);
@@ -6283,12 +6344,13 @@ class NumberFormat {
6283
6344
  if (!maxFractionDigits)
6284
6345
  return null;
6285
6346
  }
6286
- return new NumberFormat(number, locale, currency, minFractionDigits, maxFractionDigits);
6347
+ return new NumberFormat(number, locale, currency, unit, minFractionDigits, maxFractionDigits);
6287
6348
  }
6288
6349
  evaluate(ctx) {
6289
6350
  return new Intl.NumberFormat(this.locale ? this.locale.evaluate(ctx) : [], {
6290
- style: this.currency ? 'currency' : 'decimal',
6351
+ style: this.currency ? 'currency' : this.unit ? 'unit' : 'decimal',
6291
6352
  currency: this.currency ? this.currency.evaluate(ctx) : undefined,
6353
+ unit: this.unit ? this.unit.evaluate(ctx) : undefined,
6292
6354
  minimumFractionDigits: this.minFractionDigits
6293
6355
  ? this.minFractionDigits.evaluate(ctx)
6294
6356
  : undefined,
@@ -6305,6 +6367,9 @@ class NumberFormat {
6305
6367
  if (this.currency) {
6306
6368
  fn(this.currency);
6307
6369
  }
6370
+ if (this.unit) {
6371
+ fn(this.unit);
6372
+ }
6308
6373
  if (this.minFractionDigits) {
6309
6374
  fn(this.minFractionDigits);
6310
6375
  }
@@ -8043,6 +8108,16 @@ CompoundExpression.register(expressions$1, {
8043
8108
  varargs(ValueType),
8044
8109
  (ctx, args) => args.map((arg) => valueToString(arg.evaluate(ctx))).join('')
8045
8110
  ],
8111
+ split: [
8112
+ array(StringType),
8113
+ [StringType, StringType],
8114
+ (ctx, [s, delim]) => s.evaluate(ctx).split(delim.evaluate(ctx))
8115
+ ],
8116
+ join: [
8117
+ StringType,
8118
+ [array(StringType), StringType],
8119
+ (ctx, [arr, delim]) => arr.value.join(delim.evaluate(ctx))
8120
+ ],
8046
8121
  'resolved-locale': [
8047
8122
  StringType,
8048
8123
  [CollatorType],
@@ -9187,13 +9262,11 @@ class SVGRenderer {
9187
9262
  width;
9188
9263
  height;
9189
9264
  #svg;
9190
- #scale;
9191
9265
  #backgroundColor;
9192
9266
  constructor(opt) {
9193
9267
  this.width = opt.width;
9194
9268
  this.height = opt.height;
9195
9269
  this.#svg = [];
9196
- this.#scale = opt.scale;
9197
9270
  this.#backgroundColor = Color.transparent;
9198
9271
  }
9199
9272
  drawBackgroundFill(style) {
@@ -9201,7 +9274,7 @@ class SVGRenderer {
9201
9274
  color.alpha *= style.opacity;
9202
9275
  this.#backgroundColor = color;
9203
9276
  }
9204
- drawPolygons(features) {
9277
+ drawPolygons(id, features) {
9205
9278
  if (features.length === 0)
9206
9279
  return;
9207
9280
  const groups = new Map();
@@ -9213,7 +9286,7 @@ class SVGRenderer {
9213
9286
  return;
9214
9287
  const translate = style.translate[0] === 0 && style.translate[1] === 0
9215
9288
  ? ''
9216
- : ` transform="translate(${formatPoint(style.translate, this.#scale)})"`;
9289
+ : ` transform="translate(${formatPoint(style.translate)})"`;
9217
9290
  const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
9218
9291
  const key = color.hex + translate + opacityAttr;
9219
9292
  let group = groups.get(key);
@@ -9222,15 +9295,17 @@ class SVGRenderer {
9222
9295
  groups.set(key, group);
9223
9296
  }
9224
9297
  feature.geometry.forEach((ring) => {
9225
- group.segments.push(ring.map((p) => roundXY(p.x, p.y, this.#scale)));
9298
+ group.segments.push(ring.map((p) => roundXY(p.x, p.y)));
9226
9299
  });
9227
9300
  });
9301
+ this.#svg.push(`<g id="${escapeXml(id)}">`);
9228
9302
  for (const { segments, attrs } of groups.values()) {
9229
9303
  const d = segmentsToPath(segments, true);
9230
9304
  this.#svg.push(`<path d="${d}" ${attrs} />`);
9231
9305
  }
9306
+ this.#svg.push('</g>');
9232
9307
  }
9233
- drawLineStrings(features) {
9308
+ drawLineStrings(id, features) {
9234
9309
  if (features.length === 0)
9235
9310
  return;
9236
9311
  const groups = new Map();
@@ -9242,10 +9317,10 @@ class SVGRenderer {
9242
9317
  return;
9243
9318
  const translate = style.translate[0] === 0 && style.translate[1] === 0
9244
9319
  ? ''
9245
- : ` transform="translate(${formatPoint(style.translate, this.#scale)})"`;
9246
- const roundedWidth = formatScaled(style.width, this.#scale);
9320
+ : ` transform="translate(${formatPoint(style.translate)})"`;
9321
+ const roundedWidth = formatScaled(style.width);
9247
9322
  const dasharrayStr = style.dasharray
9248
- ? style.dasharray.map((v) => formatScaled(v * style.width, this.#scale)).join(',')
9323
+ ? style.dasharray.map((v) => formatScaled(v * style.width)).join(',')
9249
9324
  : '';
9250
9325
  const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
9251
9326
  const key = [
@@ -9276,16 +9351,18 @@ class SVGRenderer {
9276
9351
  groups.set(key, group);
9277
9352
  }
9278
9353
  feature.geometry.forEach((line) => {
9279
- group.segments.push(line.map((p) => roundXY(p.x, p.y, this.#scale)));
9354
+ group.segments.push(line.map((p) => roundXY(p.x, p.y)));
9280
9355
  });
9281
9356
  });
9357
+ this.#svg.push(`<g id="${escapeXml(id)}">`);
9282
9358
  for (const { segments, attrs } of groups.values()) {
9283
9359
  const chains = chainSegments(segments);
9284
9360
  const d = segmentsToPath(chains);
9285
9361
  this.#svg.push(`<path d="${d}" ${attrs} />`);
9286
9362
  }
9363
+ this.#svg.push('</g>');
9287
9364
  }
9288
- drawCircles(features) {
9365
+ drawCircles(id, features) {
9289
9366
  if (features.length === 0)
9290
9367
  return;
9291
9368
  const groups = new Map();
@@ -9297,12 +9374,10 @@ class SVGRenderer {
9297
9374
  return;
9298
9375
  const translate = style.translate[0] === 0 && style.translate[1] === 0
9299
9376
  ? ''
9300
- : ` transform="translate(${formatPoint(style.translate, this.#scale)})"`;
9301
- const roundedRadius = formatScaled(style.radius, this.#scale);
9377
+ : ` transform="translate(${formatPoint(style.translate)})"`;
9378
+ const roundedRadius = formatScaled(style.radius);
9302
9379
  const strokeColor = new Color(style.strokeColor);
9303
- const strokeAttrs = style.strokeWidth > 0
9304
- ? ` ${strokeAttr(strokeColor, formatScaled(style.strokeWidth, this.#scale))}`
9305
- : '';
9380
+ const strokeAttrs = style.strokeWidth > 0 ? ` ${strokeAttr(strokeColor, formatScaled(style.strokeWidth))}` : '';
9306
9381
  const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
9307
9382
  const key = [color.hex, roundedRadius, strokeAttrs, opacityAttr, translate].join('\0');
9308
9383
  let group = groups.get(key);
@@ -9316,16 +9391,111 @@ class SVGRenderer {
9316
9391
  feature.geometry.forEach((ring) => {
9317
9392
  const p = ring[0];
9318
9393
  if (p)
9319
- group.points.push(roundXY(p.x, p.y, this.#scale));
9394
+ group.points.push(roundXY(p.x, p.y));
9320
9395
  });
9321
9396
  });
9397
+ this.#svg.push(`<g id="${escapeXml(id)}">`);
9322
9398
  for (const { points, attrs } of groups.values()) {
9323
9399
  for (const [x, y] of points) {
9324
9400
  this.#svg.push(`<circle cx="${formatNum(x)}" cy="${formatNum(y)}" ${attrs} />`);
9325
9401
  }
9326
9402
  }
9403
+ this.#svg.push('</g>');
9404
+ }
9405
+ drawLabels(id, features) {
9406
+ if (features.length === 0)
9407
+ return;
9408
+ this.#svg.push(`<g id="${escapeXml(id)}">`);
9409
+ for (const [feature, style] of features) {
9410
+ if (style.opacity <= 0 || !style.text)
9411
+ continue;
9412
+ const color = new Color(style.color);
9413
+ if (color.alpha <= 0)
9414
+ continue;
9415
+ const ring = feature.geometry[0];
9416
+ if (!ring || ring.length === 0)
9417
+ continue;
9418
+ const point = ring[Math.floor(ring.length / 2)];
9419
+ const [px, py] = roundXY(point.x, point.y);
9420
+ const fontSize = formatScaled(style.size);
9421
+ const fontFamily = style.font.join(', ') + ', Helvetica, Arial, sans-serif';
9422
+ const [svgAnchor, baseline] = mapTextAnchor(style.anchor);
9423
+ const offsetX = style.offset[0] * style.size;
9424
+ const offsetY = style.offset[1] * style.size;
9425
+ const [dx, dy] = roundXY(offsetX, offsetY);
9426
+ const attrs = [
9427
+ `x="${formatNum(px)}"`,
9428
+ `y="${formatNum(py)}"`,
9429
+ `font-family="${escapeXml(fontFamily)}"`,
9430
+ `font-size="${fontSize}"`,
9431
+ `text-anchor="${svgAnchor}"`,
9432
+ `dominant-baseline="${baseline}"`,
9433
+ ];
9434
+ if (dx !== 0)
9435
+ attrs.push(`dx="${formatNum(dx)}"`);
9436
+ if (dy !== 0)
9437
+ attrs.push(`dy="${formatNum(dy)}"`);
9438
+ if (style.rotate !== 0) {
9439
+ attrs.push(`transform="rotate(${String(style.rotate)},${formatNum(px)},${formatNum(py)})"`);
9440
+ }
9441
+ const haloColor = new Color(style.haloColor);
9442
+ if (style.haloWidth > 0 && haloColor.alpha > 0) {
9443
+ const haloWidth = formatScaled(style.haloWidth);
9444
+ attrs.push('paint-order="stroke fill"', `stroke="${haloColor.rgb}"`, `stroke-width="${haloWidth}"`, 'stroke-linejoin="round"');
9445
+ if (haloColor.alpha < 255)
9446
+ attrs.push(`stroke-opacity="${haloColor.opacity.toFixed(3)}"`);
9447
+ }
9448
+ attrs.push(fillAttr(color));
9449
+ if (style.opacity < 1)
9450
+ attrs.push(`opacity="${style.opacity.toFixed(3)}"`);
9451
+ this.#svg.push(`<text ${attrs.join(' ')}>${escapeXml(style.text)}</text>`);
9452
+ }
9453
+ this.#svg.push('</g>');
9454
+ }
9455
+ drawIcons(id, features, spriteAtlas) {
9456
+ if (features.length === 0)
9457
+ return;
9458
+ this.#svg.push(`<g id="${escapeXml(id)}">`);
9459
+ for (const [feature, style] of features) {
9460
+ if (style.opacity <= 0)
9461
+ continue;
9462
+ const sprite = spriteAtlas.get(style.image);
9463
+ if (!sprite)
9464
+ continue;
9465
+ const ring = feature.geometry[0];
9466
+ if (!ring || ring.length === 0)
9467
+ continue;
9468
+ const point = ring[Math.floor(ring.length / 2)];
9469
+ const scale = style.size / sprite.pixelRatio;
9470
+ const iconW = sprite.width * scale;
9471
+ const iconH = sprite.height * scale;
9472
+ const [anchorDx, anchorDy] = mapIconAnchor(style.anchor, iconW, iconH);
9473
+ const ox = style.offset[0] * style.size + anchorDx;
9474
+ const oy = style.offset[1] * style.size + anchorDy;
9475
+ const x = point.x + ox;
9476
+ const y = point.y + oy;
9477
+ const [sx, sy] = roundXY(x, y);
9478
+ const [sw, sh] = roundXY(iconW, iconH);
9479
+ const viewBox = `${String(sprite.x)} ${String(sprite.y)} ${String(sprite.width)} ${String(sprite.height)}`;
9480
+ const attrs = [
9481
+ `x="${formatNum(sx)}"`,
9482
+ `y="${formatNum(sy)}"`,
9483
+ `width="${formatNum(sw)}"`,
9484
+ `height="${formatNum(sh)}"`,
9485
+ ];
9486
+ if (style.opacity < 1)
9487
+ attrs.push(`opacity="${style.opacity.toFixed(3)}"`);
9488
+ if (style.rotate !== 0) {
9489
+ const [cx, cy] = roundXY(point.x + style.offset[0] * style.size, point.y + style.offset[1] * style.size);
9490
+ attrs.push(`transform="rotate(${String(style.rotate)},${formatNum(cx)},${formatNum(cy)})"`);
9491
+ }
9492
+ this.#svg.push(`<svg ${attrs.join(' ')} viewBox="${viewBox}" xmlns="http://www.w3.org/2000/svg">` +
9493
+ `<image width="${String(sprite.sheetWidth)}" height="${String(sprite.sheetHeight)}" href="${sprite.sheetDataUri}" />` +
9494
+ `</svg>`);
9495
+ }
9496
+ this.#svg.push('</g>');
9327
9497
  }
9328
- drawRasterTiles(tiles, style) {
9498
+ drawRasterTiles(id, tiles, style) {
9329
9499
  if (tiles.length === 0)
9330
9500
  return;
9331
9501
  if (style.opacity <= 0)
@@ -9341,15 +9511,14 @@ class SVGRenderer {
9341
9511
  const brightness = (style.brightnessMin + style.brightnessMax) / 2;
9342
9512
  filters.push(`brightness(${String(brightness)})`);
9343
9513
  }
9344
- let gAttrs = `opacity="${String(style.opacity)}"`;
9514
+ let gAttrs = `id="${escapeXml(id)}" opacity="${String(style.opacity)}"`;
9345
9515
  if (filters.length > 0)
9346
9516
  gAttrs += ` filter="${filters.join(' ')}"`;
9347
9517
  this.#svg.push(`<g ${gAttrs}>`);
9348
9518
  const pixelated = style.resampling === 'nearest';
9349
9519
  for (const tile of tiles) {
9350
9520
  const overlap = Math.min(tile.width, tile.height) / 10000; // slight overlap to prevent sub-pixel gaps between tiles
9351
- const s = this.#scale;
9352
- let attrs = `x="${formatScaled(tile.x - overlap, s)}" y="${formatScaled(tile.y - overlap, s)}" width="${formatScaled(tile.width + overlap * 2, s)}" height="${formatScaled(tile.height + overlap * 2, s)}" href="${tile.dataUri}"`;
9521
+ let attrs = `x="${formatScaled(tile.x - overlap)}" y="${formatScaled(tile.y - overlap)}" width="${formatScaled(tile.width + overlap * 2)}" height="${formatScaled(tile.height + overlap * 2)}" href="${tile.dataUri}"`;
9353
9522
  if (pixelated)
9354
9523
  attrs += ' style="image-rendering:pixelated"';
9355
9524
  this.#svg.push(`<image ${attrs} />`);
@@ -9383,16 +9552,67 @@ function strokeAttr(color, width) {
9383
9552
  attr += ` stroke-opacity="${color.opacity.toFixed(3)}"`;
9384
9553
  return attr;
9385
9554
  }
9386
- function formatScaled(v, scale) {
9387
- return formatNum(Math.round(v * scale * 10));
9555
+ function formatScaled(v) {
9556
+ return formatNum(Math.round(v * 10));
9388
9557
  }
9389
- function roundXY(x, y, scale) {
9390
- return [Math.round(x * scale * 10), Math.round(y * scale * 10)];
9558
+ function roundXY(x, y) {
9559
+ return [Math.round(x * 10), Math.round(y * 10)];
9391
9560
  }
9392
- function formatPoint(p, scale) {
9393
- const [x, y] = roundXY(p[0], p[1], scale);
9561
+ function formatPoint(p) {
9562
+ const [x, y] = roundXY(p[0], p[1]);
9394
9563
  return formatNum(x) + ',' + formatNum(y);
9395
9564
  }
9565
+ function mapTextAnchor(anchor) {
9566
+ switch (anchor) {
9567
+ case 'left':
9568
+ return ['start', 'central'];
9569
+ case 'right':
9570
+ return ['end', 'central'];
9571
+ case 'top':
9572
+ return ['middle', 'text-before-edge'];
9573
+ case 'bottom':
9574
+ return ['middle', 'text-after-edge'];
9575
+ case 'top-left':
9576
+ return ['start', 'text-before-edge'];
9577
+ case 'top-right':
9578
+ return ['end', 'text-before-edge'];
9579
+ case 'bottom-left':
9580
+ return ['start', 'text-after-edge'];
9581
+ case 'bottom-right':
9582
+ return ['end', 'text-after-edge'];
9583
+ default:
9584
+ return ['middle', 'central'];
9585
+ }
9586
+ }
9587
+ function mapIconAnchor(anchor, w, h) {
9588
+ switch (anchor) {
9589
+ case 'left':
9590
+ return [0, -h / 2];
9591
+ case 'right':
9592
+ return [-w, -h / 2];
9593
+ case 'top':
9594
+ return [-w / 2, 0];
9595
+ case 'bottom':
9596
+ return [-w / 2, -h];
9597
+ case 'top-left':
9598
+ return [0, 0];
9599
+ case 'top-right':
9600
+ return [-w, 0];
9601
+ case 'bottom-left':
9602
+ return [0, -h];
9603
+ case 'bottom-right':
9604
+ return [-w, -h];
9605
+ default:
9606
+ return [-w / 2, -h / 2];
9607
+ }
9608
+ }
9609
+ function escapeXml(s) {
9610
+ return s
9611
+ .replace(/&/g, '&amp;')
9612
+ .replace(/</g, '&lt;')
9613
+ .replace(/>/g, '&gt;')
9614
+ .replace(/"/g, '&quot;');
9615
+ }
9396
9616
 
9397
9617
  /*
9398
9618
  * bignumber.js v9.3.1
@@ -15941,6 +16161,65 @@ async function getRasterTiles(job, sourceName) {
15941
16161
  return rasterTiles.filter((tile) => tile !== null);
15942
16162
  }
15943
16163
 
16164
+ async function loadSpriteAtlas(style) {
16165
+ const atlas = new Map();
16166
+ const sprite = style.sprite;
16167
+ if (!sprite)
16168
+ return atlas;
16169
+ const sources = [];
16170
+ if (typeof sprite === 'string') {
16171
+ sources.push({ id: 'default', url: sprite });
16172
+ }
16173
+ else if (Array.isArray(sprite)) {
16174
+ for (const s of sprite) {
16175
+ sources.push({
16176
+ id: s.id,
16177
+ url: s.url,
16178
+ });
16179
+ }
16180
+ }
16181
+ await Promise.all(sources.map(async ({ id, url }) => {
16182
+ try {
16183
+ const [jsonResponse, imageResponse] = await Promise.all([
16184
+ fetch(`${url}.json`),
16185
+ fetch(`${url}.png`),
16186
+ ]);
16187
+ if (!jsonResponse.ok || !imageResponse.ok)
16188
+ return;
16189
+ const json = (await jsonResponse.json());
16190
+ const imageBuffer = await imageResponse.arrayBuffer();
16191
+ const base64 = typeof Buffer !== 'undefined'
16192
+ ? Buffer.from(imageBuffer).toString('base64')
16193
+ : btoa(String.fromCharCode(...new Uint8Array(imageBuffer)));
16194
+ const sheetDataUri = `data:image/png;base64,${base64}`;
16195
+ // Estimate sheet dimensions from sprite entries
16196
+ let sheetWidth = 0;
16197
+ let sheetHeight = 0;
16198
+ for (const entry of Object.values(json)) {
16199
+ sheetWidth = Math.max(sheetWidth, entry.x + entry.width);
16200
+ sheetHeight = Math.max(sheetHeight, entry.y + entry.height);
16201
+ }
16202
+ const prefix = id === 'default' ? '' : `${id}:`;
16203
+ for (const [name, entry] of Object.entries(json)) {
16204
+ atlas.set(`${prefix}${name}`, {
16205
+ width: entry.width,
16206
+ height: entry.height,
16207
+ x: entry.x,
16208
+ y: entry.y,
16209
+ pixelRatio: entry.pixelRatio ?? 1,
16210
+ sheetDataUri,
16211
+ sheetWidth,
16212
+ sheetHeight,
16213
+ });
16214
+ }
16215
+ }
16216
+ catch {
16217
+ // Silently skip failed sprite loads
16218
+ }
16219
+ }));
16220
+ return atlas;
16221
+ }
16222
+
15944
16223
  async function getLayerFeatures(job) {
15945
16224
  const { width, height } = job.renderer;
15946
16225
  const { zoom, center } = job.view;
@@ -16101,6 +16380,12 @@ function getLayerStyles(layers) {
16101
16380
  return layers.map(createStyleLayer);
16102
16381
  }
16103
16382
 
16383
+ function resolveTokens(text, properties) {
16384
+ return text.replace(/\{([^}]+)\}/g, (_, key) => {
16385
+ const value = properties[key];
16386
+ return value != null ? String(value) : '';
16387
+ });
16388
+ }
16104
16389
  async function renderMap(job) {
16105
16390
  await render(job);
16106
16391
  return job.renderer.getString();
@@ -16111,9 +16396,12 @@ function getFeatures(layerFeatures, layerStyle) {
16111
16396
  async function render(job) {
16112
16397
  const { renderer } = job;
16113
16398
  const { zoom } = job.view;
16114
- const layerFeatures = await getLayerFeatures(job);
16399
+ const [layerFeatures, spriteAtlas] = await Promise.all([
16400
+ getLayerFeatures(job),
16401
+ job.renderLabels ? loadSpriteAtlas(job.style) : Promise.resolve(new Map()),
16402
+ ]);
16115
16403
  const layerStyles = getLayerStyles(job.style.layers);
16116
- const availableImages = [];
16404
+ const availableImages = [...spriteAtlas.keys()];
16117
16405
  const featureState = {};
16118
16406
  for (const layerStyle of layerStyles) {
16119
16407
  if (layerStyle.isHidden(zoom))
@@ -16134,6 +16422,7 @@ async function render(job) {
16134
16422
  function getLayout(key, feature) {
16135
16423
  return getStyleValue(layerStyle.layout, key, feature);
16136
16424
  }
16425
+ const layerId = layerStyle.id;
16137
16426
  switch (layerStyle.type) {
16138
16427
  case 'background':
16139
16428
  {
@@ -16153,7 +16442,7 @@ async function render(job) {
16153
16442
  : polygons;
16154
16443
  if (polygonFeatures.length === 0)
16155
16444
  continue;
16156
- renderer.drawPolygons(polygonFeatures.map((feature) => [
16445
+ renderer.drawPolygons(layerId, polygonFeatures.map((feature) => [
16157
16446
  feature,
16158
16447
  {
16159
16448
  color: getPaint('fill-color', feature),
@@ -16173,7 +16462,7 @@ async function render(job) {
16173
16462
  : lineStrings;
16174
16463
  if (lineStringFeatures.length === 0)
16175
16464
  continue;
16176
- renderer.drawLineStrings(lineStringFeatures.map((feature) => [
16465
+ renderer.drawLineStrings(layerId, lineStringFeatures.map((feature) => [
16177
16466
  feature,
16178
16467
  {
16179
16468
  color: getPaint('line-color', feature),
@@ -16192,7 +16481,7 @@ async function render(job) {
16192
16481
  case 'raster':
16193
16482
  {
16194
16483
  const tiles = await getRasterTiles(job, layerStyle.source);
16195
- renderer.drawRasterTiles(tiles, {
16484
+ renderer.drawRasterTiles(layerId, tiles, {
16196
16485
  opacity: getPaint('raster-opacity'),
16197
16486
  hueRotate: getPaint('raster-hue-rotate'),
16198
16487
  brightnessMin: getPaint('raster-brightness-min'),
@@ -16213,7 +16502,7 @@ async function render(job) {
16213
16502
  : points;
16214
16503
  if (pointFeatures.length === 0)
16215
16504
  continue;
16216
- renderer.drawCircles(pointFeatures.map((feature) => [
16505
+ renderer.drawCircles(layerId, pointFeatures.map((feature) => [
16217
16506
  feature,
16218
16507
  {
16219
16508
  color: getPaint('circle-color', feature),
@@ -16226,11 +16515,76 @@ async function render(job) {
16226
16515
  ]));
16227
16516
  }
16228
16517
  continue;
16518
+ case 'symbol':
16519
+ {
16520
+ if (!job.renderLabels)
16521
+ continue;
16522
+ const features = getFeatures(layerFeatures, layerStyle);
16523
+ const allFeatures = [
16524
+ ...(features?.points ?? []),
16525
+ ...(features?.linestrings ?? []),
16526
+ ...(features?.polygons ?? []),
16527
+ ];
16528
+ if (allFeatures.length === 0)
16529
+ continue;
16530
+ const symbolFeatures = layerStyle.filterFn
16531
+ ? allFeatures.filter((feature) => layerStyle.filterFn.filter({ zoom }, feature))
16532
+ : allFeatures;
16533
+ if (symbolFeatures.length === 0)
16534
+ continue;
16535
+ // Render icons first (underneath text)
16536
+ renderer.drawIcons(`${layerId}-icons`, symbolFeatures.flatMap((feature) => {
16537
+ const iconImage = getLayout('icon-image', feature);
16538
+ const iconName = iconImage != null
16539
+ ? resolveTokens(iconImage.toString(), feature.properties)
16540
+ : '';
16541
+ if (!iconName || !spriteAtlas.has(iconName))
16542
+ return [];
16543
+ return [
16544
+ [
16545
+ feature,
16546
+ {
16547
+ image: iconName,
16548
+ size: getLayout('icon-size', feature),
16549
+ anchor: getLayout('icon-anchor', feature),
16550
+ offset: getLayout('icon-offset', feature),
16551
+ rotate: getLayout('icon-rotate', feature),
16552
+ opacity: getPaint('icon-opacity', feature),
16553
+ },
16554
+ ],
16555
+ ];
16556
+ }), spriteAtlas);
16557
+ // Render text labels on top
16558
+ renderer.drawLabels(`${layerId}-labels`, symbolFeatures.flatMap((feature) => {
16559
+ const textField = getLayout('text-field', feature);
16560
+ const textRaw = textField != null ? textField.toString() : '';
16561
+ const text = resolveTokens(textRaw, feature.properties);
16562
+ if (!text)
16563
+ return [];
16564
+ return [
16565
+ [
16566
+ feature,
16567
+ {
16568
+ text,
16569
+ size: getLayout('text-size', feature),
16570
+ font: getLayout('text-font', feature),
16571
+ anchor: getLayout('text-anchor', feature),
16572
+ offset: getLayout('text-offset', feature),
16573
+ rotate: getLayout('text-rotate', feature),
16574
+ color: getPaint('text-color', feature),
16575
+ opacity: getPaint('text-opacity', feature),
16576
+ haloColor: getPaint('text-halo-color', feature),
16577
+ haloWidth: getPaint('text-halo-width', feature),
16578
+ },
16579
+ ],
16580
+ ];
16581
+ }));
16582
+ }
16583
+ continue;
16229
16584
  case 'color-relief':
16230
16585
  case 'fill-extrusion':
16231
16586
  case 'heatmap':
16232
16587
  case 'hillshade':
16233
- case 'symbol':
16234
16588
  continue;
16235
16589
  default:
16236
16590
  throw Error('layerStyle.type: ' + String(layerStyle.type));
@@ -16241,20 +16595,18 @@ async function render(job) {
16241
16595
  async function renderToSVG(options) {
16242
16596
  const width = options.width ?? 1024;
16243
16597
  const height = options.height ?? 1024;
16244
- const scale = options.scale ?? 1;
16245
16598
  if (width <= 0)
16246
16599
  throw new Error('width must be positive');
16247
16600
  if (height <= 0)
16248
16601
  throw new Error('height must be positive');
16249
- if (scale <= 0)
16250
- throw new Error('scale must be positive');
16251
16602
  return await renderMap({
16252
- renderer: new SVGRenderer({ width, height, scale }),
16603
+ renderer: new SVGRenderer({ width, height }),
16253
16604
  style: options.style,
16254
16605
  view: {
16255
16606
  center: [options.lon ?? 0, options.lat ?? 0],
16256
16607
  zoom: options.zoom ?? 2,
16257
16608
  },
16609
+ renderLabels: options.renderLabels ?? false,
16258
16610
  });
16259
16611
  }
16260
16612