@versatiles/svg-renderer 0.3.0 → 0.5.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.js CHANGED
@@ -9031,7 +9031,9 @@ class Color {
9031
9031
  this.values = [args[0], args[1], args[2], args[3]];
9032
9032
  return;
9033
9033
  }
9034
- throw Error('Unsupported Color arguments: ' + JSON.stringify(args));
9034
+ throw Error('Unsupported Color arguments: ' +
9035
+ JSON.stringify(args) +
9036
+ '. Expected a MaplibreColor, hex string (#RRGGBB or #RRGGBBAA), or 3-4 numeric components.');
9035
9037
  function h2d(text) {
9036
9038
  return parseInt(text, 16);
9037
9039
  }
@@ -9041,14 +9043,12 @@ class Color {
9041
9043
  }
9042
9044
  get hex() {
9043
9045
  return `#${d2h(this.values[0])}${d2h(this.values[1])}${d2h(this.values[2])}${this.values[3] === 255 ? '' : d2h(this.values[3])}`;
9044
- function d2h(num) {
9045
- if (num < 0)
9046
- num = 0;
9047
- if (num > 255)
9048
- num = 255;
9049
- const str = Math.round(num).toString(16).toUpperCase();
9050
- return str.length < 2 ? '0' + str : str;
9051
- }
9046
+ }
9047
+ get rgb() {
9048
+ return `#${d2h(this.values[0])}${d2h(this.values[1])}${d2h(this.values[2])}`;
9049
+ }
9050
+ get opacity() {
9051
+ return this.values[3] / 255;
9052
9052
  }
9053
9053
  get alpha() {
9054
9054
  return this.values[3];
@@ -9060,6 +9060,126 @@ class Color {
9060
9060
  return new Color(...this.values);
9061
9061
  }
9062
9062
  }
9063
+ function d2h(num) {
9064
+ if (num < 0)
9065
+ num = 0;
9066
+ if (num > 255)
9067
+ num = 255;
9068
+ const str = Math.round(num).toString(16).toUpperCase();
9069
+ return str.length < 2 ? '0' + str : str;
9070
+ }
9071
+
9072
+ function chainSegments(segments) {
9073
+ // Phase 1: normalize segments left-to-right, then chain
9074
+ normalizeSegments(segments, 0);
9075
+ let chains = greedyChain(segments);
9076
+ // Phase 2: normalize remaining chains top-to-bottom, then chain again
9077
+ normalizeSegments(chains, 1);
9078
+ chains = greedyChain(chains);
9079
+ return chains;
9080
+ }
9081
+ function normalizeSegments(segments, coordIndex) {
9082
+ for (const seg of segments) {
9083
+ const first = seg[0];
9084
+ const last = seg[seg.length - 1];
9085
+ if (first && last && last[coordIndex] < first[coordIndex])
9086
+ seg.reverse();
9087
+ }
9088
+ }
9089
+ function greedyChain(segments) {
9090
+ const byStart = new Map();
9091
+ for (const seg of segments) {
9092
+ const start = seg[0];
9093
+ if (!start)
9094
+ continue;
9095
+ const key = String(start[0]) + ',' + String(start[1]);
9096
+ let list = byStart.get(key);
9097
+ if (!list) {
9098
+ list = [];
9099
+ byStart.set(key, list);
9100
+ }
9101
+ list.push(seg);
9102
+ }
9103
+ const visited = new Set();
9104
+ const chains = [];
9105
+ for (const seg of segments) {
9106
+ if (visited.has(seg))
9107
+ continue;
9108
+ visited.add(seg);
9109
+ const chain = [...seg];
9110
+ let endPoint = chain[chain.length - 1];
9111
+ let candidates = endPoint
9112
+ ? byStart.get(String(endPoint[0]) + ',' + String(endPoint[1]))
9113
+ : undefined;
9114
+ while (candidates) {
9115
+ let next;
9116
+ for (const c of candidates) {
9117
+ if (!visited.has(c)) {
9118
+ next = c;
9119
+ break;
9120
+ }
9121
+ }
9122
+ if (!next)
9123
+ break;
9124
+ visited.add(next);
9125
+ for (let i = 1; i < next.length; i++)
9126
+ chain.push(next[i]);
9127
+ endPoint = chain[chain.length - 1];
9128
+ candidates = endPoint
9129
+ ? byStart.get(String(endPoint[0]) + ',' + String(endPoint[1]))
9130
+ : undefined;
9131
+ }
9132
+ chains.push(chain);
9133
+ }
9134
+ return chains;
9135
+ }
9136
+ function segmentsToPath(chains, close = false) {
9137
+ let d = '';
9138
+ for (const chain of chains) {
9139
+ const first = chain[0];
9140
+ if (!first)
9141
+ continue;
9142
+ d += 'M' + formatNum(first[0]) + ',' + formatNum(first[1]);
9143
+ let px = first[0];
9144
+ let py = first[1];
9145
+ for (let i = 1; i < chain.length; i++) {
9146
+ const x = chain[i][0];
9147
+ const y = chain[i][1];
9148
+ const dx = x - px;
9149
+ const dy = y - py;
9150
+ if (dy === 0) {
9151
+ const rel = 'h' + formatNum(dx);
9152
+ const abs = 'H' + formatNum(x);
9153
+ d += rel.length <= abs.length ? rel : abs;
9154
+ }
9155
+ else if (dx === 0) {
9156
+ const rel = 'v' + formatNum(dy);
9157
+ const abs = 'V' + formatNum(y);
9158
+ d += rel.length <= abs.length ? rel : abs;
9159
+ }
9160
+ else {
9161
+ const rel = 'l' + formatNum(dx) + ',' + formatNum(dy);
9162
+ const abs = 'L' + formatNum(x) + ',' + formatNum(y);
9163
+ d += rel.length <= abs.length ? rel : abs;
9164
+ }
9165
+ px = x;
9166
+ py = y;
9167
+ }
9168
+ if (close)
9169
+ d += 'z';
9170
+ }
9171
+ return d;
9172
+ }
9173
+ function formatNum(tenths) {
9174
+ if (tenths % 10 === 0)
9175
+ return String(tenths / 10);
9176
+ const negative = tenths < 0;
9177
+ if (negative)
9178
+ tenths = -tenths;
9179
+ const whole = Math.floor(tenths / 10);
9180
+ const frac = tenths % 10;
9181
+ return (negative ? '-' : '') + String(whole) + '.' + String(frac);
9182
+ }
9063
9183
 
