@versatiles/svg-renderer 0.2.0 → 0.4.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/README.md CHANGED
@@ -64,18 +64,13 @@ document.body.innerHTML = svg;
64
64
 
65
65
  The package includes an `SVGExportControl` that adds an export button to any MapLibre GL JS map.
66
66
 
67
- ```bash
68
- npm install @versatiles/svg-renderer maplibre-gl
69
- ```
70
-
71
67
  ```html
72
68
  <!DOCTYPE html>
73
69
  <html>
74
70
  <head>
75
71
  <link rel="stylesheet" href="https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.css" />
76
72
  <script src="https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.js"></script>
77
- <script src="…/svg-renderer/dist/maplibre.cjs"></script>
78
- <style></style>
73
+ <script src="https://unpkg.com/@versatiles/svg-renderer/dist/maplibre.umd.js"></script>
79
74
  </head>
80
75
  <body>
81
76
  <div id="map"></div>
@@ -86,7 +81,7 @@ npm install @versatiles/svg-renderer maplibre-gl
86
81
  center: [13.4, 52.5],
87
82
  zoom: 10,
88
83
  });
89
- map.addControl(new SVGExportControl(), 'top-right');
84
+ map.addControl(new VersaTilesSVG.SVGExportControl(), 'top-right');
90
85
  </script>
91
86
  </body>
92
87
  </html>
@@ -141,47 +136,55 @@ subgraph 0["src"]
141
136
  subgraph 3["lib"]
142
137
  4["geometry.ts"]
143
138
  7["color.ts"]
144
- B["style_layer.ts"]
139
+ G["style_layer.ts"]
145
140
  end
146
141
  subgraph 5["processor"]
147
142
  6["render.ts"]
148
- 8["raster.ts"]
149
- 9["tiles.ts"]
150
- A["styles.ts"]
151
- C["vector.ts"]
152
- D["helper.ts"]
143
+ subgraph 8["sources"]
144
+ 9["index.ts"]
145
+ B["geojson.ts"]
146
+ C["raster.ts"]
147
+ D["tiles.ts"]
148
+ E["vector.ts"]
149
+ N["types.ts"]
150
+ end
151
+ A["helper.ts"]
152
+ F["styles.ts"]
153
153
  end
154
- subgraph E["renderer"]
155
- F["renderer_svg.ts"]
154
+ subgraph H["renderer"]
155
+ I["renderer_svg.ts"]
156
156
  end
157
- subgraph G["maplibre"]
158
- H["control.ts"]
159
- I["styles.ts"]
160
- J["index.ts"]
157
+ subgraph J["maplibre"]
158
+ K["control.ts"]
159
+ L["styles.ts"]
160
+ M["index.ts"]
161
161
  end
162
- K["types.ts"]
162
+ O["types.ts"]
163
163
  end
164
164
  1-->2
165
165
  2-->4
166
166
  2-->6
167
- 2-->F
167
+ 2-->I
168
168
  6-->7
169
169
  6-->4
170
- 6-->8
171
- 6-->A
172
- 6-->C
173
- 8-->9
174
- A-->B
175
- C-->4
170
+ 6-->9
171
+ 6-->F
172
+ 9-->A
173
+ 9-->B
174
+ 9-->C
175
+ 9-->E
176
+ A-->4
177
+ B-->4
176
178
  C-->D
177
- C-->9
178
- D-->4
179
- F-->7
180
- H-->2
181
- H-->I
182
- J-->2
183
- J-->H
184
-
185
- class 0,3,5,E,G subgraphs;
179
+ E-->4
180
+ E-->D
181
+ F-->G
182
+ I-->7
183
+ K-->2
184
+ K-->L
185
+ M-->2
186
+ M-->K
187
+
188
+ class 0,3,5,8,H,J subgraphs;
186
189
  classDef subgraphs fill-opacity:0.1, fill:#888, color:#888, stroke:#888;
187
190
  ```
package/dist/index.cjs CHANGED
@@ -9052,6 +9052,20 @@ class Color {
9052
9052
  return str.length < 2 ? '0' + str : str;
9053
9053
  }
9054
9054
  }
9055
+ get rgb() {
9056
+ 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
+ }
9066
+ get opacity() {
9067
+ return this.values[3] / 255;
9068
+ }
9055
9069
  get alpha() {
9056
9070
  return this.values[3];
9057
9071
  }
