@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/maplibre.cjs CHANGED
@@ -9209,6 +9209,20 @@ class Color {
9209
9209
  return str.length < 2 ? '0' + str : str;
9210
9210
  }
9211
9211
  }
9212
+ get rgb() {
9213
+ return `#${d2h(this.values[0])}${d2h(this.values[1])}${d2h(this.values[2])}`;
9214
+ function d2h(num) {
9215
+ if (num < 0)
9216
+ num = 0;
9217
+ if (num > 255)
9218
+ num = 255;
9219
+ const str = Math.round(num).toString(16).toUpperCase();
9220
+ return str.length < 2 ? '0' + str : str;
9221
+ }
9222
+ }
9223
+ get opacity() {
9224
+ return this.values[3] / 255;
9225
+ }
9212
9226
  get alpha() {
9213
9227
  return this.values[3];
9214
9228
  }
@@ -9254,7 +9268,7 @@ class SVGRenderer {
9254
9268
  const key = style.color.hex + translate;
9255
9269
  let group = groups.get(key);
9256
9270
  if (!group) {
9257
- group = { segments: [], attrs: `fill="${style.color.hex}"${translate}` };
9271
+ group = { segments: [], attrs: `${fillAttr(style.color)}${translate}` };
9258
9272
  groups.set(key, group);
9259
9273
  }
9260
9274
  feature.geometry.forEach((ring) => {
@@ -9295,8 +9309,7 @@ class SVGRenderer {
9295
9309
  segments: [],
9296
9310
  attrs: [
9297
9311
  'fill="none"',
9298
- `stroke="${style.color.hex}"`,
9299
- `stroke-width="${roundedWidth}"`,
9312
+ strokeAttr(style.color, roundedWidth),
9300
9313
  `stroke-linecap="${style.cap}"`,
9301
9314
  `stroke-linejoin="${style.join}"`,
9302
9315
  `stroke-miterlimit="${String(style.miterLimit)}"`,
@@ -9315,6 +9328,43 @@ class SVGRenderer {
9315
9328
  }
9316
9329
  this.#svg.push('</g>');
9317
9330
  }
9331
+ drawCircles(features, opacity) {
9332
+ if (features.length === 0)
9333
+ return;
9334
+ if (opacity <= 0)
9335
+ return;
9336
+ this.#svg.push(`<g opacity="${String(opacity)}">`);
9337
+ const groups = new Map();
9338
+ features.forEach(([feature, style]) => {
9339
+ if (style.radius <= 0 || style.color.alpha <= 0)
9340
+ return;
9341
+ const translate = style.translate.isZero()
9342
+ ? ''
9343
+ : ` transform="translate(${formatPoint(style.translate, this.#scale)})"`;
9344
+ const roundedRadius = roundValue(style.radius, this.#scale);
9345
+ const strokeAttrs = style.strokeWidth > 0
9346
+ ? ` ${strokeAttr(style.strokeColor, roundValue(style.strokeWidth, this.#scale))}`
9347
+ : '';
9348
+ const key = [style.color.hex, roundedRadius, strokeAttrs, translate].join('\0');
9349
+ let group = groups.get(key);
9350
+ if (!group) {
9351
+ group = {
9352
+ points: [],
9353
+ attrs: `r="${roundedRadius}" ${fillAttr(style.color)}${strokeAttrs}${translate}`,
9354
+ };
9355
+ groups.set(key, group);
9356
+ }
9357
+ feature.geometry.forEach((ring) => {
9358
+ group.points.push(roundXY(ring[0], this.#scale));
9359
+ });
9360
+ });
9361
+ for (const { points, attrs } of groups.values()) {
9362
+ for (const [x, y] of points) {
9363
+ this.#svg.push(`<circle cx="${formatNum(x)}" cy="${formatNum(y)}" ${attrs} />`);
9364
+ }
9365
+ }
9366
+ this.#svg.push('</g>');
9367
+ }
9318
9368
  drawRasterTiles(tiles, style) {
9319
9369
  if (tiles.length === 0)
9320
9370
  return;
@@ -9347,13 +9397,32 @@ class SVGRenderer {
9347
9397
  this.#svg.push('</g>');
9348
9398
  }
9349
9399
  getString() {
9350
- return [
9351
- `<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}">`,
9352
- ...this.#svg,
9353
- '</svg>',
9354
- ].join('\n');
9400
+ const w = this.width.toFixed(0);
9401
+ const h = this.height.toFixed(0);
9402
+ const parts = [
9403
+ `<svg viewBox="0 0 ${w} ${h}" width="${w}" height="${h}" xmlns="http://www.w3.org/2000/svg">`,
9404
+ `<defs><clipPath id="vb"><rect width="${w}" height="${h}"/></clipPath></defs>`,
9405
+ `<g clip-path="url(#vb)">`,
9406
+ ];
9407
+ if (this.#backgroundColor.alpha > 0) {
9408
+ parts.push(`<rect x="-1" y="-1" width="${(this.width + 2).toFixed(0)}" height="${(this.height + 2).toFixed(0)}" ${fillAttr(this.#backgroundColor)} />`);
9409
+ }
9410
+ parts.push(...this.#svg, '</g>', '</svg>');
9411
+ return parts.join('\n');
9355
9412
  }
9356
9413
  }
9414
+ function fillAttr(color) {
9415
+ let attr = `fill="${color.rgb}"`;
9416
+ if (color.alpha < 255)
9417
+ attr += ` fill-opacity="${color.opacity.toFixed(3)}"`;
9418
+ return attr;
9419
+ }
9420
+ function strokeAttr(color, width) {
9421
+ let attr = `stroke="${color.rgb}" stroke-width="${width}"`;
9422
+ if (color.alpha < 255)
9423
+ attr += ` stroke-opacity="${color.opacity.toFixed(3)}"`;
9424
+ return attr;
9425
+ }
9357
9426
  function roundValue(v, scale) {
9358
9427
  return (v * scale).toFixed(3);
9359
9428
  }
@@ -9464,77 +9533,6 @@ function formatNum(tenths) {
9464
9533
  return (negative ? '-' : '') + String(whole) + '.' + String(frac);
9465
9534
  }
9466
9535
 
9467
- class Point2D {
9468
- x;
9469
- y;
9470
- constructor(x, y) {
9471
- this.x = x;
9472
- this.y = y;
9473
- }
9474
- isZero() {
9475
- return this.x === 0 && this.y === 0;
9476
- }
9477
- scale(factor) {
9478
- this.x *= factor;
9479
- this.y *= factor;
9480
- return this;
9481
- }
9482
- translate(offset) {
9483
- this.x += offset.x;
9484
- this.y += offset.y;
9485
- return this;
9486
- }
9487
- getProject2Pixel() {
9488
- const s = Math.sin((this.y * Math.PI) / 180.0);
9489
- return new Point2D(this.x / 360.0 + 0.5, 0.5 - (0.25 * Math.log((1 + s) / (1 - s))) / Math.PI);
9490
- }
9491
- }
9492
- class Feature {
9493
- type;
9494
- id;
9495
- properties;
9496
- patterns;
9497
- geometry;
9498
- constructor(opt) {
9499
- this.type = opt.type;
9500
- this.id = opt.id;
9501
- this.properties = opt.properties;
9502
- this.patterns = opt.patterns;
9503
- this.geometry = opt.geometry;
9504
- }
9505
- getBbox() {
9506
- let xMin = Infinity;
9507
- let yMin = Infinity;
9508
- let xMax = -Infinity;
9509
- let yMax = -Infinity;
9510
- this.geometry.forEach((ring) => {
9511
- ring.forEach((point) => {
9512
- if (xMin > point.x)
9513
- xMin = point.x;
9514
- if (yMin > point.y)
9515
- yMin = point.y;
9516
- if (xMax < point.x)
9517
- xMax = point.x;
9518
- if (yMax < point.y)
9519
- yMax = point.y;
9520
- });
9521
- });
9522
- return [xMin, yMin, xMax, yMax];
9523
- }
9524
- doesOverlap(bbox) {
9525
- const featureBbox = this.getBbox();
9526
- if (featureBbox[0] > bbox[2])
9527
- return false;
9528
- if (featureBbox[1] > bbox[3])
9529
- return false;
9530
- if (featureBbox[2] < bbox[0])
9531
- return false;
9532
- if (featureBbox[3] < bbox[1])
9533
- return false;
9534
- return true;
9535
- }
9536
- }
9537
-
9538
9536
  /*
9539
9537
  * bignumber.js v9.3.1
9540
9538
  * A JavaScript library for arbitrary-precision arithmetic.
@@ -14166,6 +14164,77 @@ function union2(features, options = {}) {
14166
14164
  else return multiPolygon(unioned, options.properties);
14167
14165
  }
14168
14166
 
14167
+ class Point2D {
14168
+ x;
14169
+ y;
14170
+ constructor(x, y) {
14171
+ this.x = x;
14172
+ this.y = y;
14173
+ }
14174
+ isZero() {
14175
+ return this.x === 0 && this.y === 0;
14176
+ }
14177
+ scale(factor) {
14178
+ this.x *= factor;
14179
+ this.y *= factor;
14180
+ return this;
14181
+ }
14182
+ translate(offset) {
14183
+ this.x += offset.x;
14184
+ this.y += offset.y;
14185
+ return this;
14186
+ }
14187
+ getProject2Pixel() {
14188
+ const s = Math.sin((this.y * Math.PI) / 180.0);
14189
+ return new Point2D(this.x / 360.0 + 0.5, 0.5 - (0.25 * Math.log((1 + s) / (1 - s))) / Math.PI);
14190
+ }
14191
+ }
14192
+ class Feature {
14193
+ type;
14194
+ id;
14195
+ properties;
14196
+ patterns;
14197
+ geometry;
14198
+ constructor(opt) {
14199
+ this.type = opt.type;
14200
+ this.id = opt.id;
14201
+ this.properties = opt.properties;
14202
+ this.patterns = opt.patterns;
14203
+ this.geometry = opt.geometry;
14204
+ }
14205
+ getBbox() {
14206
+ let xMin = Infinity;
14207
+ let yMin = Infinity;
14208
+ let xMax = -Infinity;
14209
+ let yMax = -Infinity;
14210
+ this.geometry.forEach((ring) => {
14211
+ ring.forEach((point) => {
14212
+ if (xMin > point.x)
14213
+ xMin = point.x;
14214
+ if (yMin > point.y)
14215
+ yMin = point.y;
14216
+ if (xMax < point.x)
14217
+ xMax = point.x;
14218
+ if (yMax < point.y)
14219
+ yMax = point.y;
14220
+ });
14221
+ });
14222
+ return [xMin, yMin, xMax, yMax];
14223
+ }
14224
+ doesOverlap(bbox) {
14225
+ const featureBbox = this.getBbox();
14226
+ if (featureBbox[0] > bbox[2])
14227
+ return false;
14228
+ if (featureBbox[1] > bbox[3])
14229
+ return false;
14230
+ if (featureBbox[2] < bbox[0])
14231
+ return false;
14232
+ if (featureBbox[3] < bbox[1])
14233
+ return false;
14234
+ return true;
14235
+ }
14236
+ }
14237
+
14169
14238
  function geojsonToFeature(id, polygonFeature) {
14170
14239
  const geometry = polygonFeature.geometry.coordinates.map((ring) => {
14171
14240
  return ring.map((coord) => new Point2D(coord[0], coord[1]));
@@ -14199,15 +14268,13 @@ function mergePolygons(featureList) {
14199
14268
  const turfFeatures = [];
14200
14269
  features.forEach((f) => {
14201
14270
  const rings = f.geometry.map((ring) => ring.map((p) => [p.x, p.y]));
14202
- rings.forEach((ring) => {
14203
- turfFeatures.push({
14204
- type: 'Feature',
14205
- geometry: {
14206
- type: 'Polygon',
14207
- coordinates: [ring],
14208
- },
14209
- properties: f.properties,
14210
- });
14271
+ turfFeatures.push({
14272
+ type: 'Feature',
14273
+ geometry: {
14274
+ type: 'Polygon',
14275
+ coordinates: rings,
14276
+ },
14277
+ properties: f.properties,
14211
14278
  });
14212
14279
  });
14213
14280
  const merged = union2({
@@ -15816,24 +15883,16 @@ function writeUtf8(buf, str, pos) {
15816
15883
  }
15817
15884
 
15818
15885
  const TILE_EXTENT = 4096;
15819
- async function getLayerFeatures(job) {
15886
+ async function loadVectorSource(source, job, layerFeatures) {
15887
+ const tiles = source.tiles;
15888
+ if (!tiles)
15889
+ return;
15820
15890
  const { width, height } = job.renderer;
15821
15891
  const { zoom, center } = job.view;
15822
- const { sources } = job.style;
15823
- const source = sources['versatiles-shortbread'];
15824
- if (!source)
15825
- return new Map();
15826
- if (source.type !== 'vector' || !source.tiles) {
15827
- console.error('Invalid source configuration. Expected a vector source with tile URLs.');
15828
- console.error('Source config:', source);
15829
- throw Error('Invalid source');
15830
- }
15831
- const sourceUrl = source.tiles[0];
15832
15892
  const { zoomLevel, tileSize, tiles: tileCoordinates, } = calculateTileGrid(width, height, center, zoom, source.maxzoom);
15833
- const layerFeatures = new Map();
15834
15893
  await Promise.all(tileCoordinates.map(async ({ x, y, offsetX, offsetY }) => {
15835
15894
  const offset = new Point2D(offsetX, offsetY);
15836
- const tile = await getTile(sourceUrl, zoomLevel, x, y);
15895
+ const tile = await getTile(tiles[0], zoomLevel, x, y);
15837
15896
  if (!tile)
15838
15897
  return;
15839
15898
  const vectorTile = new VectorTile(new Pbf(tile.buffer));
@@ -15877,14 +15936,108 @@ async function getLayerFeatures(job) {
15877
15936
  }
15878
15937
  }
15879
15938
  }));
15880
- for (const [name, features] of layerFeatures) {
15881
- layerFeatures.set(name, {
15882
- points: features.points,
15883
- linestrings: features.linestrings,
15884
- polygons: mergePolygons(features.polygons),
15885
- });
15939
+ }
15940
+
15941
+ function loadGeoJSONSource(sourceName, data, width, height, zoom, center, layerFeatures) {
15942
+ const existing = layerFeatures.get(sourceName);
15943
+ const features = existing ?? { points: [], linestrings: [], polygons: [] };
15944
+ if (!existing)
15945
+ layerFeatures.set(sourceName, features);
15946
+ const worldSize = 512 * 2 ** zoom;
15947
+ const centerMercator = center.getProject2Pixel();
15948
+ function projectCoord(coord) {
15949
+ const mercator = new Point2D(coord[0], coord[1]).getProject2Pixel();
15950
+ return new Point2D((mercator.x - centerMercator.x) * worldSize + width / 2, (mercator.y - centerMercator.y) * worldSize + height / 2);
15951
+ }
15952
+ function makeFeature(type, geometry, id, properties) {
15953
+ const feature = new Feature({ type, geometry, id, properties });
15954
+ if (!feature.doesOverlap([0, 0, width, height]))
15955
+ return null;
15956
+ return feature;
15957
+ }
15958
+ function extractPoints(geometry) {
15959
+ return geometry.flatMap((ring) => ring.map((p) => [p]));
15960
+ }
15961
+ function addFeature(type, geometry, id, properties) {
15962
+ switch (type) {
15963
+ case 'Point': {
15964
+ const f = makeFeature('Point', geometry, id, properties);
15965
+ if (f)
15966
+ features.points.push(f);
15967
+ break;
15968
+ }
15969
+ case 'LineString': {
15970
+ const f = makeFeature('LineString', geometry, id, properties);
15971
+ if (f) {
15972
+ features.linestrings.push(f);
15973
+ features.points.push(new Feature({ type: 'Point', geometry: extractPoints(geometry), id, properties }));
15974
+ }
15975
+ break;
15976
+ }
15977
+ case 'Polygon': {
15978
+ geometry.forEach((ring, ringIndex) => {
15979
+ const needsCW = ringIndex === 0;
15980
+ let area = 0;
15981
+ for (let i = 0; i < ring.length; i++) {
15982
+ const j = (i + 1) % ring.length;
15983
+ area += ring[i].x * ring[j].y;
15984
+ area -= ring[j].x * ring[i].y;
15985
+ }
15986
+ if (area < 0 !== needsCW)
15987
+ ring.reverse();
15988
+ });
15989
+ const f = makeFeature('Polygon', geometry, id, properties);
15990
+ if (f) {
15991
+ features.polygons.push(f);
15992
+ features.linestrings.push(new Feature({ type: 'LineString', geometry, id, properties }));
15993
+ features.points.push(new Feature({ type: 'Point', geometry: extractPoints(geometry), id, properties }));
15994
+ }
15995
+ break;
15996
+ }
15997
+ }
15998
+ }
15999
+ function processGeometry(geom, id, properties) {
16000
+ const coords = geom.coordinates;
16001
+ switch (geom.type) {
16002
+ case 'Point':
16003
+ addFeature('Point', [[projectCoord(coords)]], id, properties);
16004
+ break;
16005
+ case 'MultiPoint':
16006
+ addFeature('Point', coords.map((c) => [projectCoord(c)]), id, properties);
16007
+ break;
16008
+ case 'LineString':
16009
+ addFeature('LineString', [coords.map((c) => projectCoord(c))], id, properties);
16010
+ break;
16011
+ case 'MultiLineString':
16012
+ addFeature('LineString', coords.map((line) => line.map((c) => projectCoord(c))), id, properties);
16013
+ break;
16014
+ case 'Polygon':
16015
+ addFeature('Polygon', coords.map((ring) => ring.map((c) => projectCoord(c))), id, properties);
16016
+ break;
16017
+ case 'MultiPolygon':
16018
+ addFeature('Polygon', coords.flatMap((polygon) => polygon.map((ring) => ring.map((c) => projectCoord(c)))), id, properties);
16019
+ break;
16020
+ case 'GeometryCollection':
16021
+ for (const g of geom.geometries) {
16022
+ processGeometry(g, id, properties);
16023
+ }
16024
+ break;
16025
+ }
16026
+ }
16027
+ const geojson = data;
16028
+ switch (geojson.type) {
16029
+ case 'FeatureCollection':
16030
+ for (const f of geojson.features) {
16031
+ processGeometry(f.geometry, f.id, (f.properties ?? {}));
16032
+ }
16033
+ break;
16034
+ case 'Feature':
16035
+ processGeometry(geojson.geometry, geojson.id, (geojson.properties ?? {}));
16036
+ break;
16037
+ default:
16038
+ processGeometry(geojson, undefined, {});
16039
+ break;
15886
16040
  }
15887
- return layerFeatures;
15888
16041
  }
15889
16042
 
15890
16043
  async function getRasterTiles(job, sourceName) {
@@ -15915,6 +16068,34 @@ async function getRasterTiles(job, sourceName) {
15915
16068
  return rasterTiles.filter((tile) => tile !== null);
15916
16069
  }
15917
16070
 
16071
+ async function getLayerFeatures(job) {
16072
+ const { width, height } = job.renderer;
16073
+ const { zoom, center } = job.view;
16074
+ const { sources } = job.style;
16075
+ const layerFeatures = new Map();
16076
+ for (const [sourceName, sourceSpec] of Object.entries(sources)) {
16077
+ const source = sourceSpec;
16078
+ switch (source.type) {
16079
+ case 'vector':
16080
+ await loadVectorSource(source, job, layerFeatures);
16081
+ break;
16082
+ case 'geojson':
16083
+ if (source.data) {
16084
+ loadGeoJSONSource(sourceName, source.data, width, height, zoom, center, layerFeatures);
16085
+ }
16086
+ break;
16087
+ }
16088
+ }
16089
+ for (const [name, features] of layerFeatures) {
16090
+ layerFeatures.set(name, {
16091
+ points: features.points,
16092
+ linestrings: features.linestrings,
16093
+ polygons: mergePolygons(features.polygons),
16094
+ });
16095
+ }
16096
+ return layerFeatures;
16097
+ }
16098
+
15918
16099
  /**
15919
16100
  * Wraps a source/composite expression for per-feature evaluation.
15920
16101
  */
@@ -16080,7 +16261,7 @@ async function render(job) {
16080
16261
  continue;
16081
16262
  case 'fill':
16082
16263
  {
16083
- const polygons = layerFeatures.get(layerStyle.sourceLayer)?.polygons;
16264
+ const polygons = (layerFeatures.get(layerStyle.sourceLayer) ?? layerFeatures.get(layerStyle.source))?.polygons;
16084
16265
  if (!polygons || polygons.length === 0)
16085
16266
  continue;
16086
16267
  const filter = featureFilter(layerStyle.filter);
@@ -16098,7 +16279,7 @@ async function render(job) {
16098
16279
  continue;
16099
16280
  case 'line':
16100
16281
  {
16101
- const lineStrings = layerFeatures.get(layerStyle.sourceLayer)?.linestrings;
16282
+ const lineStrings = (layerFeatures.get(layerStyle.sourceLayer) ?? layerFeatures.get(layerStyle.source))?.linestrings;
16102
16283
  if (!lineStrings || lineStrings.length === 0)
16103
16284
  continue;
16104
16285
  const filter = featureFilter(layerStyle.filter);
@@ -16138,6 +16319,27 @@ async function render(job) {
16138
16319
  }
16139
16320
  continue;
16140
16321
  case 'circle':
16322
+ {
16323
+ const points = (layerFeatures.get(layerStyle.sourceLayer) ?? layerFeatures.get(layerStyle.source))?.points;
16324
+ if (!points || points.length === 0)
16325
+ continue;
16326
+ const filter = featureFilter(layerStyle.filter);
16327
+ const pointFeatures = points.filter((feature) => filter.filter({ zoom }, feature));
16328
+ if (pointFeatures.length === 0)
16329
+ continue;
16330
+ renderer.drawCircles(pointFeatures.map((feature) => [
16331
+ feature,
16332
+ {
16333
+ color: new Color(getPaint('circle-color', feature)),
16334
+ radius: getPaint('circle-radius', feature),
16335
+ blur: getPaint('circle-blur', feature),
16336
+ translate: new Point2D(...getPaint('circle-translate', feature)),
16337
+ strokeWidth: getPaint('circle-stroke-width', feature),
16338
+ strokeColor: new Color(getPaint('circle-stroke-color', feature)),
16339
+ },
16340
+ ]), getPaint('circle-opacity', pointFeatures[0]));
16341
+ }
16342
+ continue;
16141
16343
  case 'color-relief':
16142
16344
  case 'fill-extrusion':
16143
16345
  case 'heatmap':