9064
9184
  class SVGRenderer {
9065
9185
  width;
@@ -9075,78 +9195,86 @@ class SVGRenderer {
9075
9195
  this.#backgroundColor = Color.transparent;
9076
9196
  }
9077
9197
  drawBackgroundFill(style) {
9078
- const color = style.color.clone();
9198
+ const color = new Color(style.color);
9079
9199
  color.alpha *= style.opacity;
9080
9200
  this.#backgroundColor = color;
9081
9201
  }
9082
- drawPolygons(features, opacity) {
9202
+ drawPolygons(features) {
9083
9203
  if (features.length === 0)
9084
9204
  return;
9085
- if (opacity <= 0)
9086
- return;
9087
- this.#svg.push(`<g opacity="${String(opacity)}">`);
9088
9205
  const groups = new Map();
9089
9206
  features.forEach(([feature, style]) => {
9090
- if (style.color.alpha <= 0)
9207
+ if (style.opacity <= 0)
9208
+ return;
9209
+ const color = new Color(style.color);
9210
+ if (color.alpha <= 0)
9091
9211
  return;
9092
- const translate = style.translate.isZero()
9212
+ const translate = style.translate[0] === 0 && style.translate[1] === 0
9093
9213
  ? ''
9094
9214
  : ` transform="translate(${formatPoint(style.translate, this.#scale)})"`;
9095
- const key = style.color.hex + translate;
9215
+ const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
9216
+ const key = color.hex + translate + opacityAttr;
9096
9217
  let group = groups.get(key);
9097
9218
  if (!group) {
9098
- group = { segments: [], attrs: `fill="${style.color.hex}"${translate}` };
9219
+ group = { segments: [], attrs: `${fillAttr(color)}${translate}${opacityAttr}` };
9099
9220
  groups.set(key, group);
9100
9221
  }
9101
9222
  feature.geometry.forEach((ring) => {
9102
- group.segments.push(ring.map((p) => roundXY(p, this.#scale)));
9223
+ group.segments.push(ring.map((p) => roundXY(p.x, p.y, this.#scale)));
9103
9224
  });
9104
9225
  });
9105
9226
  for (const { segments, attrs } of groups.values()) {
9106
9227
  const d = segmentsToPath(segments, true);
9107
9228
  this.#svg.push(`<path d="${d}" ${attrs} />`);
9108
9229
  }
9109
- this.#svg.push('</g>');
9110
9230
  }
9111
- drawLineStrings(features, opacity) {
9231
+ drawLineStrings(features) {
9112
9232
  if (features.length === 0)
9113
9233
  return;
9114
- if (opacity <= 0)
9115
- return;
9116
- this.#svg.push(`<g opacity="${String(opacity)}">`);
9117
9234
  const groups = new Map();
9118
9235
  features.forEach(([feature, style]) => {
9119
- if (style.width <= 0 || style.color.alpha <= 0)
9236
+ if (style.opacity <= 0)
9237
+ return;
9238
+ const color = new Color(style.color);
9239
+ if (style.width <= 0 || color.alpha <= 0)
9120
9240
  return;
9121
- const translate = style.translate.isZero()
9241
+ const translate = style.translate[0] === 0 && style.translate[1] === 0
9122
9242
  ? ''
9123
9243
  : ` transform="translate(${formatPoint(style.translate, this.#scale)})"`;
9124
- const roundedWidth = roundValue(style.width, this.#scale);
9244
+ const roundedWidth = formatScaled(style.width, this.#scale);
9245
+ const dasharrayStr = style.dasharray
9246
+ ? style.dasharray.map((v) => formatScaled(v * style.width, this.#scale)).join(',')
9247
+ : '';
9248
+ const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
9125
9249
  const key = [
9126
- style.color.hex,
9250
+ color.hex,
9127
9251
  roundedWidth,
9128
9252
  style.cap,
9129
9253
  style.join,
9130
9254
  String(style.miterLimit),
9255
+ dasharrayStr,
9256
+ opacityAttr,
9131
9257
  translate,
9132
9258
  ].join('\0');
9133
9259
  let group = groups.get(key);
9134
9260
  if (!group) {
9261
+ const attrs = [
9262
+ 'fill="none"',
9263
+ strokeAttr(color, roundedWidth),
9264
+ `stroke-linecap="${style.cap}"`,
9265
+ `stroke-linejoin="${style.join}"`,
9266
+ `stroke-miterlimit="${String(style.miterLimit)}"`,
9267
+ ];
9268
+ if (dasharrayStr)
9269
+ attrs.push(`stroke-dasharray="${dasharrayStr}"`);
9135
9270
  group = {
9136
9271
  segments: [],
9137
- attrs: [
9138
- 'fill="none"',
9139
- `stroke="${style.color.hex}"`,
9140
- `stroke-width="${roundedWidth}"`,
9141
- `stroke-linecap="${style.cap}"`,
9142
- `stroke-linejoin="${style.join}"`,
9143
- `stroke-miterlimit="${String(style.miterLimit)}"`,
9144
- ].join(' ') + translate,
9272
+ attrs: attrs.join(' ') + translate + opacityAttr,
9145
9273
  };
9146
9274
  groups.set(key, group);
9147
9275
  }
9148
9276
  feature.geometry.forEach((line) => {
9149
- group.segments.push(line.map((p) => roundXY(p, this.#scale)));
9277
+ group.segments.push(line.map((p) => roundXY(p.x, p.y, this.#scale)));
9150
9278
  });
9151
9279
  });
9152
9280
  for (const { segments, attrs } of groups.values()) {
@@ -9154,7 +9282,46 @@ class SVGRenderer {
9154
9282
  const d = segmentsToPath(chains);
9155
9283
  this.#svg.push(`<path d="${d}" ${attrs} />`);
9156
9284
  }
9157
- this.#svg.push('</g>');
9285
+ }
9286
+ drawCircles(features) {
9287
+ if (features.length === 0)
9288
+ return;
9289
+ const groups = new Map();
9290
+ features.forEach(([feature, style]) => {
9291
+ if (style.opacity <= 0)
9292
+ return;
9293
+ const color = new Color(style.color);
9294
+ if (style.radius <= 0 || color.alpha <= 0)
9295
+ return;
9296
+ const translate = style.translate[0] === 0 && style.translate[1] === 0
9297
+ ? ''
9298
+ : ` transform="translate(${formatPoint(style.translate, this.#scale)})"`;
9299
+ const roundedRadius = formatScaled(style.radius, this.#scale);
9300
+ const strokeColor = new Color(style.strokeColor);
9301
+ const strokeAttrs = style.strokeWidth > 0
9302
+ ? ` ${strokeAttr(strokeColor, formatScaled(style.strokeWidth, this.#scale))}`
9303
+ : '';
9304
+ const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
9305
+ const key = [color.hex, roundedRadius, strokeAttrs, opacityAttr, translate].join('\0');
9306
+ let group = groups.get(key);
9307
+ if (!group) {
9308
+ group = {
9309
+ points: [],
9310
+ attrs: `r="${roundedRadius}" ${fillAttr(color)}${strokeAttrs}${translate}${opacityAttr}`,
9311
+ };
9312
+ groups.set(key, group);
9313
+ }
9314
+ feature.geometry.forEach((ring) => {
9315
+ const p = ring[0];
9316
+ if (p)
9317
+ group.points.push(roundXY(p.x, p.y, this.#scale));
9318
+ });
9319
+ });
9320
+ for (const { points, attrs } of groups.values()) {
9321
+ for (const [x, y] of points) {
9322
+ this.#svg.push(`<circle cx="${formatNum(x)}" cy="${formatNum(y)}" ${attrs} />`);
9323
+ }
9324
+ }
9158
9325
  }
9159
9326
  drawRasterTiles(tiles, style) {
9160
9327
  if (tiles.length === 0)
@@ -9180,7 +9347,7 @@ class SVGRenderer {
9180
9347
  for (const tile of tiles) {
9181
9348
  const overlap = Math.min(tile.width, tile.height) / 10000; // slight overlap to prevent sub-pixel gaps between tiles
9182
9349
  const s = this.#scale;
9183
- let attrs = `x="${roundValue(tile.x - overlap, s)}" y="${roundValue(tile.y - overlap, s)}" width="${roundValue(tile.width + overlap * 2, s)}" height="${roundValue(tile.height + overlap * 2, s)}" href="${tile.dataUri}"`;
9350
+ 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}"`;
9184
9351
  if (pixelated)
9185
9352
  attrs += ' style="image-rendering:pixelated"';
9186
9353
  this.#svg.push(`<image ${attrs} />`);
@@ -9188,192 +9355,41 @@ class SVGRenderer {
9188
9355
  this.#svg.push('</g>');
9189
9356
  }
9190
9357
  getString() {
9191
- return [
9192
- `<svg viewBox="0 0 ${String(this.width)} ${String(this.height)}" width="${String(this.width)}" height="${String(this.height)}" xmlns="http://www.w3.org/2000/svg" style="background-color:${this.#backgroundColor.hex}">`,
9193
- ...this.#svg,
9194
- '</svg>',
9195
- ].join('\n');
9196
- }
9197
- }
9198
- function roundValue(v, scale) {
9199
- return (v * scale).toFixed(3);
9200
- }
9201
- function roundXY(p, scale) {
9202
- return [Math.round(p.x * scale * 10), Math.round(p.y * scale * 10)];
9203
- }
9204
- function formatPoint(p, scale) {
9205
- const [x, y] = roundXY(p, scale);
9206
- return formatNum(x) + ',' + formatNum(y);
9207
- }
9208
- function chainSegments(segments) {
9209
- // Phase 1: normalize segments left-to-right, then chain
9210
- normalizeSegments(segments, 0);
9211
- let chains = greedyChain(segments);
9212
- // Phase 2: normalize remaining chains top-to-bottom, then chain again
9213
- normalizeSegments(chains, 1);
9214
- chains = greedyChain(chains);
9215
- return chains;
9216
- }
9217
- function normalizeSegments(segments, coordIndex) {
9218
- for (const seg of segments) {
9219
- if (seg[seg.length - 1][coordIndex] < seg[0][coordIndex])
9220
- seg.reverse();
9221
- }
9222
- }
9223
- function greedyChain(segments) {
9224
- const byStart = new Map();
9225
- for (const seg of segments) {
9226
- const key = String(seg[0][0]) + ',' + String(seg[0][1]);
9227
- let list = byStart.get(key);
9228
- if (!list) {
9229
- list = [];
9230
- byStart.set(key, list);
9231
- }
9232
- list.push(seg);
9233
- }
9234
- const visited = new Set();
9235
- const chains = [];
9236
- for (const seg of segments) {
9237
- if (visited.has(seg))
9238
- continue;
9239
- visited.add(seg);
9240
- const chain = [...seg];
9241
- let endPoint = chain[chain.length - 1];
9242
- let candidates = byStart.get(String(endPoint[0]) + ',' + String(endPoint[1]));
9243
- while (candidates) {
9244
- let next;
9245
- for (const c of candidates) {
9246
- if (!visited.has(c)) {
9247
- next = c;
9248
- break;
9249
- }
9250
- }
9251
- if (!next)
9252
- break;
9253
- visited.add(next);
9254
- for (let i = 1; i < next.length; i++)
9255
- chain.push(next[i]);
9256
- endPoint = chain[chain.length - 1];
9257
- candidates = byStart.get(String(endPoint[0]) + ',' + String(endPoint[1]));
9358
+ const w = this.width.toFixed(0);
9359
+ const h = this.height.toFixed(0);
9360
+ const parts = [
9361
+ `<svg viewBox="0 0 ${w} ${h}" width="${w}" height="${h}" xmlns="http://www.w3.org/2000/svg">`,
9362
+ `<defs><clipPath id="vb"><rect width="${w}" height="${h}"/></clipPath></defs>`,
9363
+ `<g clip-path="url(#vb)">`,
9364
+ ];
9365
+ if (this.#backgroundColor.alpha > 0) {
9366
+ parts.push(`<rect x="-1" y="-1" width="${(this.width + 2).toFixed(0)}" height="${(this.height + 2).toFixed(0)}" ${fillAttr(this.#backgroundColor)} />`);
9258
9367
  }
9259
- chains.push(chain);
9368
+ parts.push(...this.#svg, '</g>', '</svg>');
9369
+ return parts.join('\n');
9260
9370
  }
9261
- return chains;
9262
9371
  }
9263
- function segmentsToPath(chains, close = false) {
9264
- let d = '';
9265
- for (const chain of chains) {
9266
- d += 'M' + formatNum(chain[0][0]) + ',' + formatNum(chain[0][1]);
9267
- let px = chain[0][0];
9268
- let py = chain[0][1];
9269
- for (let i = 1; i < chain.length; i++) {
9270
- const x = chain[i][0];
9271
- const y = chain[i][1];
9272
- const dx = x - px;
9273
- const dy = y - py;
9274
- if (dy === 0) {
9275
- const rel = 'h' + formatNum(dx);
9276
- const abs = 'H' + formatNum(x);
9277
- d += rel.length <= abs.length ? rel : abs;
9278
- }
9279
- else if (dx === 0) {
9280
- const rel = 'v' + formatNum(dy);
9281
- const abs = 'V' + formatNum(y);
9282
- d += rel.length <= abs.length ? rel : abs;
9283
- }
9284
- else {
9285
- const rel = 'l' + formatNum(dx) + ',' + formatNum(dy);
9286
- const abs = 'L' + formatNum(x) + ',' + formatNum(y);
9287
- d += rel.length <= abs.length ? rel : abs;
9288
- }
9289
- px = x;
9290
- py = y;
9291
- }
9292
- if (close)
9293
- d += 'z';
9294
- }
9295
- return d;
9372
+ function fillAttr(color) {
9373
+ let attr = `fill="${color.rgb}"`;
9374
+ if (color.alpha < 255)
9375
+ attr += ` fill-opacity="${color.opacity.toFixed(3)}"`;
9376
+ return attr;
9296
9377
  }
9297
- function formatNum(tenths) {
9298
- if (tenths % 10 === 0)
9299
- return String(tenths / 10);
9300
- const negative = tenths < 0;
9301
- if (negative)
9302
- tenths = -tenths;
9303
- const whole = Math.floor(tenths / 10);
9304
- const frac = tenths % 10;
9305
- return (negative ? '-' : '') + String(whole) + '.' + String(frac);
9378
+ function strokeAttr(color, width) {
9379
+ let attr = `stroke="${color.rgb}" stroke-width="${width}"`;
9380
+ if (color.alpha < 255)
9381
+ attr += ` stroke-opacity="${color.opacity.toFixed(3)}"`;
9382
+ return attr;
9306
9383
  }
9307
-
9308
- class Point2D {
9309
- x;
9310
- y;
9311
- constructor(x, y) {
9312
- this.x = x;
9313
- this.y = y;
9314
- }
9315
- isZero() {
9316
- return this.x === 0 && this.y === 0;
9317
- }
9318
- scale(factor) {
9319
- this.x *= factor;
9320
- this.y *= factor;
9321
- return this;
9322
- }
9323
- translate(offset) {
9324
- this.x += offset.x;
9325
- this.y += offset.y;
9326
- return this;
9327
- }
9328
- getProject2Pixel() {
9329
- const s = Math.sin((this.y * Math.PI) / 180.0);
9330
- return new Point2D(this.x / 360.0 + 0.5, 0.5 - (0.25 * Math.log((1 + s) / (1 - s))) / Math.PI);
9331
- }
9384
+ function formatScaled(v, scale) {
9385
+ return formatNum(Math.round(v * scale * 10));
9332
9386
  }
9333
- class Feature {
9334
- type;
9335
- id;
9336
- properties;
9337
- patterns;
9338
- geometry;
9339
- constructor(opt) {
9340
- this.type = opt.type;
9341
- this.id = opt.id;
9342
- this.properties = opt.properties;
9343
- this.patterns = opt.patterns;
9344
- this.geometry = opt.geometry;
9345
- }
9346
- getBbox() {
9347
- let xMin = Infinity;
9348
- let yMin = Infinity;
9349
- let xMax = -Infinity;
9350
- let yMax = -Infinity;
9351
- this.geometry.forEach((ring) => {
9352
- ring.forEach((point) => {
9353
- if (xMin > point.x)
9354
- xMin = point.x;
9355
- if (yMin > point.y)
9356
- yMin = point.y;
9357
- if (xMax < point.x)
9358
- xMax = point.x;
9359
- if (yMax < point.y)
9360
- yMax = point.y;
9361
- });
9362
- });
9363
- return [xMin, yMin, xMax, yMax];
9364
- }
9365
- doesOverlap(bbox) {
9366
- const featureBbox = this.getBbox();
9367
- if (featureBbox[0] > bbox[2])
9368
- return false;
9369
- if (featureBbox[1] > bbox[3])
9370
- return false;
9371
- if (featureBbox[2] < bbox[0])
9372
- return false;
9373
- if (featureBbox[3] < bbox[1])
9374
- return false;
9375
- return true;
9376
- }
9387
+ function roundXY(x, y, scale) {
9388
+ return [Math.round(x * scale * 10), Math.round(y * scale * 10)];
9389
+ }
9390
+ function formatPoint(p, scale) {
9391
+ const [x, y] = roundXY(p[0], p[1], scale);
9392
+ return formatNum(x) + ',' + formatNum(y);
9377
9393
  }
9378
9394
 
9379
9395
  /*
@@ -14007,9 +14023,84 @@ function union2(features, options = {}) {
14007
14023
  else return multiPolygon(unioned, options.properties);
14008
14024
  }
14009
14025
 
14026
+ class Point2D {
14027
+ x;
14028
+ y;
14029
+ constructor(x, y) {
14030
+ this.x = x;
14031
+ this.y = y;
14032
+ }
14033
+ isZero() {
14034
+ return this.x === 0 && this.y === 0;
14035
+ }
14036
+ scale(factor) {
14037
+ this.x *= factor;
14038
+ this.y *= factor;
14039
+ return this;
14040
+ }
14041
+ translate(offset) {
14042
+ this.x += offset.x;
14043
+ this.y += offset.y;
14044
+ return this;
14045
+ }
14046
+ getProject2Pixel() {
14047
+ const s = Math.sin((this.y * Math.PI) / 180.0);
14048
+ return new Point2D(this.x / 360.0 + 0.5, 0.5 - (0.25 * Math.log((1 + s) / (1 - s))) / Math.PI);
14049
+ }
14050
+ }
14051
+ class Feature {
14052
+ type;
14053
+ id;
14054
+ properties;
14055
+ patterns;
14056
+ geometry;
14057
+ #bbox;
14058
+ constructor(opt) {
14059
+ this.type = opt.type;
14060
+ this.id = opt.id;
14061
+ this.properties = opt.properties;
14062
+ this.patterns = opt.patterns;
14063
+ this.geometry = opt.geometry;
14064
+ }
14065
+ getBbox() {
14066
+ if (this.#bbox)
14067
+ return this.#bbox;
14068
+ let xMin = Infinity;
14069
+ let yMin = Infinity;
14070
+ let xMax = -Infinity;
14071
+ let yMax = -Infinity;
14072
+ this.geometry.forEach((ring) => {
14073
+ ring.forEach((point) => {
14074
+ if (xMin > point.x)
14075
+ xMin = point.x;
14076
+ if (yMin > point.y)
14077
+ yMin = point.y;
14078
+ if (xMax < point.x)
14079
+ xMax = point.x;
14080
+ if (yMax < point.y)
14081
+ yMax = point.y;
14082
+ });
14083
+ });
14084
+ this.#bbox = [xMin, yMin, xMax, yMax];
14085
+ return this.#bbox;
14086
+ }
14087
+ doesOverlap(bbox) {
14088
+ const featureBbox = this.getBbox();
14089
+ if (featureBbox[0] > bbox[2])
14090
+ return false;
14091
+ if (featureBbox[1] > bbox[3])
14092
+ return false;
14093
+ if (featureBbox[2] < bbox[0])
14094
+ return false;
14095
+ if (featureBbox[3] < bbox[1])
14096
+ return false;
14097
+ return true;
14098
+ }
14099
+ }
14100
+
14010
14101
  function geojsonToFeature(id, polygonFeature) {
14011
14102
  const geometry = polygonFeature.geometry.coordinates.map((ring) => {
14012
- return ring.map((coord) => new Point2D(coord[0], coord[1]));
14103
+ return ring.map((coord) => new Point2D(coord[0] ?? 0, coord[1] ?? 0));
14013
14104
  });
14014
14105
  return new Feature({
14015
14106
  type: 'Polygon',
@@ -14018,7 +14109,7 @@ function geojsonToFeature(id, polygonFeature) {
14018
14109
  properties: polygonFeature.properties ?? {},
14019
14110
  });
14020
14111
  }
14021
- function mergePolygons(featureList) {
14112
+ function mergePolygonsByFeatureId(featureList) {
14022
14113
  const featuresById = new Map();
14023
14114
  let nextId = -1;
14024
14115
  for (const feature of featureList) {
@@ -14040,15 +14131,13 @@ function mergePolygons(featureList) {
14040
14131
  const turfFeatures = [];
14041
14132
  features.forEach((f) => {
14042
14133
  const rings = f.geometry.map((ring) => ring.map((p) => [p.x, p.y]));
14043
- rings.forEach((ring) => {
14044
- turfFeatures.push({
14045
- type: 'Feature',
14046
- geometry: {
14047
- type: 'Polygon',
14048
- coordinates: [ring],
14049
- },
14050
- properties: f.properties,
14051
- });
14134
+ turfFeatures.push({
14135
+ type: 'Feature',
14136
+ geometry: {
14137
+ type: 'Polygon',
14138
+ coordinates: rings,
14139
+ },
14140
+ properties: f.properties,
14052
14141
  });
14053
14142
  });
14054
14143
  const merged = union2({
@@ -14080,7 +14169,9 @@ function mergePolygons(featureList) {
14080
14169
 
14081
14170
  function calculateTileGrid(width, height, center, zoom, maxzoom) {
14082
14171
  const zoomLevel = Math.min(Math.floor(zoom), maxzoom ?? Infinity);
14083
- const tileCenterCoordinate = center.getProject2Pixel().scale(2 ** zoomLevel);
14172
+ const tileCenterCoordinate = new Point2D(center[0], center[1])
14173
+ .getProject2Pixel()
14174
+ .scale(2 ** zoomLevel);
14084
14175
  const tileSize = 2 ** (zoom - zoomLevel + 9); // 512 (2^9) is the standard tile size
14085
14176
  const tileCols = width / tileSize;
14086
14177
  const tileRows = height / tileSize;
@@ -14088,11 +14179,15 @@ function calculateTileGrid(width, height, center, zoom, maxzoom) {
14088
14179
  const tileMinY = Math.floor(tileCenterCoordinate.y - tileRows / 2);
14089
14180
  const tileMaxX = Math.floor(tileCenterCoordinate.x + tileCols / 2);
14090
14181
  const tileMaxY = Math.floor(tileCenterCoordinate.y + tileRows / 2);
14182
+ const tilesPerZoom = 2 ** zoomLevel;
14091
14183
  const tiles = [];
14092
14184
  for (let x = tileMinX; x <= tileMaxX; x++) {
14185
+ const wrappedX = ((x % tilesPerZoom) + tilesPerZoom) % tilesPerZoom;
14093
14186
  for (let y = tileMinY; y <= tileMaxY; y++) {
14187
+ if (y < 0 || y >= tilesPerZoom)
14188
+ continue;
14094
14189
  tiles.push({
14095
- x,
14190
+ x: wrappedX,
14096
14191
  y,
14097
14192
  offsetX: width / 2 + (x - tileCenterCoordinate.x) * tileSize,
14098
14193
  offsetY: height / 2 + (y - tileCenterCoordinate.y) * tileSize,
@@ -14111,8 +14206,8 @@ async function getTile(url, z, x, y) {
14111
14206
  const contentType = response.headers.get('content-type') ?? 'application/octet-stream';
14112
14207
  return { buffer, contentType };
14113
14208
  }
14114
- catch {
14115
- console.warn(`Failed to load tile: ${tileUrl}`);
14209
+ catch (error) {
14210
+ console.warn(`Failed to load tile: ${tileUrl}`, error);
14116
14211
  return null;
14117
14212
  }
14118
14213
  }
@@ -15657,24 +15752,17 @@ function writeUtf8(buf, str, pos) {
15657
15752
  }
15658
15753
 
15659
15754
  const TILE_EXTENT = 4096;
15660
- async function getLayerFeatures(job) {
15755
+ const VTFeatureType = { Unknown: 0, Point: 1, LineString: 2, Polygon: 3 };
15756
+ async function loadVectorSource(source, job, layerFeatures) {
15757
+ const tiles = source.tiles;
15758
+ if (!tiles)
15759
+ return;
15661
15760
  const { width, height } = job.renderer;
15662
15761
  const { zoom, center } = job.view;
15663
- const { sources } = job.style;
15664
- const source = sources['versatiles-shortbread'];
15665
- if (!source)
15666
- return new Map();
15667
- if (source.type !== 'vector' || !source.tiles) {
15668
- console.error('Invalid source configuration. Expected a vector source with tile URLs.');
15669
- console.error('Source config:', source);
15670
- throw Error('Invalid source');
15671
- }
15672
- const sourceUrl = source.tiles[0];
15673
15762
  const { zoomLevel, tileSize, tiles: tileCoordinates, } = calculateTileGrid(width, height, center, zoom, source.maxzoom);
15674
- const layerFeatures = new Map();
15675
15763
  await Promise.all(tileCoordinates.map(async ({ x, y, offsetX, offsetY }) => {
15676
15764
  const offset = new Point2D(offsetX, offsetY);
15677
- const tile = await getTile(sourceUrl, zoomLevel, x, y);
15765
+ const tile = await getTile(tiles[0], zoomLevel, x, y);
15678
15766
  if (!tile)
15679
15767
  return;
15680
15768
  const vectorTile = new VectorTile(new Pbf(tile.buffer));
@@ -15692,17 +15780,17 @@ async function getLayerFeatures(job) {
15692
15780
  let type;
15693
15781
  let list;
15694
15782
  switch (featureSrc.type) {
15695
- case 0: //Unknown
15783
+ case VTFeatureType.Unknown:
15696
15784
  throw Error('Unknown feature type in vector tile');
15697
- case 1: //Point
15785
+ case VTFeatureType.Point:
15698
15786
  type = 'Point';
15699
15787
  list = features.points;
15700
15788
  break;
15701
- case 2: //LineString
15789
+ case VTFeatureType.LineString:
15702
15790
  type = 'LineString';
15703
15791
  list = features.linestrings;
15704
15792
  break;
15705
- case 3: //Polygon
15793
+ case VTFeatureType.Polygon:
15706
15794
  type = 'Polygon';
15707
15795
  list = features.polygons;
15708
15796
  break;
@@ -15718,14 +15806,109 @@ async function getLayerFeatures(job) {
15718
15806
  }
15719
15807
  }
15720
15808
  }));
15721
- for (const [name, features] of layerFeatures) {
15722
- layerFeatures.set(name, {
15723
- points: features.points,
15724
- linestrings: features.linestrings,
15725
- polygons: mergePolygons(features.polygons),
15726
- });
15809
+ }
15810
+
15811
+ function loadGeoJSONSource(options) {
15812
+ const { sourceName, data, width, height, zoom, center, layerFeatures } = options;
15813
+ const existing = layerFeatures.get(sourceName);
15814
+ const features = existing ?? { points: [], linestrings: [], polygons: [] };
15815
+ if (!existing)
15816
+ layerFeatures.set(sourceName, features);
15817
+ const worldSize = 512 * 2 ** zoom;
15818
+ const centerMercator = new Point2D(center[0], center[1]).getProject2Pixel();
15819
+ function projectCoord(coord) {
15820
+ const mercator = new Point2D(coord[0], coord[1]).getProject2Pixel();
15821
+ return new Point2D((mercator.x - centerMercator.x) * worldSize + width / 2, (mercator.y - centerMercator.y) * worldSize + height / 2);
15822
+ }
15823
+ function makeFeature(type, geometry, id, properties) {
15824
+ const feature = new Feature({ type, geometry, id, properties });
15825
+ if (!feature.doesOverlap([0, 0, width, height]))
15826
+ return null;
15827
+ return feature;
15828
+ }
15829
+ function extractPoints(geometry) {
15830
+ return geometry.flatMap((ring) => ring.map((p) => [p]));
15831
+ }
15832
+ function addFeature(type, geometry, id, properties) {
15833
+ switch (type) {
15834
+ case 'Point': {
15835
+ const f = makeFeature('Point', geometry, id, properties);
15836
+ if (f)
15837
+ features.points.push(f);
15838
+ break;
15839
+ }
15840
+ case 'LineString': {
15841
+ const f = makeFeature('LineString', geometry, id, properties);
15842
+ if (f) {
15843
+ features.linestrings.push(f);
15844
+ features.points.push(new Feature({ type: 'Point', geometry: extractPoints(geometry), id, properties }));
15845
+ }
15846
+ break;
15847
+ }
15848
+ case 'Polygon': {
15849
+ geometry.forEach((ring, ringIndex) => {
15850
+ const needsCW = ringIndex === 0;
15851
+ let area = 0;
15852
+ for (let i = 0; i < ring.length; i++) {
15853
+ const j = (i + 1) % ring.length;
15854
+ area += ring[i].x * ring[j].y;
15855
+ area -= ring[j].x * ring[i].y;
15856
+ }
15857
+ if (area < 0 !== needsCW)
15858
+ ring.reverse();
15859
+ });
15860
+ const f = makeFeature('Polygon', geometry, id, properties);
15861
+ if (f) {
15862
+ features.polygons.push(f);
15863
+ features.linestrings.push(new Feature({ type: 'LineString', geometry, id, properties }));
15864
+ features.points.push(new Feature({ type: 'Point', geometry: extractPoints(geometry), id, properties }));
15865
+ }
15866
+ break;
15867
+ }
15868
+ }
15869
+ }
15870
+ function processGeometry(geom, id, properties) {
15871
+ switch (geom.type) {
15872
+ case 'Point':
15873
+ addFeature('Point', [[projectCoord(geom.coordinates)]], id, properties);
15874
+ break;
15875
+ case 'MultiPoint':
15876
+ addFeature('Point', geom.coordinates.map((c) => [projectCoord(c)]), id, properties);
15877
+ break;
15878
+ case 'LineString':
15879
+ addFeature('LineString', [geom.coordinates.map((c) => projectCoord(c))], id, properties);
15880
+ break;
15881
+ case 'MultiLineString':
15882
+ addFeature('LineString', geom.coordinates.map((line) => line.map((c) => projectCoord(c))), id, properties);
15883
+ break;
15884
+ case 'Polygon':
15885
+ addFeature('Polygon', geom.coordinates.map((ring) => ring.map((c) => projectCoord(c))), id, properties);
15886
+ break;
15887
+ case 'MultiPolygon':
15888
+ for (const polygon of geom.coordinates) {
15889
+ addFeature('Polygon', polygon.map((ring) => ring.map((c) => projectCoord(c))), id, properties);
15890
+ }
15891
+ break;
15892
+ case 'GeometryCollection':
15893
+ for (const g of geom.geometries) {
15894
+ processGeometry(g, id, properties);
15895
+ }
15896
+ break;
15897
+ }
15898
+ }
15899
+ switch (data.type) {
15900
+ case 'FeatureCollection':
15901
+ for (const f of data.features) {
15902
+ processGeometry(f.geometry, f.id, (f.properties ?? {}));
15903
+ }
15904
+ break;
15905
+ case 'Feature':
15906
+ processGeometry(data.geometry, data.id, (data.properties ?? {}));
15907
+ break;
15908
+ default:
15909
+ processGeometry(data, undefined, {});
15910
+ break;
15727
15911
  }
15728
- return layerFeatures;
15729
15912
  }
15730
15913
 
15731
15914
  async function getRasterTiles(job, sourceName) {
@@ -15733,7 +15916,7 @@ async function getRasterTiles(job, sourceName) {
15733
15916
  const { zoom, center } = job.view;
15734
15917
  const source = job.style.sources[sourceName];
15735
15918
  if (source?.type !== 'raster' || !source.tiles) {
15736
- throw Error('Invalid raster source: ' + sourceName);
15919
+ throw Error(`Invalid raster source "${sourceName}": expected type "raster" with a "tiles" array`);
15737
15920
  }
15738
15921
  const sourceUrl = source.tiles[0];
15739
15922
  const { zoomLevel, tileSize, tiles } = calculateTileGrid(width, height, center, zoom, source.maxzoom);
@@ -15756,6 +15939,44 @@ async function getRasterTiles(job, sourceName) {
15756
15939
  return rasterTiles.filter((tile) => tile !== null);
15757
15940
  }
15758
15941
 
15942
+ async function getLayerFeatures(job) {
15943
+ const { width, height } = job.renderer;
15944
+ const { zoom, center } = job.view;
15945
+ const { sources } = job.style;
15946
+ const layerFeatures = new Map();
15947
+ const loadPromises = [];
15948
+ for (const [sourceName, sourceSpec] of Object.entries(sources)) {
15949
+ const source = sourceSpec;
15950
+ switch (source.type) {
15951
+ case 'vector':
15952
+ loadPromises.push(loadVectorSource(source, job, layerFeatures));
15953
+ break;
15954
+ case 'geojson':
15955
+ if (source.data) {
15956
+ loadGeoJSONSource({
15957
+ sourceName,
15958
+ data: source.data,
15959
+ width,
15960
+ height,
15961
+ zoom,
15962
+ center,
15963
+ layerFeatures,
15964
+ });
15965
+ }
15966
+ break;
15967
+ }
15968
+ }
15969
+ await Promise.all(loadPromises);
15970
+ for (const [name, features] of layerFeatures) {
15971
+ layerFeatures.set(name, {
15972
+ points: features.points,
15973
+ linestrings: features.linestrings,
15974
+ polygons: mergePolygonsByFeatureId(features.polygons),
15975
+ });
15976
+ }
15977
+ return layerFeatures;
15978
+ }
15979
+
15759
15980
  /**
15760
15981
  * Wraps a source/composite expression for per-feature evaluation.
15761
15982
  */
@@ -15798,6 +16019,7 @@ class StyleLayer {
15798
16019
  minzoom;
15799
16020
  maxzoom;
15800
16021
  filter;
16022
+ filterFn;
15801
16023
  paint;
15802
16024
  layout;
15803
16025
  paintExpressions;
@@ -15816,6 +16038,7 @@ class StyleLayer {
15816
16038
  this.source = spec.source;
15817
16039
  this.sourceLayer = spec['source-layer'];
15818
16040
  this.filter = spec.filter;
16041
+ this.filterFn = featureFilter(this.filter);
15819
16042
  }
15820
16043
  this.visibility = (spec.layout?.visibility ?? 'visible');
15821
16044
  // Initialize paint property expressions
@@ -15853,7 +16076,7 @@ class StyleLayer {
15853
16076
  this.layout = new EvaluatedProperties();
15854
16077
  for (const [name, expr] of this.paintExpressions) {
15855
16078
  if (expr.kind === 'constant' || expr.kind === 'camera') {
15856
- this.paint.set(name, expr.evaluate(params, null, {}, undefined, availableImages));
16079
+ this.paint.set(name, expr.evaluate(params, undefined, {}, undefined, availableImages));
15857
16080
  }
15858
16081
  else {
15859
16082
  this.paint.set(name, new PossiblyEvaluatedPropertyValue(expr, params));
@@ -15861,7 +16084,7 @@ class StyleLayer {
15861
16084
  }
15862
16085
  for (const [name, expr] of this.layoutExpressions) {
15863
16086
  if (expr.kind === 'constant' || expr.kind === 'camera') {
15864
- this.layout.set(name, expr.evaluate(params, null, {}, undefined, availableImages));
16087
+ this.layout.set(name, expr.evaluate(params, undefined, {}, undefined, availableImages));
15865
16088
  }
15866
16089
  else {
15867
16090
  this.layout.set(name, new PossiblyEvaluatedPropertyValue(expr, params));
@@ -15872,18 +16095,17 @@ class StyleLayer {
15872
16095
  function createStyleLayer(spec) {
15873
16096
  return new StyleLayer(spec);
15874
16097
  }
15875
-
15876
16098
  function getLayerStyles(layers) {
15877
- return layers.map((layerSpecification) => {
15878
- const styleLayer = createStyleLayer(layerSpecification);
15879
- return styleLayer;
15880
- });
16099
+ return layers.map(createStyleLayer);
15881
16100
  }
15882
16101
 
15883
- async function renderVectorTiles(job) {
16102
+ async function renderMap(job) {
15884
16103
  await render(job);
15885
16104
  return job.renderer.getString();
15886
16105
  }
16106
+ function getFeatures(layerFeatures, layerStyle) {
16107
+ return layerFeatures.get(layerStyle.sourceLayer) ?? layerFeatures.get(layerStyle.source);
16108
+ }
15887
16109
  async function render(job) {
15888
16110
  const { renderer } = job;
15889
16111
  const { zoom } = job.view;
@@ -15914,54 +16136,55 @@ async function render(job) {
15914
16136
  case 'background':
15915
16137
  {
15916
16138
  renderer.drawBackgroundFill({
15917
- color: new Color(getPaint('background-color')),
16139
+ color: getPaint('background-color'),
15918
16140
  opacity: getPaint('background-opacity'),
15919
16141
  });
15920
16142
  }
15921
16143
  continue;
15922
16144
  case 'fill':
15923
16145
  {
15924
- const polygons = layerFeatures.get(layerStyle.sourceLayer)?.polygons;
16146
+ const polygons = getFeatures(layerFeatures, layerStyle)?.polygons;
15925
16147
  if (!polygons || polygons.length === 0)
15926
16148
  continue;
15927
- const filter = featureFilter(layerStyle.filter);
15928
- const polygonFeatures = polygons.filter((feature) => filter.filter({ zoom }, feature));
16149
+ const polygonFeatures = layerStyle.filterFn
16150
+ ? polygons.filter((feature) => layerStyle.filterFn.filter({ zoom }, feature))
16151
+ : polygons;
15929
16152
  if (polygonFeatures.length === 0)
15930
16153
  continue;
15931
16154
  renderer.drawPolygons(polygonFeatures.map((feature) => [
15932
16155
  feature,
15933
16156
  {
15934
- color: new Color(getPaint('fill-color', feature)),
15935
- translate: new Point2D(...getPaint('fill-translate', feature)),
16157
+ color: getPaint('fill-color', feature),
16158
+ opacity: getPaint('fill-opacity', feature),
16159
+ translate: getPaint('fill-translate', feature),
15936
16160
  },
15937
- ]), getPaint('fill-opacity', polygonFeatures[0]));
16161
+ ]));
15938
16162
  }
15939
16163
  continue;
15940
16164
  case 'line':
15941
16165
  {
15942
- const lineStrings = layerFeatures.get(layerStyle.sourceLayer)?.linestrings;
16166
+ const lineStrings = getFeatures(layerFeatures, layerStyle)?.linestrings;
15943
16167
  if (!lineStrings || lineStrings.length === 0)
15944
16168
  continue;
15945
- const filter = featureFilter(layerStyle.filter);
15946
- const lineStringFeatures = lineStrings.filter((feature) => filter.filter({ zoom }, feature));
16169
+ const lineStringFeatures = layerStyle.filterFn
16170
+ ? lineStrings.filter((feature) => layerStyle.filterFn.filter({ zoom }, feature))
16171
+ : lineStrings;
15947
16172
  if (lineStringFeatures.length === 0)
15948
16173
  continue;
15949
16174
  renderer.drawLineStrings(lineStringFeatures.map((feature) => [
15950
16175
  feature,
15951
16176
  {
15952
- color: new Color(getPaint('line-color', feature)),
15953
- translate: new Point2D(...getPaint('line-translate', feature)),
15954
- blur: getPaint('line-blur', feature),
16177
+ color: getPaint('line-color', feature),
16178
+ translate: getPaint('line-translate', feature),
15955
16179
  cap: getLayout('line-cap', feature),
15956
16180
  dasharray: getPaint('line-dasharray', feature),
15957
- gapWidth: getPaint('line-gap-width', feature),
15958
16181
  join: getLayout('line-join', feature),
15959
16182
  miterLimit: getLayout('line-miter-limit', feature),
15960
16183
  offset: getPaint('line-offset', feature),
15961
- roundLimit: getLayout('line-round-limit', feature),
16184
+ opacity: getPaint('line-opacity', feature),
15962
16185
  width: getPaint('line-width', feature),
15963
16186
  },
15964
- ]), getPaint('line-opacity', lineStringFeatures[0]));
16187
+ ]));
15965
16188
  }
15966
16189
  continue;
15967
16190
  case 'raster':
@@ -15979,6 +16202,28 @@ async function render(job) {
15979
16202
  }
15980
16203
  continue;
15981
16204
  case 'circle':
16205
+ {
16206
+ const points = getFeatures(layerFeatures, layerStyle)?.points;
16207
+ if (!points || points.length === 0)
16208
+ continue;
16209
+ const pointFeatures = layerStyle.filterFn
16210
+ ? points.filter((feature) => layerStyle.filterFn.filter({ zoom }, feature))
16211
+ : points;
16212
+ if (pointFeatures.length === 0)
16213
+ continue;
16214
+ renderer.drawCircles(pointFeatures.map((feature) => [
16215
+ feature,
16216
+ {
16217
+ color: getPaint('circle-color', feature),
16218
+ opacity: getPaint('circle-opacity', feature),
16219
+ radius: getPaint('circle-radius', feature),
16220
+ translate: getPaint('circle-translate', feature),
16221
+ strokeWidth: getPaint('circle-stroke-width', feature),
16222
+ strokeColor: getPaint('circle-stroke-color', feature),
16223
+ },
16224
+ ]));
16225
+ }
16226
+ continue;
15982
16227
  case 'color-relief':
15983
16228
  case 'fill-extrusion':
15984
16229
  case 'heatmap':
@@ -15992,15 +16237,20 @@ async function render(job) {
15992
16237
  }
15993
16238
 
15994
16239
  async function renderToSVG(options) {
15995
- return await renderVectorTiles({
15996
- renderer: new SVGRenderer({
15997
- width: options.width ?? 1024,
15998
- height: options.height ?? 1024,
15999
- scale: options.scale ?? 1,
16000
- }),
16240
+ const width = options.width ?? 1024;
16241
+ const height = options.height ?? 1024;
16242
+ const scale = options.scale ?? 1;
16243
+ if (width <= 0)
16244
+ throw new Error('width must be positive');
16245
+ if (height <= 0)
16246
+ throw new Error('height must be positive');
16247
+ if (scale <= 0)
16248
+ throw new Error('scale must be positive');
16249
+ return await renderMap({
16250
+ renderer: new SVGRenderer({ width, height, scale }),
16001
16251
  style: options.style,
16002
16252
  view: {
16003
- center: new Point2D(options.lon ?? 0, options.lat ?? 0),
16253
+ center: [options.lon ?? 0, options.lat ?? 0],
16004
16254
  zoom: options.zoom ?? 2,
16005
16255
  },
16006
16256
  });