@versatiles/svg-renderer 0.3.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/dist/index.js CHANGED
@@ -9050,6 +9050,20 @@ class Color {
9050
9050
  return str.length < 2 ? '0' + str : str;
9051
9051
  }
9052
9052
  }
9053
+ get rgb() {
9054
+ return `#${d2h(this.values[0])}${d2h(this.values[1])}${d2h(this.values[2])}`;
9055
+ function d2h(num) {
9056
+ if (num < 0)
9057
+ num = 0;
9058
+ if (num > 255)
9059
+ num = 255;
9060
+ const str = Math.round(num).toString(16).toUpperCase();
9061
+ return str.length < 2 ? '0' + str : str;
9062
+ }
9063
+ }
9064
+ get opacity() {
9065
+ return this.values[3] / 255;
9066
+ }
9053
9067
  get alpha() {
9054
9068
  return this.values[3];
9055
9069
  }
@@ -9095,7 +9109,7 @@ class SVGRenderer {
9095
9109
  const key = style.color.hex + translate;
9096
9110
  let group = groups.get(key);
9097
9111
  if (!group) {
9098
- group = { segments: [], attrs: `fill="${style.color.hex}"${translate}` };
9112
+ group = { segments: [], attrs: `${fillAttr(style.color)}${translate}` };
9099
9113
  groups.set(key, group);
9100
9114
  }
9101
9115
  feature.geometry.forEach((ring) => {
@@ -9136,8 +9150,7 @@ class SVGRenderer {
9136
9150
  segments: [],
9137
9151
  attrs: [
9138
9152
  'fill="none"',
9139
- `stroke="${style.color.hex}"`,
9140
- `stroke-width="${roundedWidth}"`,
9153
+ strokeAttr(style.color, roundedWidth),
9141
9154
  `stroke-linecap="${style.cap}"`,
9142
9155
  `stroke-linejoin="${style.join}"`,
9143
9156
  `stroke-miterlimit="${String(style.miterLimit)}"`,
@@ -9156,6 +9169,43 @@ class SVGRenderer {
9156
9169
  }
9157
9170
  this.#svg.push('</g>');
9158
9171
  }
9172
+ drawCircles(features, opacity) {
9173
+ if (features.length === 0)
9174
+ return;
9175
+ if (opacity <= 0)
9176
+ return;
9177
+ this.#svg.push(`<g opacity="${String(opacity)}">`);
9178
+ const groups = new Map();
9179
+ features.forEach(([feature, style]) => {
9180
+ if (style.radius <= 0 || style.color.alpha <= 0)
9181
+ return;
9182
+ const translate = style.translate.isZero()
9183
+ ? ''
9184
+ : ` transform="translate(${formatPoint(style.translate, this.#scale)})"`;
9185
+ const roundedRadius = roundValue(style.radius, this.#scale);
9186
+ const strokeAttrs = style.strokeWidth > 0
9187
+ ? ` ${strokeAttr(style.strokeColor, roundValue(style.strokeWidth, this.#scale))}`
9188
+ : '';
9189
+ const key = [style.color.hex, roundedRadius, strokeAttrs, translate].join('\0');
9190
+ let group = groups.get(key);
9191
+ if (!group) {
9192
+ group = {
9193
+ points: [],
9194
+ attrs: `r="${roundedRadius}" ${fillAttr(style.color)}${strokeAttrs}${translate}`,
9195
+ };
9196
+ groups.set(key, group);
9197
+ }
9198
+ feature.geometry.forEach((ring) => {
9199
+ group.points.push(roundXY(ring[0], this.#scale));
9200
+ });
9201
+ });
9202
+ for (const { points, attrs } of groups.values()) {
9203
+ for (const [x, y] of points) {
9204
+ this.#svg.push(`<circle cx="${formatNum(x)}" cy="${formatNum(y)}" ${attrs} />`);
9205
+ }
9206
+ }
9207
+ this.#svg.push('</g>');
9208
+ }
9159
9209
  drawRasterTiles(tiles, style) {
9160
9210
  if (tiles.length === 0)
9161
9211
  return;
@@ -9188,13 +9238,32 @@ class SVGRenderer {
9188
9238
  this.#svg.push('</g>');
9189
9239
  }
9190
9240
  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');
9241
+ const w = this.width.toFixed(0);
9242
+ const h = this.height.toFixed(0);
9243
+ const parts = [
9244
+ `<svg viewBox="0 0 ${w} ${h}" width="${w}" height="${h}" xmlns="http://www.w3.org/2000/svg">`,
9245
+ `<defs><clipPath id="vb"><rect width="${w}" height="${h}"/></clipPath></defs>`,
9246
+ `<g clip-path="url(#vb)">`,
9247
+ ];
9248
+ if (this.#backgroundColor.alpha > 0) {
9249
+ parts.push(`<rect x="-1" y="-1" width="${(this.width + 2).toFixed(0)}" height="${(this.height + 2).toFixed(0)}" ${fillAttr(this.#backgroundColor)} />`);
9250
+ }
9251
+ parts.push(...this.#svg, '</g>', '</svg>');
9252
+ return parts.join('\n');
9196
9253
  }
9197
9254
  }
9255
+ function fillAttr(color) {
9256
+ let attr = `fill="${color.rgb}"`;
9257
+ if (color.alpha < 255)
9258
+ attr += ` fill-opacity="${color.opacity.toFixed(3)}"`;
9259
+ return attr;
9260
+ }
9261
+ function strokeAttr(color, width) {
9262
+ let attr = `stroke="${color.rgb}" stroke-width="${width}"`;
9263
+ if (color.alpha < 255)
9264
+ attr += ` stroke-opacity="${color.opacity.toFixed(3)}"`;
9265
+ return attr;
9266
+ }
9198
9267
  function roundValue(v, scale) {
9199
9268
  return (v * scale).toFixed(3);
9200
9269
  }
@@ -9305,77 +9374,6 @@ function formatNum(tenths) {
9305
9374
  return (negative ? '-' : '') + String(whole) + '.' + String(frac);
9306
9375
  }
9307
9376
 
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
- }
9332
- }
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
- }
9377
- }
9378
-
9379
9377
  /*
9380
9378
  * bignumber.js v9.3.1
9381
9379
  * A JavaScript library for arbitrary-precision arithmetic.
@@ -14007,6 +14005,77 @@ function union2(features, options = {}) {
14007
14005
  else return multiPolygon(unioned, options.properties);
14008
14006
  }
14009
14007
 
14008
+ class Point2D {
14009
+ x;
14010
+ y;
14011
+ constructor(x, y) {
14012
+ this.x = x;
14013
+ this.y = y;
14014
+ }
14015
+ isZero() {
14016
+ return this.x === 0 && this.y === 0;
14017
+ }
14018
+ scale(factor) {
14019
+ this.x *= factor;
14020
+ this.y *= factor;
14021
+ return this;
14022
+ }
14023
+ translate(offset) {
14024
+ this.x += offset.x;
14025
+ this.y += offset.y;
14026
+ return this;
14027
+ }
14028
+ getProject2Pixel() {
14029
+ const s = Math.sin((this.y * Math.PI) / 180.0);
14030
+ return new Point2D(this.x / 360.0 + 0.5, 0.5 - (0.25 * Math.log((1 + s) / (1 - s))) / Math.PI);
14031
+ }
14032
+ }
14033
+ class Feature {
14034
+ type;
14035
+ id;
14036
+ properties;
14037
+ patterns;
14038
+ geometry;
14039
+ constructor(opt) {
14040
+ this.type = opt.type;
14041
+ this.id = opt.id;
14042
+ this.properties = opt.properties;
14043
+ this.patterns = opt.patterns;
14044
+ this.geometry = opt.geometry;
14045
+ }
14046
+ getBbox() {
14047
+ let xMin = Infinity;
14048
+ let yMin = Infinity;
14049
+ let xMax = -Infinity;
14050
+ let yMax = -Infinity;
14051
+ this.geometry.forEach((ring) => {
14052
+ ring.forEach((point) => {
14053
+ if (xMin > point.x)
14054
+ xMin = point.x;
14055
+ if (yMin > point.y)
14056
+ yMin = point.y;
14057
+ if (xMax < point.x)
14058
+ xMax = point.x;
14059
+ if (yMax < point.y)
14060
+ yMax = point.y;
14061
+ });
14062
+ });
14063
+ return [xMin, yMin, xMax, yMax];
14064
+ }
14065
+ doesOverlap(bbox) {
14066
+ const featureBbox = this.getBbox();
14067
+ if (featureBbox[0] > bbox[2])
14068
+ return false;
14069
+ if (featureBbox[1] > bbox[3])
14070
+ return false;
14071
+ if (featureBbox[2] < bbox[0])
14072
+ return false;
14073
+ if (featureBbox[3] < bbox[1])
14074
+ return false;
14075
+ return true;
14076
+ }
14077
+ }
14078
+
14010
14079
  function geojsonToFeature(id, polygonFeature) {
14011
14080
  const geometry = polygonFeature.geometry.coordinates.map((ring) => {
14012
14081
  return ring.map((coord) => new Point2D(coord[0], coord[1]));
@@ -14040,15 +14109,13 @@ function mergePolygons(featureList) {
14040
14109
  const turfFeatures = [];
14041
14110
  features.forEach((f) => {
14042
14111
  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
- });
14112
+ turfFeatures.push({
14113
+ type: 'Feature',
14114
+ geometry: {
14115
+ type: 'Polygon',
14116
+ coordinates: rings,
14117
+ },
14118
+ properties: f.properties,
14052
14119
  });
14053
14120
  });
14054
14121
  const merged = union2({
@@ -15657,24 +15724,16 @@ function writeUtf8(buf, str, pos) {
15657
15724
  }
15658
15725
 
15659
15726
  const TILE_EXTENT = 4096;
15660
- async function getLayerFeatures(job) {
15727
+ async function loadVectorSource(source, job, layerFeatures) {
15728
+ const tiles = source.tiles;
15729
+ if (!tiles)
15730
+ return;
15661
15731
  const { width, height } = job.renderer;
15662
15732
  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
15733
  const { zoomLevel, tileSize, tiles: tileCoordinates, } = calculateTileGrid(width, height, center, zoom, source.maxzoom);
15674
- const layerFeatures = new Map();
15675
15734
  await Promise.all(tileCoordinates.map(async ({ x, y, offsetX, offsetY }) => {
15676
15735
  const offset = new Point2D(offsetX, offsetY);
15677
- const tile = await getTile(sourceUrl, zoomLevel, x, y);
15736
+ const tile = await getTile(tiles[0], zoomLevel, x, y);
15678
15737
  if (!tile)
15679
15738
  return;
15680
15739
  const vectorTile = new VectorTile(new Pbf(tile.buffer));
@@ -15718,14 +15777,108 @@ async function getLayerFeatures(job) {
15718
15777
  }
15719
15778
  }
15720
15779
  }));
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
- });
15780
+ }
15781
+
15782
+ function loadGeoJSONSource(sourceName, data, width, height, zoom, center, layerFeatures) {
15783
+ const existing = layerFeatures.get(sourceName);
15784
+ const features = existing ?? { points: [], linestrings: [], polygons: [] };
15785
+ if (!existing)
15786
+ layerFeatures.set(sourceName, features);
15787
+ const worldSize = 512 * 2 ** zoom;
15788
+ const centerMercator = center.getProject2Pixel();
15789
+ function projectCoord(coord) {
15790
+ const mercator = new Point2D(coord[0], coord[1]).getProject2Pixel();
15791
+ return new Point2D((mercator.x - centerMercator.x) * worldSize + width / 2, (mercator.y - centerMercator.y) * worldSize + height / 2);
15792
+ }
15793
+ function makeFeature(type, geometry, id, properties) {
15794
+ const feature = new Feature({ type, geometry, id, properties });
15795
+ if (!feature.doesOverlap([0, 0, width, height]))
15796
+ return null;
15797
+ return feature;
15798
+ }
15799
+ function extractPoints(geometry) {
15800
+ return geometry.flatMap((ring) => ring.map((p) => [p]));
15801
+ }
15802
+ function addFeature(type, geometry, id, properties) {
15803
+ switch (type) {
15804
+ case 'Point': {
15805
+ const f = makeFeature('Point', geometry, id, properties);
15806
+ if (f)
15807
+ features.points.push(f);
15808
+ break;
15809
+ }
15810
+ case 'LineString': {
15811
+ const f = makeFeature('LineString', geometry, id, properties);
15812
+ if (f) {
15813
+ features.linestrings.push(f);
15814
+ features.points.push(new Feature({ type: 'Point', geometry: extractPoints(geometry), id, properties }));
15815
+ }
15816
+ break;
15817
+ }
15818
+ case 'Polygon': {
15819
+ geometry.forEach((ring, ringIndex) => {
15820
+ const needsCW = ringIndex === 0;
15821
+ let area = 0;
15822
+ for (let i = 0; i < ring.length; i++) {
15823
+ const j = (i + 1) % ring.length;
15824
+ area += ring[i].x * ring[j].y;
15825
+ area -= ring[j].x * ring[i].y;
15826
+ }
15827
+ if (area < 0 !== needsCW)
15828
+ ring.reverse();
15829
+ });
15830
+ const f = makeFeature('Polygon', geometry, id, properties);
15831
+ if (f) {
15832
+ features.polygons.push(f);
15833
+ features.linestrings.push(new Feature({ type: 'LineString', geometry, id, properties }));
15834
+ features.points.push(new Feature({ type: 'Point', geometry: extractPoints(geometry), id, properties }));
15835
+ }
15836
+ break;
15837
+ }
15838
+ }
15839
+ }
15840
+ function processGeometry(geom, id, properties) {
15841
+ const coords = geom.coordinates;
15842
+ switch (geom.type) {
15843
+ case 'Point':
15844
+ addFeature('Point', [[projectCoord(coords)]], id, properties);
15845
+ break;
15846
+ case 'MultiPoint':
15847
+ addFeature('Point', coords.map((c) => [projectCoord(c)]), id, properties);
15848
+ break;
15849
+ case 'LineString':
15850
+ addFeature('LineString', [coords.map((c) => projectCoord(c))], id, properties);
15851
+ break;
15852
+ case 'MultiLineString':
15853
+ addFeature('LineString', coords.map((line) => line.map((c) => projectCoord(c))), id, properties);
15854
+ break;
15855
+ case 'Polygon':
15856
+ addFeature('Polygon', coords.map((ring) => ring.map((c) => projectCoord(c))), id, properties);
15857
+ break;
15858
+ case 'MultiPolygon':
15859
+ addFeature('Polygon', coords.flatMap((polygon) => polygon.map((ring) => ring.map((c) => projectCoord(c)))), id, properties);
15860
+ break;
15861
+ case 'GeometryCollection':
15862
+ for (const g of geom.geometries) {
15863
+ processGeometry(g, id, properties);
15864
+ }
15865
+ break;
15866
+ }
15867
+ }
15868
+ const geojson = data;
15869
+ switch (geojson.type) {
15870
+ case 'FeatureCollection':
15871
+ for (const f of geojson.features) {
15872
+ processGeometry(f.geometry, f.id, (f.properties ?? {}));
15873
+ }
15874
+ break;
15875
+ case 'Feature':
15876
+ processGeometry(geojson.geometry, geojson.id, (geojson.properties ?? {}));
15877
+ break;
15878
+ default:
15879
+ processGeometry(geojson, undefined, {});
15880
+ break;
15727
15881
  }
15728
- return layerFeatures;
15729
15882
  }
15730
15883
 
15731
15884
  async function getRasterTiles(job, sourceName) {
@@ -15756,6 +15909,34 @@ async function getRasterTiles(job, sourceName) {
15756
15909
  return rasterTiles.filter((tile) => tile !== null);
15757
15910
  }
15758
15911
 
15912
+ async function getLayerFeatures(job) {
15913
+ const { width, height } = job.renderer;
15914
+ const { zoom, center } = job.view;
15915
+ const { sources } = job.style;
15916
+ const layerFeatures = new Map();
15917
+ for (const [sourceName, sourceSpec] of Object.entries(sources)) {
15918
+ const source = sourceSpec;
15919
+ switch (source.type) {
15920
+ case 'vector':
15921
+ await loadVectorSource(source, job, layerFeatures);
15922
+ break;
15923
+ case 'geojson':
15924
+ if (source.data) {
15925
+ loadGeoJSONSource(sourceName, source.data, width, height, zoom, center, layerFeatures);
15926
+ }
15927
+ break;
15928
+ }
15929
+ }
15930
+ for (const [name, features] of layerFeatures) {
15931
+ layerFeatures.set(name, {
15932
+ points: features.points,
15933
+ linestrings: features.linestrings,
15934
+ polygons: mergePolygons(features.polygons),
15935
+ });
15936
+ }
15937
+ return layerFeatures;
15938
+ }
15939
+
15759
15940
  /**
15760
15941
  * Wraps a source/composite expression for per-feature evaluation.
15761
15942
  */
@@ -15921,7 +16102,7 @@ async function render(job) {
15921
16102
  continue;
15922
16103
  case 'fill':
15923
16104
  {
15924
- const polygons = layerFeatures.get(layerStyle.sourceLayer)?.polygons;
16105
+ const polygons = (layerFeatures.get(layerStyle.sourceLayer) ?? layerFeatures.get(layerStyle.source))?.polygons;
15925
16106
  if (!polygons || polygons.length === 0)
15926
16107
  continue;
15927
16108
  const filter = featureFilter(layerStyle.filter);
@@ -15939,7 +16120,7 @@ async function render(job) {
15939
16120
  continue;
15940
16121
  case 'line':
15941
16122
  {
15942
- const lineStrings = layerFeatures.get(layerStyle.sourceLayer)?.linestrings;
16123
+ const lineStrings = (layerFeatures.get(layerStyle.sourceLayer) ?? layerFeatures.get(layerStyle.source))?.linestrings;
15943
16124
  if (!lineStrings || lineStrings.length === 0)
15944
16125
  continue;
15945
16126
  const filter = featureFilter(layerStyle.filter);
@@ -15979,6 +16160,27 @@ async function render(job) {
15979
16160
  }
15980
16161
  continue;
15981
16162
  case 'circle':
16163
+ {
16164
+ const points = (layerFeatures.get(layerStyle.sourceLayer) ?? layerFeatures.get(layerStyle.source))?.points;
16165
+ if (!points || points.length === 0)
16166
+ continue;
16167
+ const filter = featureFilter(layerStyle.filter);
16168
+ const pointFeatures = points.filter((feature) => filter.filter({ zoom }, feature));
16169
+ if (pointFeatures.length === 0)
16170
+ continue;
16171
+ renderer.drawCircles(pointFeatures.map((feature) => [
16172
+ feature,
16173
+ {
16174
+ color: new Color(getPaint('circle-color', feature)),
16175
+ radius: getPaint('circle-radius', feature),
16176
+ blur: getPaint('circle-blur', feature),
16177
+ translate: new Point2D(...getPaint('circle-translate', feature)),
16178
+ strokeWidth: getPaint('circle-stroke-width', feature),
16179
+ strokeColor: new Color(getPaint('circle-stroke-color', feature)),
16180
+ },
16181
+ ]), getPaint('circle-opacity', pointFeatures[0]));
16182
+ }
16183
+ continue;
15982
16184
  case 'color-relief':
15983
16185
  case 'fill-extrusion':
15984
16186
  case 'heatmap':