@versatiles/svg-renderer 0.4.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.cjs CHANGED
@@ -9033,7 +9033,9 @@ class Color {
9033
9033
  this.values = [args[0], args[1], args[2], args[3]];
9034
9034
  return;
9035
9035
  }
9036
- throw Error('Unsupported Color arguments: ' + JSON.stringify(args));
9036
+ throw Error('Unsupported Color arguments: ' +
9037
+ JSON.stringify(args) +
9038
+ '. Expected a MaplibreColor, hex string (#RRGGBB or #RRGGBBAA), or 3-4 numeric components.');
9037
9039
  function h2d(text) {
9038
9040
  return parseInt(text, 16);
9039
9041
  }
@@ -9043,25 +9045,9 @@ class Color {
9043
9045
  }
9044
9046
  get hex() {
9045
9047
  return `#${d2h(this.values[0])}${d2h(this.values[1])}${d2h(this.values[2])}${this.values[3] === 255 ? '' : d2h(this.values[3])}`;
9046
- function d2h(num) {
9047
- if (num < 0)
9048
- num = 0;
9049
- if (num > 255)
9050
- num = 255;
9051
- const str = Math.round(num).toString(16).toUpperCase();
9052
- return str.length < 2 ? '0' + str : str;
9053
- }
9054
9048
  }
9055
9049
  get rgb() {
9056
9050
  return `#${d2h(this.values[0])}${d2h(this.values[1])}${d2h(this.values[2])}`;
9057
- function d2h(num) {
9058
- if (num < 0)
9059
- num = 0;
9060
- if (num > 255)
9061
- num = 255;
9062
- const str = Math.round(num).toString(16).toUpperCase();
9063
- return str.length < 2 ? '0' + str : str;
9064
- }
9065
9051
  }
9066
9052
  get opacity() {
9067
9053
  return this.values[3] / 255;
@@ -9076,6 +9062,126 @@ class Color {
9076
9062
  return new Color(...this.values);
9077
9063
  }
9078
9064
  }
9065
+ function d2h(num) {
9066
+ if (num < 0)
9067
+ num = 0;
9068
+ if (num > 255)
9069
+ num = 255;
9070
+ const str = Math.round(num).toString(16).toUpperCase();
9071
+ return str.length < 2 ? '0' + str : str;
9072
+ }
9073
+
9074
+ function chainSegments(segments) {
9075
+ // Phase 1: normalize segments left-to-right, then chain
9076
+ normalizeSegments(segments, 0);
9077
+ let chains = greedyChain(segments);
9078
+ // Phase 2: normalize remaining chains top-to-bottom, then chain again
9079
+ normalizeSegments(chains, 1);
9080
+ chains = greedyChain(chains);
9081
+ return chains;
9082
+ }
9083
+ function normalizeSegments(segments, coordIndex) {
9084
+ for (const seg of segments) {
9085
+ const first = seg[0];
9086
+ const last = seg[seg.length - 1];
9087
+ if (first && last && last[coordIndex] < first[coordIndex])
9088
+ seg.reverse();
9089
+ }
9090
+ }
9091
+ function greedyChain(segments) {
9092
+ const byStart = new Map();
9093
+ for (const seg of segments) {
9094
+ const start = seg[0];
9095
+ if (!start)
9096
+ continue;
9097
+ const key = String(start[0]) + ',' + String(start[1]);
9098
+ let list = byStart.get(key);
9099
+ if (!list) {
9100
+ list = [];
9101
+ byStart.set(key, list);
9102
+ }
9103
+ list.push(seg);
9104
+ }
9105
+ const visited = new Set();
9106
+ const chains = [];
9107
+ for (const seg of segments) {
9108
+ if (visited.has(seg))
9109
+ continue;
9110
+ visited.add(seg);
9111
+ const chain = [...seg];
9112
+ let endPoint = chain[chain.length - 1];
9113
+ let candidates = endPoint
9114
+ ? byStart.get(String(endPoint[0]) + ',' + String(endPoint[1]))
9115
+ : undefined;
9116
+ while (candidates) {
9117
+ let next;
9118
+ for (const c of candidates) {
9119
+ if (!visited.has(c)) {
9120
+ next = c;
9121
+ break;
9122
+ }
9123
+ }
9124
+ if (!next)
9125
+ break;
9126
+ visited.add(next);
9127
+ for (let i = 1; i < next.length; i++)
9128
+ chain.push(next[i]);
9129
+ endPoint = chain[chain.length - 1];
9130
+ candidates = endPoint
9131
+ ? byStart.get(String(endPoint[0]) + ',' + String(endPoint[1]))
9132
+ : undefined;
9133
+ }
9134
+ chains.push(chain);
9135
+ }
9136
+ return chains;
9137
+ }
9138
+ function segmentsToPath(chains, close = false) {
9139
+ let d = '';
9140
+ for (const chain of chains) {
9141
+ const first = chain[0];
9142
+ if (!first)
9143
+ continue;
9144
+ d += 'M' + formatNum(first[0]) + ',' + formatNum(first[1]);
9145
+ let px = first[0];
9146
+ let py = first[1];
9147
+ for (let i = 1; i < chain.length; i++) {
9148
+ const x = chain[i][0];
9149
+ const y = chain[i][1];
9150
+ const dx = x - px;
9151
+ const dy = y - py;
9152
+ if (dy === 0) {
9153
+ const rel = 'h' + formatNum(dx);
9154
+ const abs = 'H' + formatNum(x);
9155
+ d += rel.length <= abs.length ? rel : abs;
9156
+ }
9157
+ else if (dx === 0) {
9158
+ const rel = 'v' + formatNum(dy);
9159
+ const abs = 'V' + formatNum(y);
9160
+ d += rel.length <= abs.length ? rel : abs;
9161
+ }
9162
+ else {
9163
+ const rel = 'l' + formatNum(dx) + ',' + formatNum(dy);
9164
+ const abs = 'L' + formatNum(x) + ',' + formatNum(y);
9165
+ d += rel.length <= abs.length ? rel : abs;
9166
+ }
9167
+ px = x;
9168
+ py = y;
9169
+ }
9170
+ if (close)
9171
+ d += 'z';
9172
+ }
9173
+ return d;
9174
+ }
9175
+ function formatNum(tenths) {
9176
+ if (tenths % 10 === 0)
9177
+ return String(tenths / 10);
9178
+ const negative = tenths < 0;
9179
+ if (negative)
9180
+ tenths = -tenths;
9181
+ const whole = Math.floor(tenths / 10);
9182
+ const frac = tenths % 10;
9183
+ return (negative ? '-' : '') + String(whole) + '.' + String(frac);
9184
+ }
9079
9185
 