@@ -9097,7 +9111,7 @@ class SVGRenderer {
9097
9111
  const key = style.color.hex + translate;
9098
9112
  let group = groups.get(key);
9099
9113
  if (!group) {
9100
- group = { segments: [], attrs: `fill="${style.color.hex}"${translate}` };
9114
+ group = { segments: [], attrs: `${fillAttr(style.color)}${translate}` };
9101
9115
  groups.set(key, group);
9102
9116
  }
9103
9117
  feature.geometry.forEach((ring) => {
@@ -9138,8 +9152,7 @@ class SVGRenderer {
9138
9152
  segments: [],
9139
9153
  attrs: [
9140
9154
  'fill="none"',
9141
- `stroke="${style.color.hex}"`,
9142
- `stroke-width="${roundedWidth}"`,
9155
+ strokeAttr(style.color, roundedWidth),
9143
9156
  `stroke-linecap="${style.cap}"`,
9144
9157
  `stroke-linejoin="${style.join}"`,
9145
9158
  `stroke-miterlimit="${String(style.miterLimit)}"`,
@@ -9158,6 +9171,43 @@ class SVGRenderer {
9158
9171
  }
9159
9172
  this.#svg.push('</g>');
9160
9173
  }
9174
+ drawCircles(features, opacity) {
9175
+ if (features.length === 0)
9176
+ return;
9177
+ if (opacity <= 0)
9178
+ return;
9179
+ this.#svg.push(`<g opacity="${String(opacity)}">`);
9180
+ const groups = new Map();
9181
+ features.forEach(([feature, style]) => {
9182
+ if (style.radius <= 0 || style.color.alpha <= 0)
9183
+ return;
9184
+ const translate = style.translate.isZero()
9185
+ ? ''
9186
+ : ` transform="translate(${formatPoint(style.translate, this.#scale)})"`;
9187
+ const roundedRadius = roundValue(style.radius, this.#scale);
9188
+ const strokeAttrs = style.strokeWidth > 0
9189
+ ? ` ${strokeAttr(style.strokeColor, roundValue(style.strokeWidth, this.#scale))}`
9190
+ : '';
9191
+ const key = [style.color.hex, roundedRadius, strokeAttrs, translate].join('\0');
9192
+ let group = groups.get(key);
9193
+ if (!group) {
9194
+ group = {
9195
+ points: [],
9196
+ attrs: `r="${roundedRadius}" ${fillAttr(style.color)}${strokeAttrs}${translate}`,
9197
+ };
9198
+ groups.set(key, group);
9199
+ }
9200
+ feature.geometry.forEach((ring) => {
9201
+ group.points.push(roundXY(ring[0], this.#scale));
9202
+ });
9203
+ });
9204
+ for (const { points, attrs } of groups.values()) {
9205
+ for (const [x, y] of points) {
9206
+ this.#svg.push(`<circle cx="${formatNum(x)}" cy="${formatNum(y)}" ${attrs} />`);
9207
+ }
9208
+ }
9209
+ this.#svg.push('</g>');
9210
+ }
9161
9211
  drawRasterTiles(tiles, style) {
9162
9212
  if (tiles.length === 0)
9163
9213
  return;
@@ -9190,13 +9240,32 @@ class SVGRenderer {
9190
9240
  this.#svg.push('</g>');
9191
9241
  }
9192
9242
  getString() {
9193
- return [
9194
- `<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}">`,
9195
- ...this.#svg,
9196
- '</svg>',
9197
- ].join('\n');
9243
+ const w = this.width.toFixed(0);
9244
+ const h = this.height.toFixed(0);
9245
+ const parts = [
9246
+ `<svg viewBox="0 0 ${w} ${h}" width="${w}" height="${h}" xmlns="http://www.w3.org/2000/svg">`,
9247
+ `<defs><clipPath id="vb"><rect width="${w}" height="${h}"/></clipPath></defs>`,
9248
+ `<g clip-path="url(#vb)">`,
9249
+ ];
9250
+ if (this.#backgroundColor.alpha > 0) {
9251
+ parts.push(`<rect x="-1" y="-1" width="${(this.width + 2).toFixed(0)}" height="${(this.height + 2).toFixed(0)}" ${fillAttr(this.#backgroundColor)} />`);
9252
+ }
9253
+ parts.push(...this.#svg, '</g>', '</svg>');
9254
+ return parts.join('\n');
9198
9255
  }
9199
9256
  }
9257
+ function fillAttr(color) {
9258
+ let attr = `fill="${color.rgb}"`;
9259
+ if (color.alpha < 255)
9260
+ attr += ` fill-opacity="${color.opacity.toFixed(3)}"`;
9261
+ return attr;
9262
+ }
9263
+ function strokeAttr(color, width) {
9264
+ let attr = `stroke="${color.rgb}" stroke-width="${width}"`;
9265
+ if (color.alpha < 255)
9266
+ attr += ` stroke-opacity="${color.opacity.toFixed(3)}"`;
9267
+ return attr;
9268
+ }
9200
9269
  function roundValue(v, scale) {
9201
9270
  return (v * scale).toFixed(3);
9202
9271
  }
@@ -9307,77 +9376,6 @@ function formatNum(tenths) {
9307
9376
  return (negative ? '-' : '') + String(whole) + '.' + String(frac);
9308
9377
  }
9309
9378
 
9310
- class Point2D {
9311
- x;
9312
- y;
9313
- constructor(x, y) {
9314
- this.x = x;
9315
- this.y = y;
9316
- }
9317
- isZero() {
9318
- return this.x === 0 && this.y === 0;
9319
- }
9320
- scale(factor) {
9321
- this.x *= factor;
9322
- this.y *= factor;
9323
- return this;
9324
- }
9325
- translate(offset) {
9326
- this.x += offset.x;
9327
- this.y += offset.y;
9328
- return this;
9329
- }
9330
- getProject2Pixel() {
9331
- const s = Math.sin((this.y * Math.PI) / 180.0);
9332
- return new Point2D(this.x / 360.0 + 0.5, 0.5 - (0.25 * Math.log((1 + s) / (1 - s))) / Math.PI);
9333
- }
9334
- }
9335
- class Feature {
9336
- type;
9337
- id;
9338
- properties;
9339
- patterns;
9340
- geometry;
9341
- constructor(opt) {
9342
- this.type = opt.type;
9343
- this.id = opt.id;
9344
- this.properties = opt.properties;
9345
- this.patterns = opt.patterns;
9346
- this.geometry = opt.geometry;
9347
- }
9348
- getBbox() {
9349
- let xMin = Infinity;
9350
- let yMin = Infinity;
9351
- let xMax = -Infinity;
9352
- let yMax = -Infinity;
9353
- this.geometry.forEach((ring) => {
9354
- ring.forEach((point) => {
9355
- if (xMin > point.x)
9356
- xMin = point.x;
9357
- if (yMin > point.y)
9358
- yMin = point.y;
9359
- if (xMax < point.x)
9360
- xMax = point.x;
9361
- if (yMax < point.y)
9362
- yMax = point.y;
9363
- });
9364
- });
9365
- return [xMin, yMin, xMax, yMax];
9366
- }
9367
- doesOverlap(bbox) {
9368
- const featureBbox = this.getBbox();
9369
- if (featureBbox[0] > bbox[2])
9370
- return false;
9371
- if (featureBbox[1] > bbox[3])
9372
- return false;
9373
- if (featureBbox[2] < bbox[0])
9374
- return false;
9375
- if (featureBbox[3] < bbox[1])
9376
- return false;
9377
- return true;
9378
- }
9379
- }
9380
-
9381
9379
  /*
9382
9380
  * bignumber.js v9.3.1
9383
9381
  * A JavaScript library for arbitrary-precision arithmetic.
@@ -14009,6 +14007,77 @@ function union2(features, options = {}) {
14009
14007
  else return multiPolygon(unioned, options.properties);
14010
14008
  }
14011
14009
 
14010
+ class Point2D {
14011
+ x;
14012
+ y;
14013
+ constructor(x, y) {
14014
+ this.x = x;
14015
+ this.y = y;
14016
+ }
14017
+ isZero() {
14018
+ return this.x === 0 && this.y === 0;
14019
+ }
14020
+ scale(factor) {
14021
+ this.x *= factor;
14022
+ this.y *= factor;
14023
+ return this;
14024
+ }
14025
+ translate(offset) {
14026
+ this.x += offset.x;
14027
+ this.y += offset.y;
14028
+ return this;
14029
+ }
14030
+ getProject2Pixel() {
14031
+ const s = Math.sin((this.y * Math.PI) / 180.0);
14032
+ return new Point2D(this.x / 360.0 + 0.5, 0.5 - (0.25 * Math.log((1 + s) / (1 - s))) / Math.PI);
14033
+ }
14034
+ }
14035
+ class Feature {
14036
+ type;
14037
+ id;
14038
+ properties;
14039
+ patterns;
14040
+ geometry;
14041
+ constructor(opt) {
14042
+ this.type = opt.type;
14043
+ this.id = opt.id;
14044
+ this.properties = opt.properties;
14045
+ this.patterns = opt.patterns;
14046
+ this.geometry = opt.geometry;
14047
+ }
14048
+ getBbox() {
14049
+ let xMin = Infinity;
14050
+ let yMin = Infinity;
14051
+ let xMax = -Infinity;
14052
+ let yMax = -Infinity;
14053
+ this.geometry.forEach((ring) => {
14054
+ ring.forEach((point) => {
14055
+ if (xMin > point.x)
14056
+ xMin = point.x;
14057
+ if (yMin > point.y)
14058
+ yMin = point.y;
14059
+ if (xMax < point.x)
14060
+ xMax = point.x;
14061
+ if (yMax < point.y)
14062
+ yMax = point.y;
14063
+ });
14064
+ });
14065
+ return [xMin, yMin, xMax, yMax];
14066
+ }
14067
+ doesOverlap(bbox) {
14068
+ const featureBbox = this.getBbox();
14069
+ if (featureBbox[0] > bbox[2])
14070
+ return false;
14071
+ if (featureBbox[1] > bbox[3])
14072
+ return false;
14073
+ if (featureBbox[2] < bbox[0])
14074
+ return false;
14075
+ if (featureBbox[3] < bbox[1])
14076
+ return false;
14077
+ return true;
14078
+ }
14079
+ }
14080
+
14012
14081
  function geojsonToFeature(id, polygonFeature) {
14013
14082
  const geometry = polygonFeature.geometry.coordinates.map((ring) => {
14014
14083
  return ring.map((coord) => new Point2D(coord[0], coord[1]));
@@ -14042,15 +14111,13 @@ function mergePolygons(featureList) {
14042
14111
  const turfFeatures = [];
14043
14112
  features.forEach((f) => {
14044
14113
  const rings = f.geometry.map((ring) => ring.map((p) => [p.x, p.y]));
14045
- rings.forEach((ring) => {
14046
- turfFeatures.push({
14047
- type: 'Feature',
14048
- geometry: {
14049
- type: 'Polygon',
14050
- coordinates: [ring],
14051
- },
14052
- properties: f.properties,
14053
- });
14114
+ turfFeatures.push({
14115
+ type: 'Feature',
14116
+ geometry: {
14117
+ type: 'Polygon',
14118
+ coordinates: rings,
14119
+ },
14120
+ properties: f.properties,
14054
14121
  });
14055
14122
  });
14056
14123
  const merged = union2({
@@ -15659,24 +15726,16 @@ function writeUtf8(buf, str, pos) {
15659
15726
  }
15660
15727
 
15661
15728
  const TILE_EXTENT = 4096;
15662
- async function getLayerFeatures(job) {
15729
+ async function loadVectorSource(source, job, layerFeatures) {
15730
+ const tiles = source.tiles;
15731
+ if (!tiles)
15732
+ return;
15663
15733
  const { width, height } = job.renderer;
15664
15734
  const { zoom, center } = job.view;
15665
- const { sources } = job.style;
15666
- const source = sources['versatiles-shortbread'];
15667
- if (!source)
15668
- return new Map();
15669
- if (source.type !== 'vector' || !source.tiles) {
15670
- console.error('Invalid source configuration. Expected a vector source with tile URLs.');
15671
- console.error('Source config:', source);
15672
- throw Error('Invalid source');
15673
- }
15674
- const sourceUrl = source.tiles[0];
15675
15735
  const { zoomLevel, tileSize, tiles: tileCoordinates, } = calculateTileGrid(width, height, center, zoom, source.maxzoom);
15676
- const layerFeatures = new Map();
15677
15736
  await Promise.all(tileCoordinates.map(async ({ x, y, offsetX, offsetY }) => {
15678
15737
  const offset = new Point2D(offsetX, offsetY);
15679
- const tile = await getTile(sourceUrl, zoomLevel, x, y);
15738
+ const tile = await getTile(tiles[0], zoomLevel, x, y);
15680
15739
  if (!tile)
15681
15740
  return;
15682
15741
  const vectorTile = new VectorTile(new Pbf(tile.buffer));
@@ -15720,14 +15779,108 @@ async function getLayerFeatures(job) {
15720
15779
  }
15721
15780
  }
15722
15781
  }));
15723
- for (const [name, features] of layerFeatures) {
15724
- layerFeatures.set(name, {
15725
- points: features.points,
15726
- linestrings: features.linestrings,
15727
- polygons: mergePolygons(features.polygons),
15728
- });
15782
+ }
15783
+
15784
+ function loadGeoJSONSource(sourceName, data, width, height, zoom, center, layerFeatures) {
15785
+ const existing = layerFeatures.get(sourceName);
15786
+ const features = existing ?? { points: [], linestrings: [], polygons: [] };
15787
+ if (!existing)
15788
+ layerFeatures.set(sourceName, features);
15789
+ const worldSize = 512 * 2 ** zoom;
15790
+ const centerMercator = center.getProject2Pixel();
15791
+ function projectCoord(coord) {
15792
+ const mercator = new Point2D(coord[0], coord[1]).getProject2Pixel();
15793
+ return new Point2D((mercator.x - centerMercator.x) * worldSize + width / 2, (mercator.y - centerMercator.y) * worldSize + height / 2);
15794
+ }
15795
+ function makeFeature(type, geometry, id, properties) {
15796
+ const feature = new Feature({ type, geometry, id, properties });
15797
+ if (!feature.doesOverlap([0, 0, width, height]))
15798
+ return null;
15799
+ return feature;
15800
+ }
15801
+ function extractPoints(geometry) {
15802
+ return geometry.flatMap((ring) => ring.map((p) => [p]));
15803
+ }
15804
+ function addFeature(type, geometry, id, properties) {
15805
+ switch (type) {
15806
+ case 'Point': {
15807
+ const f = makeFeature('Point', geometry, id, properties);
15808
+ if (f)
15809
+ features.points.push(f);
15810
+ break;
15811
+ }
15812
+ case 'LineString': {
15813
+ const f = makeFeature('LineString', geometry, id, properties);
15814
+ if (f) {
15815
+ features.linestrings.push(f);
15816
+ features.points.push(new Feature({ type: 'Point', geometry: extractPoints(geometry), id, properties }));
15817
+ }
15818
+ break;
15819
+ }
15820
+ case 'Polygon': {
15821
+ geometry.forEach((ring, ringIndex) => {
15822
+ const needsCW = ringIndex === 0;
15823
+ let area = 0;
15824
+ for (let i = 0; i < ring.length; i++) {
15825
+ const j = (i + 1) % ring.length;
15826
+ area += ring[i].x * ring[j].y;
15827
+ area -= ring[j].x * ring[i].y;
15828
+ }
15829
+ if (area < 0 !== needsCW)
15830
+ ring.reverse();
15831
+ });
15832
+ const f = makeFeature('Polygon', geometry, id, properties);
15833
+ if (f) {
15834
+ features.polygons.push(f);
15835
+ features.linestrings.push(new Feature({ type: 'LineString', geometry, id, properties }));
15836
+ features.points.push(new Feature({ type: 'Point', geometry: extractPoints(geometry), id, properties }));
15837
+ }
15838
+ break;
15839
+ }
15840
+ }
15841
+ }
15842
+ function processGeometry(geom, id, properties) {
15843
+ const coords = geom.coordinates;
15844
+ switch (geom.type) {
15845
+ case 'Point':
15846
+ addFeature('Point', [[projectCoord(coords)]], id, properties);
15847
+ break;
15848
+ case 'MultiPoint':
15849
+ addFeature('Point', coords.map((c) => [projectCoord(c)]), id, properties);
15850
+ break;
15851
+ case 'LineString':
15852
+ addFeature('LineString', [coords.map((c) => projectCoord(c))], id, properties);
15853
+ break;
15854
+ case 'MultiLineString':
15855
+ addFeature('LineString', coords.map((line) => line.map((c) => projectCoord(c))), id, properties);
15856
+ break;
15857
+ case 'Polygon':
15858
+ addFeature('Polygon', coords.map((ring) => ring.map((c) => projectCoord(c))), id, properties);
15859
+ break;
15860
+ case 'MultiPolygon':
15861
+ addFeature('Polygon', coords.flatMap((polygon) => polygon.map((ring) => ring.map((c) => projectCoord(c)))), id, properties);
15862
+ break;
15863
+ case 'GeometryCollection':
15864
+ for (const g of geom.geometries) {
15865
+ processGeometry(g, id, properties);
15866
+ }
15867
+ break;
15868
+ }
15869
+ }
15870
+ const geojson = data;
15871
+ switch (geojson.type) {
15872
+ case 'FeatureCollection':
15873
+ for (const f of geojson.features) {
15874
+ processGeometry(f.geometry, f.id, (f.properties ?? {}));
15875
+ }
15876
+ break;
15877
+ case 'Feature':
15878
+ processGeometry(geojson.geometry, geojson.id, (geojson.properties ?? {}));
15879
+ break;
15880
+ default:
15881
+ processGeometry(geojson, undefined, {});
15882
+ break;
15729
15883
  }
15730
- return layerFeatures;
15731
15884
  }
15732
15885
 
15733
15886
  async function getRasterTiles(job, sourceName) {
@@ -15758,6 +15911,34 @@ async function getRasterTiles(job, sourceName) {
15758
15911
  return rasterTiles.filter((tile) => tile !== null);
15759
15912
  }
15760
15913
 
15914
+ async function getLayerFeatures(job) {
15915
+ const { width, height } = job.renderer;
15916
+ const { zoom, center } = job.view;
15917
+ const { sources } = job.style;
15918
+ const layerFeatures = new Map();
15919
+ for (const [sourceName, sourceSpec] of Object.entries(sources)) {
15920
+ const source = sourceSpec;
15921
+ switch (source.type) {
15922
+ case 'vector':
15923
+ await loadVectorSource(source, job, layerFeatures);
15924
+ break;
15925
+ case 'geojson':
15926
+ if (source.data) {
15927
+ loadGeoJSONSource(sourceName, source.data, width, height, zoom, center, layerFeatures);
15928
+ }
15929
+ break;
15930
+ }
15931
+ }
15932
+ for (const [name, features] of layerFeatures) {
15933
+ layerFeatures.set(name, {
15934
+ points: features.points,
15935
+ linestrings: features.linestrings,
15936
+ polygons: mergePolygons(features.polygons),
15937
+ });
15938
+ }
15939
+ return layerFeatures;
15940
+ }
15941
+
15761
15942
  /**
15762
15943
  * Wraps a source/composite expression for per-feature evaluation.
15763
15944
  */
@@ -15923,7 +16104,7 @@ async function render(job) {
15923
16104
  continue;
15924
16105
  case 'fill':
15925
16106
  {
15926
- const polygons = layerFeatures.get(layerStyle.sourceLayer)?.polygons;
16107
+ const polygons = (layerFeatures.get(layerStyle.sourceLayer) ?? layerFeatures.get(layerStyle.source))?.polygons;
15927
16108
  if (!polygons || polygons.length === 0)
15928
16109
  continue;
15929
16110
  const filter = featureFilter(layerStyle.filter);
@@ -15941,7 +16122,7 @@ async function render(job) {
15941
16122
  continue;
15942
16123
  case 'line':
15943
16124
  {
15944
- const lineStrings = layerFeatures.get(layerStyle.sourceLayer)?.linestrings;
16125
+ const lineStrings = (layerFeatures.get(layerStyle.sourceLayer) ?? layerFeatures.get(layerStyle.source))?.linestrings;
15945
16126
  if (!lineStrings || lineStrings.length === 0)
15946
16127
  continue;
15947
16128
  const filter = featureFilter(layerStyle.filter);
@@ -15981,6 +16162,27 @@ async function render(job) {
15981
16162
  }
15982
16163
  continue;
15983
16164
  case 'circle':
16165
+ {
16166
+ const points = (layerFeatures.get(layerStyle.sourceLayer) ?? layerFeatures.get(layerStyle.source))?.points;
16167
+ if (!points || points.length === 0)
16168
+ continue;
16169
+ const filter = featureFilter(layerStyle.filter);
16170
+ const pointFeatures = points.filter((feature) => filter.filter({ zoom }, feature));
16171
+ if (pointFeatures.length === 0)
16172
+ continue;
16173
+ renderer.drawCircles(pointFeatures.map((feature) => [
16174
+ feature,
16175
+ {
16176
+ color: new Color(getPaint('circle-color', feature)),
16177
+ radius: getPaint('circle-radius', feature),
16178
+ blur: getPaint('circle-blur', feature),
16179
+ translate: new Point2D(...getPaint('circle-translate', feature)),
16180
+ strokeWidth: getPaint('circle-stroke-width', feature),
16181
+ strokeColor: new Color(getPaint('circle-stroke-color', feature)),
16182
+ },
16183
+ ]), getPaint('circle-opacity', pointFeatures[0]));
16184
+ }
16185
+ continue;
15984
16186
  case 'color-relief':
15985
16187
  case 'fill-extrusion':
15986
16188
  case 'heatmap':