9080
9186
  class SVGRenderer {
9081
9187
  width;
@@ -9091,77 +9197,86 @@ class SVGRenderer {
9091
9197
  this.#backgroundColor = Color.transparent;
9092
9198
  }
9093
9199
  drawBackgroundFill(style) {
9094
- const color = style.color.clone();
9200
+ const color = new Color(style.color);
9095
9201
  color.alpha *= style.opacity;
9096
9202
  this.#backgroundColor = color;
9097
9203
  }
9098
- drawPolygons(features, opacity) {
9204
+ drawPolygons(features) {
9099
9205
  if (features.length === 0)
9100
9206
  return;
9101
- if (opacity <= 0)
9102
- return;
9103
- this.#svg.push(`<g opacity="${String(opacity)}">`);
9104
9207
  const groups = new Map();
9105
9208
  features.forEach(([feature, style]) => {
9106
- if (style.color.alpha <= 0)
9209
+ if (style.opacity <= 0)
9210
+ return;
9211
+ const color = new Color(style.color);
9212
+ if (color.alpha <= 0)
9107
9213
  return;
9108
- const translate = style.translate.isZero()
9214
+ const translate = style.translate[0] === 0 && style.translate[1] === 0
9109
9215
  ? ''
9110
9216
  : ` transform="translate(${formatPoint(style.translate, this.#scale)})"`;
9111
- const key = style.color.hex + translate;
9217
+ const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
9218
+ const key = color.hex + translate + opacityAttr;
9112
9219
  let group = groups.get(key);
9113
9220
  if (!group) {
9114
- group = { segments: [], attrs: `${fillAttr(style.color)}${translate}` };
9221
+ group = { segments: [], attrs: `${fillAttr(color)}${translate}${opacityAttr}` };
9115
9222
  groups.set(key, group);
9116
9223
  }
9117
9224
  feature.geometry.forEach((ring) => {
9118
- group.segments.push(ring.map((p) => roundXY(p, this.#scale)));
9225
+ group.segments.push(ring.map((p) => roundXY(p.x, p.y, this.#scale)));
9119
9226
  });
9120
9227
  });
9121
9228
  for (const { segments, attrs } of groups.values()) {
9122
9229
  const d = segmentsToPath(segments, true);
9123
9230
  this.#svg.push(`<path d="${d}" ${attrs} />`);
9124
9231
  }
9125
- this.#svg.push('</g>');
9126
9232
  }
9127
- drawLineStrings(features, opacity) {
9233
+ drawLineStrings(features) {
9128
9234
  if (features.length === 0)
9129
9235
  return;
9130
- if (opacity <= 0)
9131
- return;
9132
- this.#svg.push(`<g opacity="${String(opacity)}">`);
9133
9236
  const groups = new Map();
9134
9237
  features.forEach(([feature, style]) => {
9135
- if (style.width <= 0 || style.color.alpha <= 0)
9238
+ if (style.opacity <= 0)
9239
+ return;
9240
+ const color = new Color(style.color);
9241
+ if (style.width <= 0 || color.alpha <= 0)
9136
9242
  return;
9137
- const translate = style.translate.isZero()
9243
+ const translate = style.translate[0] === 0 && style.translate[1] === 0
9138
9244
  ? ''
9139
9245
  : ` transform="translate(${formatPoint(style.translate, this.#scale)})"`;
9140
- const roundedWidth = roundValue(style.width, this.#scale);
9246
+ const roundedWidth = formatScaled(style.width, this.#scale);
9247
+ const dasharrayStr = style.dasharray
9248
+ ? style.dasharray.map((v) => formatScaled(v * style.width, this.#scale)).join(',')
9249
+ : '';
9250
+ const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
9141
9251
  const key = [
9142
- style.color.hex,
9252
+ color.hex,
9143
9253
  roundedWidth,
9144
9254
  style.cap,
9145
9255
  style.join,
9146
9256
  String(style.miterLimit),
9257
+ dasharrayStr,
9258
+ opacityAttr,
9147
9259
  translate,
9148
9260
  ].join('\0');
9149
9261
  let group = groups.get(key);
9150
9262
  if (!group) {
9263
+ const attrs = [
9264
+ 'fill="none"',
9265
+ strokeAttr(color, roundedWidth),
9266
+ `stroke-linecap="${style.cap}"`,
9267
+ `stroke-linejoin="${style.join}"`,
9268
+ `stroke-miterlimit="${String(style.miterLimit)}"`,
9269
+ ];
9270
+ if (dasharrayStr)
9271
+ attrs.push(`stroke-dasharray="${dasharrayStr}"`);
9151
9272
  group = {
9152
9273
  segments: [],
9153
- attrs: [
9154
- 'fill="none"',
9155
- strokeAttr(style.color, roundedWidth),
9156
- `stroke-linecap="${style.cap}"`,
9157
- `stroke-linejoin="${style.join}"`,
9158
- `stroke-miterlimit="${String(style.miterLimit)}"`,
9159
- ].join(' ') + translate,
9274
+ attrs: attrs.join(' ') + translate + opacityAttr,
9160
9275
  };
9161
9276
  groups.set(key, group);
9162
9277
  }
9163
9278
  feature.geometry.forEach((line) => {
9164
- group.segments.push(line.map((p) => roundXY(p, this.#scale)));
9279
+ group.segments.push(line.map((p) => roundXY(p.x, p.y, this.#scale)));
9165
9280
  });
9166
9281
  });
9167
9282
  for (const { segments, attrs } of groups.values()) {
@@ -9169,36 +9284,39 @@ class SVGRenderer {
9169
9284
  const d = segmentsToPath(chains);
9170
9285
  this.#svg.push(`<path d="${d}" ${attrs} />`);
9171
9286
  }
9172
- this.#svg.push('</g>');
9173
9287
  }
9174
- drawCircles(features, opacity) {
9288
+ drawCircles(features) {
9175
9289
  if (features.length === 0)
9176
9290
  return;
9177
- if (opacity <= 0)
9178
- return;
9179
- this.#svg.push(`<g opacity="${String(opacity)}">`);
9180
9291
  const groups = new Map();
9181
9292
  features.forEach(([feature, style]) => {
9182
- if (style.radius <= 0 || style.color.alpha <= 0)
9293
+ if (style.opacity <= 0)
9294
+ return;
9295
+ const color = new Color(style.color);
9296
+ if (style.radius <= 0 || color.alpha <= 0)
9183
9297
  return;
9184
- const translate = style.translate.isZero()
9298
+ const translate = style.translate[0] === 0 && style.translate[1] === 0
9185
9299
  ? ''
9186
9300
  : ` transform="translate(${formatPoint(style.translate, this.#scale)})"`;
9187
- const roundedRadius = roundValue(style.radius, this.#scale);
9301
+ const roundedRadius = formatScaled(style.radius, this.#scale);
9302
+ const strokeColor = new Color(style.strokeColor);
9188
9303
  const strokeAttrs = style.strokeWidth > 0
9189
- ? ` ${strokeAttr(style.strokeColor, roundValue(style.strokeWidth, this.#scale))}`
9304
+ ? ` ${strokeAttr(strokeColor, formatScaled(style.strokeWidth, this.#scale))}`
9190
9305
  : '';
9191
- const key = [style.color.hex, roundedRadius, strokeAttrs, translate].join('\0');
9306
+ const opacityAttr = style.opacity < 1 ? ` opacity="${style.opacity.toFixed(3)}"` : '';
9307
+ const key = [color.hex, roundedRadius, strokeAttrs, opacityAttr, translate].join('\0');
9192
9308
  let group = groups.get(key);
9193
9309
  if (!group) {
9194
9310
  group = {
9195
9311
  points: [],
9196
- attrs: `r="${roundedRadius}" ${fillAttr(style.color)}${strokeAttrs}${translate}`,
9312
+ attrs: `r="${roundedRadius}" ${fillAttr(color)}${strokeAttrs}${translate}${opacityAttr}`,
9197
9313
  };
9198
9314
  groups.set(key, group);
9199
9315
  }
9200
9316
  feature.geometry.forEach((ring) => {
9201
- group.points.push(roundXY(ring[0], this.#scale));
9317
+ const p = ring[0];
9318
+ if (p)
9319
+ group.points.push(roundXY(p.x, p.y, this.#scale));
9202
9320
  });
9203
9321
  });
9204
9322
  for (const { points, attrs } of groups.values()) {
@@ -9206,7 +9324,6 @@ class SVGRenderer {
9206
9324
  this.#svg.push(`<circle cx="${formatNum(x)}" cy="${formatNum(y)}" ${attrs} />`);
9207
9325
  }
9208
9326
  }
9209
- this.#svg.push('</g>');
9210
9327
  }
9211
9328
  drawRasterTiles(tiles, style) {
9212
9329
  if (tiles.length === 0)
@@ -9232,7 +9349,7 @@ class SVGRenderer {
9232
9349
  for (const tile of tiles) {
9233
9350
  const overlap = Math.min(tile.width, tile.height) / 10000; // slight overlap to prevent sub-pixel gaps between tiles
9234
9351
  const s = this.#scale;
9235
- 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}"`;
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}"`;
9236
9353
  if (pixelated)
9237
9354
  attrs += ' style="image-rendering:pixelated"';
9238
9355
  this.#svg.push(`<image ${attrs} />`);
@@ -9266,115 +9383,16 @@ function strokeAttr(color, width) {
9266
9383
  attr += ` stroke-opacity="${color.opacity.toFixed(3)}"`;
9267
9384
  return attr;
9268
9385
  }
9269
- function roundValue(v, scale) {
9270
- return (v * scale).toFixed(3);
9386
+ function formatScaled(v, scale) {
9387
+ return formatNum(Math.round(v * scale * 10));
9271
9388
  }
9272
- function roundXY(p, scale) {
9273
- return [Math.round(p.x * scale * 10), Math.round(p.y * scale * 10)];
9389
+ function roundXY(x, y, scale) {
9390
+ return [Math.round(x * scale * 10), Math.round(y * scale * 10)];
9274
9391
  }
9275
9392
  function formatPoint(p, scale) {
9276
- const [x, y] = roundXY(p, scale);
9393
+ const [x, y] = roundXY(p[0], p[1], scale);
9277
9394
  return formatNum(x) + ',' + formatNum(y);
9278
9395
  }
9279
- function chainSegments(segments) {
9280
- // Phase 1: normalize segments left-to-right, then chain
9281
- normalizeSegments(segments, 0);
9282
- let chains = greedyChain(segments);
9283
- // Phase 2: normalize remaining chains top-to-bottom, then chain again
9284
- normalizeSegments(chains, 1);
9285
- chains = greedyChain(chains);
9286
- return chains;
9287
- }
9288
- function normalizeSegments(segments, coordIndex) {
9289
- for (const seg of segments) {
9290
- if (seg[seg.length - 1][coordIndex] < seg[0][coordIndex])
9291
- seg.reverse();
9292
- }
9293
- }
9294
- function greedyChain(segments) {
9295
- const byStart = new Map();
9296
- for (const seg of segments) {
9297
- const key = String(seg[0][0]) + ',' + String(seg[0][1]);
9298
- let list = byStart.get(key);
9299
- if (!list) {
9300
- list = [];
9301
- byStart.set(key, list);
9302
- }
9303
- list.push(seg);
9304
- }
9305
- const visited = new Set();
9306
- const chains = [];
9307
- for (const seg of segments) {
9308
- if (visited.has(seg))
9309
- continue;
9310
- visited.add(seg);
9311
- const chain = [...seg];
9312
- let endPoint = chain[chain.length - 1];
9313
- let candidates = byStart.get(String(endPoint[0]) + ',' + String(endPoint[1]));
9314
- while (candidates) {
9315
- let next;
9316
- for (const c of candidates) {
9317
- if (!visited.has(c)) {
9318
- next = c;
9319
- break;
9320
- }
9321
- }
9322
- if (!next)
9323
- break;
9324
- visited.add(next);
9325
- for (let i = 1; i < next.length; i++)
9326
- chain.push(next[i]);
9327
- endPoint = chain[chain.length - 1];
9328
- candidates = byStart.get(String(endPoint[0]) + ',' + String(endPoint[1]));
9329
- }
9330
- chains.push(chain);
9331
- }
9332
- return chains;
9333
- }
9334
- function segmentsToPath(chains, close = false) {
9335
- let d = '';
9336
- for (const chain of chains) {
9337
- d += 'M' + formatNum(chain[0][0]) + ',' + formatNum(chain[0][1]);
9338
- let px = chain[0][0];
9339
- let py = chain[0][1];
9340
- for (let i = 1; i < chain.length; i++) {
9341
- const x = chain[i][0];
9342
- const y = chain[i][1];
9343
- const dx = x - px;
9344
- const dy = y - py;
9345
- if (dy === 0) {
9346
- const rel = 'h' + formatNum(dx);
9347
- const abs = 'H' + formatNum(x);
9348
- d += rel.length <= abs.length ? rel : abs;
9349
- }
9350
- else if (dx === 0) {
9351
- const rel = 'v' + formatNum(dy);
9352
- const abs = 'V' + formatNum(y);
9353
- d += rel.length <= abs.length ? rel : abs;
9354
- }
9355
- else {
9356
- const rel = 'l' + formatNum(dx) + ',' + formatNum(dy);
9357
- const abs = 'L' + formatNum(x) + ',' + formatNum(y);
9358
- d += rel.length <= abs.length ? rel : abs;
9359
- }
9360
- px = x;
9361
- py = y;
9362
- }
9363
- if (close)
9364
- d += 'z';
9365
- }
9366
- return d;
9367
- }
9368
- function formatNum(tenths) {
9369
- if (tenths % 10 === 0)
9370
- return String(tenths / 10);
9371
- const negative = tenths < 0;
9372
- if (negative)
9373
- tenths = -tenths;
9374
- const whole = Math.floor(tenths / 10);
9375
- const frac = tenths % 10;
9376
- return (negative ? '-' : '') + String(whole) + '.' + String(frac);
9377
- }
9378
9396
 
9379
9397
  /*
9380
9398
  * bignumber.js v9.3.1
@@ -14038,6 +14056,7 @@ class Feature {
14038
14056
  properties;
14039
14057
  patterns;
14040
14058
  geometry;
14059
+ #bbox;
14041
14060
  constructor(opt) {
14042
14061
  this.type = opt.type;
14043
14062
  this.id = opt.id;
@@ -14046,6 +14065,8 @@ class Feature {
14046
14065
  this.geometry = opt.geometry;
14047
14066
  }
14048
14067
  getBbox() {
14068
+ if (this.#bbox)
14069
+ return this.#bbox;
14049
14070
  let xMin = Infinity;
14050
14071
  let yMin = Infinity;
14051
14072
  let xMax = -Infinity;
@@ -14062,7 +14083,8 @@ class Feature {
14062
14083
  yMax = point.y;
14063
14084
  });
14064
14085
  });
14065
- return [xMin, yMin, xMax, yMax];
14086
+ this.#bbox = [xMin, yMin, xMax, yMax];
14087
+ return this.#bbox;
14066
14088
  }
14067
14089
  doesOverlap(bbox) {
14068
14090
  const featureBbox = this.getBbox();
@@ -14080,7 +14102,7 @@ class Feature {
14080
14102
 
14081
14103
  function geojsonToFeature(id, polygonFeature) {
14082
14104
  const geometry = polygonFeature.geometry.coordinates.map((ring) => {
14083
- return ring.map((coord) => new Point2D(coord[0], coord[1]));
14105
+ return ring.map((coord) => new Point2D(coord[0] ?? 0, coord[1] ?? 0));
14084
14106
  });
14085
14107
  return new Feature({
14086
14108
  type: 'Polygon',
@@ -14089,7 +14111,7 @@ function geojsonToFeature(id, polygonFeature) {
14089
14111
  properties: polygonFeature.properties ?? {},
14090
14112
  });
14091
14113
  }
14092
- function mergePolygons(featureList) {
14114
+ function mergePolygonsByFeatureId(featureList) {
14093
14115
  const featuresById = new Map();
14094
14116
  let nextId = -1;
14095
14117
  for (const feature of featureList) {
@@ -14149,7 +14171,9 @@ function mergePolygons(featureList) {
14149
14171
 
14150
14172
  function calculateTileGrid(width, height, center, zoom, maxzoom) {
14151
14173
  const zoomLevel = Math.min(Math.floor(zoom), maxzoom ?? Infinity);
14152
- const tileCenterCoordinate = center.getProject2Pixel().scale(2 ** zoomLevel);
14174
+ const tileCenterCoordinate = new Point2D(center[0], center[1])
14175
+ .getProject2Pixel()
14176
+ .scale(2 ** zoomLevel);
14153
14177
  const tileSize = 2 ** (zoom - zoomLevel + 9); // 512 (2^9) is the standard tile size
14154
14178
  const tileCols = width / tileSize;
14155
14179
  const tileRows = height / tileSize;
@@ -14157,11 +14181,15 @@ function calculateTileGrid(width, height, center, zoom, maxzoom) {
14157
14181
  const tileMinY = Math.floor(tileCenterCoordinate.y - tileRows / 2);
14158
14182
  const tileMaxX = Math.floor(tileCenterCoordinate.x + tileCols / 2);
14159
14183
  const tileMaxY = Math.floor(tileCenterCoordinate.y + tileRows / 2);
14184
+ const tilesPerZoom = 2 ** zoomLevel;
14160
14185
  const tiles = [];
14161
14186
  for (let x = tileMinX; x <= tileMaxX; x++) {
14187
+ const wrappedX = ((x % tilesPerZoom) + tilesPerZoom) % tilesPerZoom;
14162
14188
  for (let y = tileMinY; y <= tileMaxY; y++) {
14189
+ if (y < 0 || y >= tilesPerZoom)
14190
+ continue;
14163
14191
  tiles.push({
14164
- x,
14192
+ x: wrappedX,
14165
14193
  y,
14166
14194
  offsetX: width / 2 + (x - tileCenterCoordinate.x) * tileSize,
14167
14195
  offsetY: height / 2 + (y - tileCenterCoordinate.y) * tileSize,
@@ -14180,8 +14208,8 @@ async function getTile(url, z, x, y) {
14180
14208
  const contentType = response.headers.get('content-type') ?? 'application/octet-stream';
14181
14209
  return { buffer, contentType };
14182
14210
  }
14183
- catch {
14184
- console.warn(`Failed to load tile: ${tileUrl}`);
14211
+ catch (error) {
14212
+ console.warn(`Failed to load tile: ${tileUrl}`, error);
14185
14213
  return null;
14186
14214
  }
14187
14215
  }
@@ -15726,6 +15754,7 @@ function writeUtf8(buf, str, pos) {
15726
15754
  }
15727
15755
 
15728
15756
  const TILE_EXTENT = 4096;
15757
+ const VTFeatureType = { Unknown: 0, Point: 1, LineString: 2, Polygon: 3 };
15729
15758
  async function loadVectorSource(source, job, layerFeatures) {
15730
15759
  const tiles = source.tiles;
15731
15760
  if (!tiles)
@@ -15753,17 +15782,17 @@ async function loadVectorSource(source, job, layerFeatures) {
15753
15782
  let type;
15754
15783
  let list;
15755
15784
  switch (featureSrc.type) {
15756
- case 0: //Unknown
15785
+ case VTFeatureType.Unknown:
15757
15786
  throw Error('Unknown feature type in vector tile');
15758
- case 1: //Point
15787
+ case VTFeatureType.Point:
15759
15788
  type = 'Point';
15760
15789
  list = features.points;
15761
15790
  break;
15762
- case 2: //LineString
15791
+ case VTFeatureType.LineString:
15763
15792
  type = 'LineString';
15764
15793
  list = features.linestrings;
15765
15794
  break;
15766
- case 3: //Polygon
15795
+ case VTFeatureType.Polygon:
15767
15796
  type = 'Polygon';
15768
15797
  list = features.polygons;
15769
15798
  break;
@@ -15781,13 +15810,14 @@ async function loadVectorSource(source, job, layerFeatures) {
15781
15810
  }));
15782
15811
  }
15783
15812
 
15784
- function loadGeoJSONSource(sourceName, data, width, height, zoom, center, layerFeatures) {
15813
+ function loadGeoJSONSource(options) {
15814
+ const { sourceName, data, width, height, zoom, center, layerFeatures } = options;
15785
15815
  const existing = layerFeatures.get(sourceName);
15786
15816
  const features = existing ?? { points: [], linestrings: [], polygons: [] };
15787
15817
  if (!existing)
15788
15818
  layerFeatures.set(sourceName, features);
15789
15819
  const worldSize = 512 * 2 ** zoom;
15790
- const centerMercator = center.getProject2Pixel();
15820
+ const centerMercator = new Point2D(center[0], center[1]).getProject2Pixel();
15791
15821
  function projectCoord(coord) {
15792
15822
  const mercator = new Point2D(coord[0], coord[1]).getProject2Pixel();
15793
15823
  return new Point2D((mercator.x - centerMercator.x) * worldSize + width / 2, (mercator.y - centerMercator.y) * worldSize + height / 2);
@@ -15840,25 +15870,26 @@ function loadGeoJSONSource(sourceName, data, width, height, zoom, center, layerF
15840
15870
  }
15841
15871
  }
15842
15872
  function processGeometry(geom, id, properties) {
15843
- const coords = geom.coordinates;
15844
15873
  switch (geom.type) {
15845
15874
  case 'Point':
15846
- addFeature('Point', [[projectCoord(coords)]], id, properties);
15875
+ addFeature('Point', [[projectCoord(geom.coordinates)]], id, properties);
15847
15876
  break;
15848
15877
  case 'MultiPoint':
15849
- addFeature('Point', coords.map((c) => [projectCoord(c)]), id, properties);
15878
+ addFeature('Point', geom.coordinates.map((c) => [projectCoord(c)]), id, properties);
15850
15879
  break;
15851
15880
  case 'LineString':
15852
- addFeature('LineString', [coords.map((c) => projectCoord(c))], id, properties);
15881
+ addFeature('LineString', [geom.coordinates.map((c) => projectCoord(c))], id, properties);
15853
15882
  break;
15854
15883
  case 'MultiLineString':
15855
- addFeature('LineString', coords.map((line) => line.map((c) => projectCoord(c))), id, properties);
15884
+ addFeature('LineString', geom.coordinates.map((line) => line.map((c) => projectCoord(c))), id, properties);
15856
15885
  break;
15857
15886
  case 'Polygon':
15858
- addFeature('Polygon', coords.map((ring) => ring.map((c) => projectCoord(c))), id, properties);
15887
+ addFeature('Polygon', geom.coordinates.map((ring) => ring.map((c) => projectCoord(c))), id, properties);
15859
15888
  break;
15860
15889
  case 'MultiPolygon':
15861
- addFeature('Polygon', coords.flatMap((polygon) => polygon.map((ring) => ring.map((c) => projectCoord(c)))), id, properties);
15890
+ for (const polygon of geom.coordinates) {
15891
+ addFeature('Polygon', polygon.map((ring) => ring.map((c) => projectCoord(c))), id, properties);
15892
+ }
15862
15893
  break;
15863
15894
  case 'GeometryCollection':
15864
15895
  for (const g of geom.geometries) {
@@ -15867,18 +15898,17 @@ function loadGeoJSONSource(sourceName, data, width, height, zoom, center, layerF
15867
15898
  break;
15868
15899
  }
15869
15900
  }
15870
- const geojson = data;
15871
- switch (geojson.type) {
15901
+ switch (data.type) {
15872
15902
  case 'FeatureCollection':
15873
- for (const f of geojson.features) {
15903
+ for (const f of data.features) {
15874
15904
  processGeometry(f.geometry, f.id, (f.properties ?? {}));
15875
15905
  }
15876
15906
  break;
15877
15907
  case 'Feature':
15878
- processGeometry(geojson.geometry, geojson.id, (geojson.properties ?? {}));
15908
+ processGeometry(data.geometry, data.id, (data.properties ?? {}));
15879
15909
  break;
15880
15910
  default:
15881
- processGeometry(geojson, undefined, {});
15911
+ processGeometry(data, undefined, {});
15882
15912
  break;
15883
15913
  }
15884
15914
  }
@@ -15888,7 +15918,7 @@ async function getRasterTiles(job, sourceName) {
15888
15918
  const { zoom, center } = job.view;
15889
15919
  const source = job.style.sources[sourceName];
15890
15920
  if (source?.type !== 'raster' || !source.tiles) {
15891
- throw Error('Invalid raster source: ' + sourceName);
15921
+ throw Error(`Invalid raster source "${sourceName}": expected type "raster" with a "tiles" array`);
15892
15922
  }
15893
15923
  const sourceUrl = source.tiles[0];
15894
15924
  const { zoomLevel, tileSize, tiles } = calculateTileGrid(width, height, center, zoom, source.maxzoom);
@@ -15916,24 +15946,34 @@ async function getLayerFeatures(job) {
15916
15946
  const { zoom, center } = job.view;
15917
15947
  const { sources } = job.style;
15918
15948
  const layerFeatures = new Map();
15949
+ const loadPromises = [];
15919
15950
  for (const [sourceName, sourceSpec] of Object.entries(sources)) {
15920
15951
  const source = sourceSpec;
15921
15952
  switch (source.type) {
15922
15953
  case 'vector':
15923
- await loadVectorSource(source, job, layerFeatures);
15954
+ loadPromises.push(loadVectorSource(source, job, layerFeatures));
15924
15955
  break;
15925
15956
  case 'geojson':
15926
15957
  if (source.data) {
15927
- loadGeoJSONSource(sourceName, source.data, width, height, zoom, center, layerFeatures);
15958
+ loadGeoJSONSource({
15959
+ sourceName,
15960
+ data: source.data,
15961
+ width,
15962
+ height,
15963
+ zoom,
15964
+ center,
15965
+ layerFeatures,
15966
+ });
15928
15967
  }
15929
15968
  break;
15930
15969
  }
15931
15970
  }
15971
+ await Promise.all(loadPromises);
15932
15972
  for (const [name, features] of layerFeatures) {
15933
15973
  layerFeatures.set(name, {
15934
15974
  points: features.points,
15935
15975
  linestrings: features.linestrings,
15936
- polygons: mergePolygons(features.polygons),
15976
+ polygons: mergePolygonsByFeatureId(features.polygons),
15937
15977
  });
15938
15978
  }
15939
15979
  return layerFeatures;
@@ -15981,6 +16021,7 @@ class StyleLayer {
15981
16021
  minzoom;
15982
16022
  maxzoom;
15983
16023
  filter;
16024
+ filterFn;
15984
16025
  paint;
15985
16026
  layout;
15986
16027
  paintExpressions;
@@ -15999,6 +16040,7 @@ class StyleLayer {
15999
16040
  this.source = spec.source;
16000
16041
  this.sourceLayer = spec['source-layer'];
16001
16042
  this.filter = spec.filter;
16043
+ this.filterFn = featureFilter(this.filter);
16002
16044
  }
16003
16045
  this.visibility = (spec.layout?.visibility ?? 'visible');
16004
16046
  // Initialize paint property expressions
@@ -16036,7 +16078,7 @@ class StyleLayer {
16036
16078
  this.layout = new EvaluatedProperties();
16037
16079
  for (const [name, expr] of this.paintExpressions) {
16038
16080
  if (expr.kind === 'constant' || expr.kind === 'camera') {
16039
- this.paint.set(name, expr.evaluate(params, null, {}, undefined, availableImages));
16081
+ this.paint.set(name, expr.evaluate(params, undefined, {}, undefined, availableImages));
16040
16082
  }
16041
16083
  else {
16042
16084
  this.paint.set(name, new PossiblyEvaluatedPropertyValue(expr, params));
@@ -16044,7 +16086,7 @@ class StyleLayer {
16044
16086
  }
16045
16087
  for (const [name, expr] of this.layoutExpressions) {
16046
16088
  if (expr.kind === 'constant' || expr.kind === 'camera') {
16047
- this.layout.set(name, expr.evaluate(params, null, {}, undefined, availableImages));
16089
+ this.layout.set(name, expr.evaluate(params, undefined, {}, undefined, availableImages));
16048
16090
  }
16049
16091
  else {
16050
16092
  this.layout.set(name, new PossiblyEvaluatedPropertyValue(expr, params));
@@ -16055,18 +16097,17 @@ class StyleLayer {
16055
16097
  function createStyleLayer(spec) {
16056
16098
  return new StyleLayer(spec);
16057
16099
  }
16058
-
16059
16100
  function getLayerStyles(layers) {
16060
- return layers.map((layerSpecification) => {
16061
- const styleLayer = createStyleLayer(layerSpecification);
16062
- return styleLayer;
16063
- });
16101
+ return layers.map(createStyleLayer);
16064
16102
  }
16065
16103
 
16066
- async function renderVectorTiles(job) {
16104
+ async function renderMap(job) {
16067
16105
  await render(job);
16068
16106
  return job.renderer.getString();
16069
16107
  }
16108
+ function getFeatures(layerFeatures, layerStyle) {
16109
+ return layerFeatures.get(layerStyle.sourceLayer) ?? layerFeatures.get(layerStyle.source);
16110
+ }
16070
16111
  async function render(job) {
16071
16112
  const { renderer } = job;
16072
16113
  const { zoom } = job.view;
@@ -16097,54 +16138,55 @@ async function render(job) {
16097
16138
  case 'background':
16098
16139
  {
16099
16140
  renderer.drawBackgroundFill({
16100
- color: new Color(getPaint('background-color')),
16141
+ color: getPaint('background-color'),
16101
16142
  opacity: getPaint('background-opacity'),
16102
16143
  });
16103
16144
  }
16104
16145
  continue;
16105
16146
  case 'fill':
16106
16147
  {
16107
- const polygons = (layerFeatures.get(layerStyle.sourceLayer) ?? layerFeatures.get(layerStyle.source))?.polygons;
16148
+ const polygons = getFeatures(layerFeatures, layerStyle)?.polygons;
16108
16149
  if (!polygons || polygons.length === 0)
16109
16150
  continue;
16110
- const filter = featureFilter(layerStyle.filter);
16111
- const polygonFeatures = polygons.filter((feature) => filter.filter({ zoom }, feature));
16151
+ const polygonFeatures = layerStyle.filterFn
16152
+ ? polygons.filter((feature) => layerStyle.filterFn.filter({ zoom }, feature))
16153
+ : polygons;
16112
16154
  if (polygonFeatures.length === 0)
16113
16155
  continue;
16114
16156
  renderer.drawPolygons(polygonFeatures.map((feature) => [
16115
16157
  feature,
16116
16158
  {
16117
- color: new Color(getPaint('fill-color', feature)),
16118
- translate: new Point2D(...getPaint('fill-translate', feature)),
16159
+ color: getPaint('fill-color', feature),
16160
+ opacity: getPaint('fill-opacity', feature),
16161
+ translate: getPaint('fill-translate', feature),
16119
16162
  },
16120
- ]), getPaint('fill-opacity', polygonFeatures[0]));
16163
+ ]));
16121
16164
  }
16122
16165
  continue;
16123
16166
  case 'line':
16124
16167
  {
16125
- const lineStrings = (layerFeatures.get(layerStyle.sourceLayer) ?? layerFeatures.get(layerStyle.source))?.linestrings;
16168
+ const lineStrings = getFeatures(layerFeatures, layerStyle)?.linestrings;
16126
16169
  if (!lineStrings || lineStrings.length === 0)
16127
16170
  continue;
16128
- const filter = featureFilter(layerStyle.filter);
16129
- const lineStringFeatures = lineStrings.filter((feature) => filter.filter({ zoom }, feature));
16171
+ const lineStringFeatures = layerStyle.filterFn
16172
+ ? lineStrings.filter((feature) => layerStyle.filterFn.filter({ zoom }, feature))
16173
+ : lineStrings;
16130
16174
  if (lineStringFeatures.length === 0)
16131
16175
  continue;
16132
16176
  renderer.drawLineStrings(lineStringFeatures.map((feature) => [
16133
16177
  feature,
16134
16178
  {
16135
- color: new Color(getPaint('line-color', feature)),
16136
- translate: new Point2D(...getPaint('line-translate', feature)),
16137
- blur: getPaint('line-blur', feature),
16179
+ color: getPaint('line-color', feature),
16180
+ translate: getPaint('line-translate', feature),
16138
16181
  cap: getLayout('line-cap', feature),
16139
16182
  dasharray: getPaint('line-dasharray', feature),
16140
- gapWidth: getPaint('line-gap-width', feature),
16141
16183
  join: getLayout('line-join', feature),
16142
16184
  miterLimit: getLayout('line-miter-limit', feature),
16143
16185
  offset: getPaint('line-offset', feature),
16144
- roundLimit: getLayout('line-round-limit', feature),
16186
+ opacity: getPaint('line-opacity', feature),
16145
16187
  width: getPaint('line-width', feature),
16146
16188
  },
16147
- ]), getPaint('line-opacity', lineStringFeatures[0]));
16189
+ ]));
16148
16190
  }
16149
16191
  continue;
16150
16192
  case 'raster':
@@ -16163,24 +16205,25 @@ async function render(job) {
16163
16205
  continue;
16164
16206
  case 'circle':
16165
16207
  {
16166
- const points = (layerFeatures.get(layerStyle.sourceLayer) ?? layerFeatures.get(layerStyle.source))?.points;
16208
+ const points = getFeatures(layerFeatures, layerStyle)?.points;
16167
16209
  if (!points || points.length === 0)
16168
16210
  continue;
16169
- const filter = featureFilter(layerStyle.filter);
16170
- const pointFeatures = points.filter((feature) => filter.filter({ zoom }, feature));
16211
+ const pointFeatures = layerStyle.filterFn
16212
+ ? points.filter((feature) => layerStyle.filterFn.filter({ zoom }, feature))
16213
+ : points;
16171
16214
  if (pointFeatures.length === 0)
16172
16215
  continue;
16173
16216
  renderer.drawCircles(pointFeatures.map((feature) => [
16174
16217
  feature,
16175
16218
  {
16176
- color: new Color(getPaint('circle-color', feature)),
16219
+ color: getPaint('circle-color', feature),
16220
+ opacity: getPaint('circle-opacity', feature),
16177
16221
  radius: getPaint('circle-radius', feature),
16178
- blur: getPaint('circle-blur', feature),
16179
- translate: new Point2D(...getPaint('circle-translate', feature)),
16222
+ translate: getPaint('circle-translate', feature),
16180
16223
  strokeWidth: getPaint('circle-stroke-width', feature),
16181
- strokeColor: new Color(getPaint('circle-stroke-color', feature)),
16224
+ strokeColor: getPaint('circle-stroke-color', feature),
16182
16225
  },
16183
- ]), getPaint('circle-opacity', pointFeatures[0]));
16226
+ ]));
16184
16227
  }
16185
16228
  continue;
16186
16229
  case 'color-relief':
@@ -16196,15 +16239,20 @@ async function render(job) {
16196
16239
  }
16197
16240
 
16198
16241
  async function renderToSVG(options) {
16199
- return await renderVectorTiles({
16200
- renderer: new SVGRenderer({
16201
- width: options.width ?? 1024,
16202
- height: options.height ?? 1024,
16203
- scale: options.scale ?? 1,
16204
- }),
16242
+ const width = options.width ?? 1024;
16243
+ const height = options.height ?? 1024;
16244
+ const scale = options.scale ?? 1;
16245
+ if (width <= 0)
16246
+ throw new Error('width must be positive');
16247
+ if (height <= 0)
16248
+ throw new Error('height must be positive');
16249
+ if (scale <= 0)
16250
+ throw new Error('scale must be positive');
16251
+ return await renderMap({
16252
+ renderer: new SVGRenderer({ width, height, scale }),
16205
16253
  style: options.style,
16206
16254
  view: {
16207
- center: new Point2D(options.lon ?? 0, options.lat ?? 0),
16255
+ center: [options.lon ?? 0, options.lat ?? 0],
16208
16256
  zoom: options.zoom ?? 2,
16209
16257
  },
16210
16258
  });