@guardian/interactive-component-library 0.2.0-rc-06ceabc.0 → 0.2.0-rc1

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.
@@ -6180,175 +6180,6 @@
6180
6180
  )
6181
6181
  ] });
6182
6182
  }
6183
- class FeatureRenderer {
6184
- constructor() {
6185
- this.drawingFunction = d3Geo.geoPath();
6186
- }
6187
- setStyle(style2) {
6188
- this.style = style2;
6189
- }
6190
- render(frameState, feature, context) {
6191
- if (!this.style) {
6192
- return;
6193
- }
6194
- const { projection, transform, pixelRatio } = frameState.viewState;
6195
- const { stroke, fill } = this.style;
6196
- this.drawingFunction.context(context);
6197
- context.beginPath();
6198
- const geometries = feature.getProjectedGeometries(projection);
6199
- if (frameState.debug) {
6200
- try {
6201
- validateGeometries(geometries);
6202
- } catch {
6203
- console.error(
6204
- `Invalid geometry. Feature skipped during rendering. Click here to inspect geometry: ${generateDebugUrl(feature)}
6205
- `,
6206
- feature
6207
- );
6208
- }
6209
- }
6210
- for (const geometry of geometries) {
6211
- this.drawingFunction(geometry);
6212
- }
6213
- if (fill) {
6214
- context.fillStyle = fill.getRgba();
6215
- context.fill();
6216
- }
6217
- if (stroke) {
6218
- context.lineWidth = stroke.width * pixelRatio / transform.k;
6219
- context.strokeStyle = stroke.getRgba();
6220
- context.stroke();
6221
- }
6222
- }
6223
- }
6224
- const textPadding = {
6225
- top: 20,
6226
- right: 20,
6227
- bottom: 20,
6228
- left: 20
6229
- };
6230
- class TextLayerRenderer {
6231
- constructor(layer) {
6232
- this.layer = layer;
6233
- this.featureRenderer = new FeatureRenderer();
6234
- this._element = document.createElement("div");
6235
- this._element.className = "gv-text-layer";
6236
- const style2 = this._element.style;
6237
- style2.position = "absolute";
6238
- style2.width = "100%";
6239
- style2.height = "100%";
6240
- style2.pointerEvents = "none";
6241
- style2.overflow = "hidden";
6242
- }
6243
- renderFrame(frameState, targetElement) {
6244
- if (this.layer.opacity === 0) return targetElement;
6245
- const { declutterTree } = frameState;
6246
- const { projection, viewPortSize, sizeInPixels, visibleExtent, transform } = frameState.viewState;
6247
- this._element.style.opacity = this.layer.opacity;
6248
- const source = this.layer.source;
6249
- const features = source.getFeaturesInExtent(visibleExtent);
6250
- const textElements = [];
6251
- for (const feature of features) {
6252
- const geometries = feature.getProjectedGeometries(projection);
6253
- const point = geometries.find((d2) => d2.type === "Point");
6254
- if (!point) {
6255
- throw new Error(
6256
- `Expected Point geometry for feature in TextLayer: ${feature}`
6257
- );
6258
- }
6259
- const styleFunction2 = feature.getStyleFunction() || this.layer.getStyleFunction();
6260
- const featureStyle = styleFunction2(feature);
6261
- const textElement = this.getTextElementWithID(feature.uid);
6262
- textElement.innerText = featureStyle.text.content;
6263
- const [relativeX, relativeY] = transform.apply(point.coordinates).map((d2, i) => d2 / sizeInPixels[i]);
6264
- const position = {
6265
- left: `${relativeX * 100}%`,
6266
- top: `${relativeY * 100}%`
6267
- };
6268
- this.styleTextElement(textElement, featureStyle.text, position);
6269
- const bbox = this.getElementBBox(textElement, {
6270
- x: relativeX * viewPortSize[0],
6271
- y: relativeY * viewPortSize[1]
6272
- });
6273
- if (declutterTree.collides(bbox)) {
6274
- continue;
6275
- }
6276
- declutterTree.insert(bbox);
6277
- if (this.layer.drawCollisionBoxes) {
6278
- const collisionBoxDebugElement = this.getCollisionBoxElement(bbox);
6279
- textElements.push(collisionBoxDebugElement);
6280
- }
6281
- textElements.push(textElement);
6282
- }
6283
- replaceChildren(this._element, textElements);
6284
- return this._element;
6285
- }
6286
- getTextElementWithID(id2) {
6287
- const elementId = `text-feature-${id2}`;
6288
- let textElement = this._element.querySelector(`#${elementId}`);
6289
- if (!textElement) {
6290
- textElement = document.createElement("div");
6291
- textElement.id = elementId;
6292
- }
6293
- return textElement;
6294
- }
6295
- styleTextElement(element, textStyle, position) {
6296
- const style2 = element.style;
6297
- style2.position = "absolute";
6298
- style2.transform = `translate(-50%, -50%)`;
6299
- style2.left = position.left;
6300
- style2.top = position.top;
6301
- style2.textAlign = "center";
6302
- style2.whiteSpace = "nowrap";
6303
- style2.fontFamily = textStyle.fontFamily;
6304
- style2.fontSize = textStyle.fontSize;
6305
- style2.fontWeight = textStyle.fontWeight;
6306
- style2.lineHeight = textStyle.lineHeight;
6307
- style2.color = textStyle.color;
6308
- style2.textShadow = textStyle.textShadow;
6309
- style2.padding = `${textPadding.top}px ${textPadding.right}px ${textPadding.bottom}px ${textPadding.left}px`;
6310
- }
6311
- getElementBBox(element, position) {
6312
- if (!element.parentElement) {
6313
- document.body.appendChild(element);
6314
- }
6315
- const { width, height } = element.getBoundingClientRect();
6316
- if (element.parentElement !== this._element) {
6317
- element.remove();
6318
- }
6319
- return {
6320
- minX: Math.floor(position.x) - width / 2,
6321
- minY: Math.floor(position.y) - height / 2,
6322
- maxX: Math.ceil(position.x + width / 2),
6323
- maxY: Math.ceil(position.y + height / 2)
6324
- };
6325
- }
6326
- getCollisionBoxElement(bbox) {
6327
- const element = document.createElement("div");
6328
- const style2 = element.style;
6329
- style2.position = "absolute";
6330
- style2.left = `${bbox.minX}px`;
6331
- style2.top = `${bbox.minY}px`;
6332
- style2.width = `${bbox.maxX - bbox.minX}px`;
6333
- style2.height = `${bbox.maxY - bbox.minY}px`;
6334
- style2.border = "2px solid black";
6335
- return element;
6336
- }
6337
- }
6338
- class Style {
6339
- constructor(properties) {
6340
- this.stroke = properties == null ? void 0 : properties.stroke;
6341
- this.fill = properties == null ? void 0 : properties.fill;
6342
- this.text = properties == null ? void 0 : properties.text;
6343
- }
6344
- clone() {
6345
- return new Style({
6346
- stroke: this.stroke,
6347
- fill: this.fill,
6348
- text: this.text
6349
- });
6350
- }
6351
- }
6352
6183
  function memoise(fn) {
6353
6184
  let called = false;
6354
6185
  let lastResult;
@@ -6365,928 +6196,1302 @@
6365
6196
  return lastResult;
6366
6197
  };
6367
6198
  }
6368
- function toRgba(color2, opacity = 1) {
6369
- color2 = color2.replace(/\s+/g, "").toLowerCase();
6370
- if (color2.startsWith("#")) {
6371
- color2 = color2.replace(/^#/, "");
6372
- if (color2.length === 3) {
6373
- color2 = color2.split("").map((char) => char + char).join("");
6374
- }
6375
- let r = parseInt(color2.substring(0, 2), 16);
6376
- let g = parseInt(color2.substring(2, 4), 16);
6377
- let b = parseInt(color2.substring(4, 6), 16);
6378
- return `rgba(${r}, ${g}, ${b}, ${opacity})`;
6379
- }
6380
- let rgbaMatch = color2.match(/^rgba?\((\d+),(\d+),(\d+)(?:,(\d+(\.\d+)?))?\)$/);
6381
- if (rgbaMatch) {
6382
- let r = parseInt(rgbaMatch[1], 10);
6383
- let g = parseInt(rgbaMatch[2], 10);
6384
- let b = parseInt(rgbaMatch[3], 10);
6385
- let a = rgbaMatch[4] !== void 0 ? parseFloat(rgbaMatch[4]) : opacity;
6386
- return `rgba(${r}, ${g}, ${b}, ${a})`;
6387
- }
6388
- throw new Error("Unsupported color format");
6389
- }
6390
- class Stroke {
6391
- constructor(options) {
6392
- this.color = (options == null ? void 0 : options.color) || "#121212";
6393
- this.width = (options == null ? void 0 : options.width) || 0.5;
6394
- this.opacity = (options == null ? void 0 : options.opacity) || 1;
6395
- this._getRgba = memoise(toRgba);
6199
+ class Feature {
6200
+ /**
6201
+ * Represents a feature on the map
6202
+ * @constructor
6203
+ * @param {Object} props - The properties for the feature.
6204
+ * @property {string} id - The unique identifier of the feature
6205
+ * @property {Array} geometries - The geometries of the feature
6206
+ * @property {Object} properties - The properties of the feature
6207
+ * @property {import("./styles").Style | import("./styles").StyleFunction} style - The style of the feature
6208
+ */
6209
+ constructor({ id: id2, geometries, properties, style: style2 }) {
6210
+ this.id = id2;
6211
+ this.geometries = geometries;
6212
+ this.properties = properties;
6213
+ this.style = style2;
6214
+ this.uid = createUid();
6215
+ this._getProjectedGeometries = memoise((projection) => {
6216
+ return this.geometries.map(
6217
+ (d2) => d2.getProjected(projection, projection.revision)
6218
+ );
6219
+ }).bind(this);
6396
6220
  }
6397
- getRgba() {
6398
- return this._getRgba(this.color, this.opacity);
6221
+ setGeometries(geometries) {
6222
+ this.geometries = geometries;
6223
+ this._extent = void 0;
6399
6224
  }
6400
- }
6401
- class Fill {
6402
- constructor(options) {
6403
- this.color = (options == null ? void 0 : options.color) || "#CCC";
6404
- this.opacity = (options == null ? void 0 : options.opacity) || 1;
6405
- this._getRgba = memoise(toRgba);
6225
+ getExtent() {
6226
+ if (this._extent) return this._extent;
6227
+ const extent = this.geometries.reduce((combinedExtent, geometry) => {
6228
+ if (!combinedExtent) return geometry.extent;
6229
+ return combineExtents(geometry.extent, combinedExtent);
6230
+ }, null);
6231
+ this._extent = extent;
6232
+ return extent;
6406
6233
  }
6407
- getRgba() {
6408
- return this._getRgba(this.color, this.opacity);
6234
+ getProjectedGeometries(projection) {
6235
+ return this._getProjectedGeometries(projection, projection.revision);
6409
6236
  }
6410
- }
6411
- class Text {
6412
- constructor(options) {
6413
- this.content = options == null ? void 0 : options.content;
6414
- this.fontFamily = (options == null ? void 0 : options.fontFamily) || "var(--text-sans)";
6415
- this.fontSize = (options == null ? void 0 : options.fontSize) || "17px";
6416
- this.fontWeight = (options == null ? void 0 : options.fontWeight) || "400";
6417
- this.lineHeight = (options == null ? void 0 : options.lineHeight) || 1.3;
6418
- this.color = (options == null ? void 0 : options.color) || "#121212";
6419
- this.textShadow = (options == null ? void 0 : options.textShadow) || "1px 1px 0px #f6f6f6, -1px -1px 0px #f6f6f6, -1px 1px 0px #f6f6f6, 1px -1px #f6f6f6";
6237
+ getStyleFunction() {
6238
+ const style2 = this.style;
6239
+ if (!style2) return null;
6240
+ if (typeof style2 === "function") return style2;
6241
+ return () => {
6242
+ return style2;
6243
+ };
6420
6244
  }
6421
- }
6422
- class TinyQueue {
6423
- constructor(data = [], compare = defaultCompare) {
6424
- this.data = data;
6425
- this.length = this.data.length;
6426
- this.compare = compare;
6427
- if (this.length > 0) {
6428
- for (let i = (this.length >> 1) - 1; i >= 0; i--) this._down(i);
6245
+ containsCoordinate(coordinate) {
6246
+ if (!containsCoordinate(this.getExtent(), coordinate)) {
6247
+ return false;
6429
6248
  }
6249
+ for (const geometries of this.geometries) {
6250
+ if (d3Geo.geoContains(geometries.getGeoJSON(), coordinate)) {
6251
+ return true;
6252
+ }
6253
+ }
6254
+ return false;
6430
6255
  }
6431
- push(item) {
6432
- this.data.push(item);
6433
- this.length++;
6434
- this._up(this.length - 1);
6435
- }
6436
- pop() {
6437
- if (this.length === 0) return void 0;
6438
- const top = this.data[0];
6439
- const bottom = this.data.pop();
6440
- this.length--;
6441
- if (this.length > 0) {
6442
- this.data[0] = bottom;
6443
- this._down(0);
6444
- }
6445
- return top;
6256
+ clone() {
6257
+ return new Feature({
6258
+ id: this.id,
6259
+ geometries: this.geometries.map((d2) => d2.clone()),
6260
+ properties: this.properties,
6261
+ style: this.style
6262
+ });
6446
6263
  }
6447
- peek() {
6448
- return this.data[0];
6264
+ /**
6265
+ * Returns the geometries as a GeoJSON object
6266
+ * @returns {Object} The GeoJSON representation of the geometries
6267
+ */
6268
+ getGeoJSON() {
6269
+ const geometries = this.geometries.map((d2) => d2.getGeoJSON());
6270
+ if (geometries.length === 1) return geometries[0];
6271
+ return {
6272
+ type: "Feature",
6273
+ geometry: this._getGeometryGeoJSON(),
6274
+ properties: this.properties
6275
+ };
6449
6276
  }
6450
- _up(pos) {
6451
- const { data, compare } = this;
6452
- const item = data[pos];
6453
- while (pos > 0) {
6454
- const parent = pos - 1 >> 1;
6455
- const current = data[parent];
6456
- if (compare(item, current) >= 0) break;
6457
- data[pos] = current;
6458
- pos = parent;
6277
+ _getGeometryGeoJSON() {
6278
+ const geometries = this.geometries.map((d2) => d2.getGeoJSON());
6279
+ if (geometries.length === 0) throw new Error("Feature has no geometries");
6280
+ if (geometries.length === 1) return geometries[0];
6281
+ if (geometries[0].type === "Polygon") {
6282
+ return {
6283
+ type: "MultiPolygon",
6284
+ coordinates: geometries.map((d2) => d2.coordinates)
6285
+ };
6286
+ } else if (geometries[0].type === "LineString") {
6287
+ return {
6288
+ type: "MultiLineString",
6289
+ coordinates: geometries.map((d2) => d2.coordinates)
6290
+ };
6291
+ } else if (geometries[0].type === "Point") {
6292
+ return {
6293
+ type: "MultiPoint",
6294
+ coordinates: geometries.map((d2) => d2.coordinates)
6295
+ };
6459
6296
  }
6460
- data[pos] = item;
6297
+ throw new Error("Could not determine geometry type");
6461
6298
  }
6462
- _down(pos) {
6463
- const { data, compare } = this;
6464
- const halfLength = this.length >> 1;
6465
- const item = data[pos];
6466
- while (pos < halfLength) {
6467
- let left = (pos << 1) + 1;
6468
- let best = data[left];
6469
- const right = left + 1;
6470
- if (right < this.length && compare(data[right], best) < 0) {
6471
- left = right;
6472
- best = data[right];
6473
- }
6474
- if (compare(best, item) >= 0) break;
6475
- data[pos] = best;
6476
- pos = left;
6477
- }
6478
- data[pos] = item;
6299
+ }
6300
+ class Geometry {
6301
+ /**
6302
+ * Represents vector geometry
6303
+ * @constructor
6304
+ * @param {Object} options
6305
+ * @param {string} options.type - The type of geometry (e.g., 'Point', 'LineString', 'Polygon')
6306
+ * @param {Array} options.extent - The extent of the geometry (e.g., [xmin, ymin, xmax, ymax])
6307
+ * @param {Array} options.coordinates - The coordinates of the geometry (e.g., [[x1, y1], [x2, y2], ...])
6308
+ */
6309
+ constructor({ type, extent, coordinates }) {
6310
+ this.type = type;
6311
+ this.extent = extent;
6312
+ this.coordinates = coordinates;
6313
+ this.getProjected = memoise(this._getProjected).bind(this);
6314
+ }
6315
+ /**
6316
+ * Returns the geometry as a GeoJSON object
6317
+ * @function
6318
+ * @param {import("../projection").Projection} projection - The projection to use for the geometry
6319
+ * @returns {Object} A GeoJSON representation of the projected geometry
6320
+ * @private
6321
+ */
6322
+ // eslint-disable-next-line no-unused-vars
6323
+ _getProjected(projection) {
6324
+ throw new Error("Not implemented");
6325
+ }
6326
+ /**
6327
+ * Returns the geometry as a GeoJSON object
6328
+ * @returns {Object} The GeoJSON representation of the geometry
6329
+ */
6330
+ getGeoJSON() {
6331
+ return {
6332
+ type: this.type,
6333
+ coordinates: this.coordinates
6334
+ };
6479
6335
  }
6480
6336
  }
6481
- function defaultCompare(a, b) {
6482
- return a < b ? -1 : a > b ? 1 : 0;
6337
+ class LineString extends Geometry {
6338
+ constructor({ type = "LineString", extent, coordinates }) {
6339
+ super({ type, extent, coordinates });
6340
+ }
6341
+ _getProjected(projection) {
6342
+ const projected = [];
6343
+ for (const point of this.coordinates) {
6344
+ projected.push(projection(point));
6345
+ }
6346
+ return {
6347
+ type: this.type,
6348
+ coordinates: projected
6349
+ };
6350
+ }
6483
6351
  }
6484
- function knn(tree, x, y, n2, predicate, maxDistance) {
6485
- let node = tree.data;
6486
- const result = [];
6487
- const toBBox = tree.toBBox;
6488
- const queue = new TinyQueue(void 0, compareDist);
6489
- while (node) {
6490
- for (let i = 0; i < node.children.length; i++) {
6491
- const child = node.children[i];
6492
- const dist = boxDist(x, y, node.leaf ? toBBox(child) : child);
6493
- {
6494
- queue.push({
6495
- node: child,
6496
- isItem: node.leaf,
6497
- dist
6498
- });
6352
+ class Polygon extends Geometry {
6353
+ constructor({ type = "Polygon", extent, coordinates }) {
6354
+ super({ type, extent, coordinates });
6355
+ }
6356
+ _getProjected(projection) {
6357
+ const projected = [];
6358
+ const rings = this.coordinates;
6359
+ for (const ring of rings) {
6360
+ const projectedRing = [];
6361
+ for (const point of ring) {
6362
+ const projectedPoint = projection(point);
6363
+ if (projectedPoint) {
6364
+ projectedRing.push(projectedPoint);
6365
+ } else {
6366
+ break;
6367
+ }
6368
+ }
6369
+ if (projectedRing.length > 0) {
6370
+ projected.push(projectedRing);
6499
6371
  }
6500
6372
  }
6501
- while (queue.length && queue.peek().isItem) {
6502
- const candidate = queue.pop().node;
6503
- if (!predicate || predicate(candidate))
6504
- result.push(candidate);
6505
- if (result.length === n2) return result;
6506
- }
6507
- node = queue.pop();
6508
- if (node) node = node.node;
6373
+ return {
6374
+ type: this.type,
6375
+ coordinates: projected
6376
+ };
6509
6377
  }
6510
- return result;
6511
- }
6512
- function compareDist(a, b) {
6513
- return a.dist - b.dist;
6514
- }
6515
- function boxDist(x, y, box) {
6516
- const dx = axisDist(x, box.minX, box.maxX), dy = axisDist(y, box.minY, box.maxY);
6517
- return dx * dx + dy * dy;
6518
- }
6519
- function axisDist(k, min, max) {
6520
- return k < min ? min - k : k <= max ? 0 : k - max;
6521
- }
6522
- class VectorSource {
6523
- constructor({ features }) {
6524
- this.dispatcher = new Dispatcher(this);
6525
- this._featuresRtree = new RBush();
6526
- this.setFeatures(features);
6378
+ getOuterRing() {
6379
+ return this.coordinates[0];
6527
6380
  }
6528
- tearDown() {
6529
- this.dispatcher = null;
6381
+ setOuterRing(coordinates) {
6382
+ this.coordinates[0] = coordinates;
6530
6383
  }
6531
- getFeatures() {
6532
- return this._features;
6384
+ setCoordinates(coordinates) {
6385
+ this.coordinates = coordinates;
6533
6386
  }
6534
- getFeaturesAtCoordinate(coordinate) {
6535
- const [lon, lat] = coordinate;
6536
- const features = knn(
6537
- this._featuresRtree,
6538
- lon,
6539
- lat,
6540
- 10,
6541
- (d2) => d2.feature.containsCoordinate(coordinate)
6542
- ).map((d2) => {
6543
- const midX = d2.minX + (d2.minX + d2.maxX) / 2;
6544
- const midY = d2.minY + (d2.minY + d2.maxY) / 2;
6545
- d2.distance = Math.hypot(midX - lon, midY - lat);
6546
- return d2;
6387
+ clone() {
6388
+ return new Polygon({
6389
+ extent: this.extent,
6390
+ coordinates: JSON.parse(JSON.stringify(this.coordinates))
6547
6391
  });
6548
- features.sort((a, b) => a.distance - b.distance);
6549
- return features.map((d2) => d2.feature);
6550
6392
  }
6551
- getFeaturesInExtent(extent) {
6552
- const [minX, minY, maxX, maxY] = extent;
6553
- return this._featuresRtree.search({ minX, minY, maxX, maxY }).map((d2) => d2.feature);
6393
+ }
6394
+ class Point extends Geometry {
6395
+ constructor({ type = "Point", coordinates }) {
6396
+ super({ type, extent: null, coordinates });
6397
+ this.extent = [...coordinates, ...coordinates];
6554
6398
  }
6555
- setFeatures(features) {
6556
- this._featuresRtree.clear();
6557
- for (const feature of features) {
6558
- const [minX, minY, maxX, maxY] = feature.getExtent();
6559
- this._featuresRtree.insert({
6560
- minX: Math.floor(minX),
6561
- minY: Math.floor(minY),
6562
- maxX: Math.ceil(maxX),
6563
- maxY: Math.ceil(maxY),
6564
- feature
6565
- });
6566
- }
6567
- this._features = features;
6568
- this.dispatcher.dispatch(MapEvent.CHANGE);
6399
+ _getProjected(projection) {
6400
+ return {
6401
+ type: this.type,
6402
+ coordinates: projection(this.coordinates)
6403
+ };
6569
6404
  }
6570
6405
  }
6571
- class TextLayer {
6572
- /** @param {TextLayerComponentProps} props */
6573
- static Component({
6574
- features,
6575
- style: style2,
6576
- minZoom,
6577
- opacity,
6578
- declutter,
6579
- drawCollisionBoxes
6580
- }) {
6581
- const { registerLayer } = hooks.useContext(MapContext);
6582
- const layer = hooks.useMemo(
6583
- () => TextLayer.with(features, {
6584
- style: style2,
6585
- minZoom,
6586
- opacity,
6587
- declutter,
6588
- drawCollisionBoxes
6589
- }),
6590
- // eslint-disable-next-line react-hooks/exhaustive-deps
6591
- [features, minZoom, opacity, declutter, drawCollisionBoxes]
6592
- );
6593
- registerLayer(layer);
6594
- hooks.useEffect(() => {
6595
- layer.style = style2;
6596
- }, [style2]);
6406
+ class GeoJSON {
6407
+ readFeaturesFromObject(object) {
6408
+ const geoJSONObject = object;
6409
+ let features = null;
6410
+ if (geoJSONObject["type"] === "FeatureCollection") {
6411
+ const geoJSONFeatureCollection = object;
6412
+ features = [];
6413
+ const geoJSONFeatures = geoJSONFeatureCollection["features"];
6414
+ for (let i = 0, ii = geoJSONFeatures.length; i < ii; ++i) {
6415
+ const featureObject = this.readFeatureFromObject(geoJSONFeatures[i]);
6416
+ if (!featureObject) {
6417
+ continue;
6418
+ }
6419
+ features.push(featureObject);
6420
+ }
6421
+ } else if (geoJSONObject["type"] === "Feature") {
6422
+ features = [this.readFeatureFromObject(geoJSONObject)];
6423
+ } else if (Array.isArray(geoJSONObject)) {
6424
+ features = [];
6425
+ for (let i = 0, ii = geoJSONObject.length; i < ii; ++i) {
6426
+ const featureObject = this.readFeatureFromObject(geoJSONObject[i]);
6427
+ if (!featureObject) {
6428
+ continue;
6429
+ }
6430
+ features.push(featureObject);
6431
+ }
6432
+ } else {
6433
+ try {
6434
+ const geometries = this.readGeometriesFromObject(geoJSONObject);
6435
+ const feature = new Feature({ geometries });
6436
+ features = [feature];
6437
+ } catch {
6438
+ console.warn("Unable to interpret GeoJSON:", geoJSONObject);
6439
+ return;
6440
+ }
6441
+ }
6442
+ return features.flat();
6443
+ }
6444
+ readFeatureFromObject(geoJSONObject) {
6445
+ const geometries = this.readGeometriesFromObject(geoJSONObject["geometry"]);
6446
+ if (geometries.length > 0) {
6447
+ return new Feature({
6448
+ id: geoJSONObject["id"],
6449
+ geometries,
6450
+ properties: geoJSONObject["properties"]
6451
+ });
6452
+ }
6597
6453
  return null;
6598
6454
  }
6455
+ readGeometriesFromObject(geometry) {
6456
+ const geometries = [];
6457
+ if (geometry.type === "Polygon") {
6458
+ const polygon = this.readPolygonForCoordinates(geometry.coordinates);
6459
+ geometries.push(polygon);
6460
+ } else if (geometry.type === "MultiPolygon") {
6461
+ for (const polygonCoordinates of geometry.coordinates) {
6462
+ const polygon = this.readPolygonForCoordinates(polygonCoordinates);
6463
+ geometries.push(polygon);
6464
+ }
6465
+ } else if (geometry.type === "LineString") {
6466
+ const lineString = this.readLineStringForCoordinates(geometry.coordinates);
6467
+ geometries.push(lineString);
6468
+ } else if (geometry.type === "MultiLineString") {
6469
+ for (const lineStringCoordinates of geometry.coordinates) {
6470
+ const lineString = this.readLineStringForCoordinates(
6471
+ lineStringCoordinates
6472
+ );
6473
+ geometries.push(lineString);
6474
+ }
6475
+ } else if (geometry.type === "Point") {
6476
+ const point = this.readPointForCoordinates(geometry.coordinates);
6477
+ geometries.push(point);
6478
+ }
6479
+ return geometries;
6480
+ }
6481
+ readPolygonForCoordinates(coordinates) {
6482
+ const outerRing = coordinates[0];
6483
+ const extent = extentForCoordinates(outerRing);
6484
+ return new Polygon({ extent, coordinates });
6485
+ }
6486
+ readLineStringForCoordinates(coordinates) {
6487
+ const extent = extentForCoordinates(coordinates);
6488
+ return new LineString({ extent, coordinates });
6489
+ }
6490
+ readPointForCoordinates(coordinates) {
6491
+ return new Point({ coordinates });
6492
+ }
6493
+ }
6494
+ class FeatureCollection {
6599
6495
  /**
6600
- * @param {import("../Feature").Feature[]} features
6601
- * @param {TextLayerOptions} options
6496
+ * Create a feature collection from GeoJSON features.
6497
+ * @param {Object[]} geoJSON - The GeoJSON object
6498
+ * @returns {FeatureCollection} The feature collection
6602
6499
  */
6603
- static with(features, options) {
6604
- const source = new VectorSource({ features });
6605
- return new TextLayer({ source, ...options });
6500
+ static fromGeoJSON(geoJSON) {
6501
+ const features = new GeoJSON().readFeaturesFromObject(geoJSON);
6502
+ return new FeatureCollection(features);
6606
6503
  }
6607
6504
  /**
6608
- * @param {Object} params
6609
- * @param {VectorSource} params.source
6610
- * @param {Style} [params.style=undefined]
6611
- * @param {number} [params.minZoom=0]
6612
- * @param {number} [params.opacity=1]
6613
- * @param {boolean} [params.declutter=true]
6614
- * @param {boolean} [params.drawCollisionBoxes=false]
6505
+ * Create a feature collection.
6506
+ * @constructor
6507
+ * @param {import("./Feature").Feature[]} features - The features to put in the collection
6615
6508
  */
6616
- constructor({
6617
- source,
6618
- style: style2,
6619
- minZoom = 0,
6620
- opacity = 1,
6621
- declutter = true,
6622
- drawCollisionBoxes = false
6623
- }) {
6624
- this.source = source;
6625
- this._style = style2;
6626
- this.minZoom = minZoom;
6627
- this.opacity = opacity;
6628
- this.declutter = declutter;
6629
- this.drawCollisionBoxes = drawCollisionBoxes;
6630
- this.renderer = new TextLayerRenderer(this);
6631
- this.dispatcher = new Dispatcher(this);
6509
+ constructor(features) {
6510
+ this.features = features;
6632
6511
  }
6633
- tearDown() {
6634
- this.dispatcher = null;
6512
+ }
6513
+ class Style {
6514
+ constructor(properties) {
6515
+ this.stroke = properties == null ? void 0 : properties.stroke;
6516
+ this.fill = properties == null ? void 0 : properties.fill;
6517
+ this.text = properties == null ? void 0 : properties.text;
6635
6518
  }
6636
- get style() {
6637
- if (this._style) return this._style;
6638
- const defaultStyle = new Style({
6639
- text: new Text()
6519
+ clone() {
6520
+ return new Style({
6521
+ stroke: this.stroke,
6522
+ fill: this.fill,
6523
+ text: this.text
6640
6524
  });
6641
- return defaultStyle;
6642
6525
  }
6643
- set style(style2) {
6644
- this._style = style2;
6645
- this.dispatcher.dispatch(MapEvent.CHANGE);
6526
+ }
6527
+ function toRgba(color2, opacity = 1) {
6528
+ color2 = color2.replace(/\s+/g, "").toLowerCase();
6529
+ if (color2.startsWith("#")) {
6530
+ color2 = color2.replace(/^#/, "");
6531
+ if (color2.length === 3) {
6532
+ color2 = color2.split("").map((char) => char + char).join("");
6533
+ }
6534
+ let r = parseInt(color2.substring(0, 2), 16);
6535
+ let g = parseInt(color2.substring(2, 4), 16);
6536
+ let b = parseInt(color2.substring(4, 6), 16);
6537
+ return `rgba(${r}, ${g}, ${b}, ${opacity})`;
6646
6538
  }
6647
- getExtent() {
6648
- if (this._extent) return this._extent;
6649
- const features = this.source.getFeatures();
6650
- const extent = features.reduce((combinedExtent, feature) => {
6651
- const featureExtent = feature.getExtent();
6652
- if (!combinedExtent) return featureExtent;
6653
- return combineExtents(featureExtent, combinedExtent);
6654
- }, null);
6655
- this._extent = extent;
6656
- return extent;
6539
+ let rgbaMatch = color2.match(/^rgba?\((\d+),(\d+),(\d+)(?:,(\d+(\.\d+)?))?\)$/);
6540
+ if (rgbaMatch) {
6541
+ let r = parseInt(rgbaMatch[1], 10);
6542
+ let g = parseInt(rgbaMatch[2], 10);
6543
+ let b = parseInt(rgbaMatch[3], 10);
6544
+ let a = rgbaMatch[4] !== void 0 ? parseFloat(rgbaMatch[4]) : opacity;
6545
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
6657
6546
  }
6658
- getStyleFunction() {
6659
- const style2 = this.style;
6660
- if (typeof style2 === "function") return style2;
6661
- return () => {
6662
- return style2;
6663
- };
6547
+ throw new Error("Unsupported color format");
6548
+ }
6549
+ const StrokePosition = {
6550
+ CENTER: "center",
6551
+ INSIDE: "inside"
6552
+ };
6553
+ class Stroke {
6554
+ constructor(options) {
6555
+ this.color = (options == null ? void 0 : options.color) || "#121212";
6556
+ this.width = (options == null ? void 0 : options.width) || 0.5;
6557
+ this.opacity = (options == null ? void 0 : options.opacity) || 1;
6558
+ this.position = (options == null ? void 0 : options.position) || StrokePosition.CENTER;
6559
+ this._getRgba = memoise(toRgba);
6664
6560
  }
6665
- renderFrame(frameState, targetElement) {
6666
- return this.renderer.renderFrame(frameState, targetElement);
6561
+ getRgba() {
6562
+ return this._getRgba(this.color, this.opacity);
6667
6563
  }
6668
6564
  }
6669
- class VectorLayerRenderer {
6670
- constructor(layer) {
6671
- this.layer = layer;
6672
- this.featureRenderer = new FeatureRenderer();
6565
+ class Fill {
6566
+ constructor(options) {
6567
+ this.color = (options == null ? void 0 : options.color) || "#CCC";
6568
+ this.opacity = (options == null ? void 0 : options.opacity) || 1;
6569
+ this._getRgba = memoise(toRgba);
6673
6570
  }
6674
- renderFrame(frameState, targetElement) {
6675
- if (this.layer.opacity === 0) return targetElement;
6676
- const { projection, sizeInPixels, visibleExtent, transform } = frameState.viewState;
6677
- const container2 = this.getOrCreateContainer(targetElement, sizeInPixels);
6678
- const context = container2.firstElementChild.getContext("2d");
6679
- context.save();
6680
- context.translate(transform.x, transform.y);
6681
- context.scale(transform.k, transform.k);
6682
- context.lineJoin = "round";
6683
- context.lineCap = "round";
6684
- context.globalAlpha = this.layer.opacity;
6685
- const source = this.layer.source;
6686
- const features = source.getFeaturesInExtent(visibleExtent);
6687
- for (const feature of features) {
6688
- const styleFunction2 = feature.getStyleFunction() || this.layer.getStyleFunction();
6689
- const featureStyle = styleFunction2(feature);
6690
- if ((featureStyle == null ? void 0 : featureStyle.stroke) || (featureStyle == null ? void 0 : featureStyle.fill)) {
6691
- context.save();
6692
- this.featureRenderer.setStyle(featureStyle);
6693
- this.featureRenderer.render(frameState, feature, context);
6694
- context.restore();
6695
- }
6696
- }
6697
- if (Object.prototype.hasOwnProperty.call(projection, "getCompositionBorders")) {
6698
- context.beginPath();
6699
- context.lineWidth = 1 / transform.k;
6700
- context.strokeStyle = "#999";
6701
- projection.drawCompositionBorders(context);
6702
- context.stroke();
6571
+ getRgba() {
6572
+ return this._getRgba(this.color, this.opacity);
6573
+ }
6574
+ }
6575
+ const TextAnchor = {
6576
+ TOP: "top",
6577
+ BOTTOM: "bottom",
6578
+ LEFT: "left",
6579
+ RIGHT: "right",
6580
+ CENTER: "center",
6581
+ TOP_LEFT: "top-left",
6582
+ TOP_RIGHT: "top-right",
6583
+ BOTTOM_LEFT: "bottom-left",
6584
+ BOTTOM_RIGHT: "bottom-right"
6585
+ };
6586
+ class Text {
6587
+ /**
6588
+ * Create a text element style
6589
+ * @constructor
6590
+ * @param {TextStyle} options - Style options
6591
+ */
6592
+ constructor(options) {
6593
+ this.content = options == null ? void 0 : options.content;
6594
+ this.anchor = (options == null ? void 0 : options.anchor) || TextAnchor.CENTER;
6595
+ this.fontFamily = (options == null ? void 0 : options.fontFamily) || "var(--text-sans)";
6596
+ this.fontSize = (options == null ? void 0 : options.fontSize) || "17px";
6597
+ this.fontWeight = (options == null ? void 0 : options.fontWeight) || "400";
6598
+ this.lineHeight = (options == null ? void 0 : options.lineHeight) || 1.3;
6599
+ this.color = (options == null ? void 0 : options.color) || "#121212";
6600
+ this.textShadow = (options == null ? void 0 : options.textShadow) || "1px 1px 0px #f6f6f6, -1px -1px 0px #f6f6f6, -1px 1px 0px #f6f6f6, 1px -1px #f6f6f6";
6601
+ this.radialOffset = (options == null ? void 0 : options.radialOffset) || 0;
6602
+ }
6603
+ /**
6604
+ * Get the relative translation for the text element based on its anchor. The translation does not take `radialOffset` into account
6605
+ * @private
6606
+ * @return {{x: number, y: number}} - The x and y translation in percentage points
6607
+ */
6608
+ _getRelativeTranslation() {
6609
+ switch (this.anchor) {
6610
+ case TextAnchor.TOP:
6611
+ return { x: -50, y: 0 };
6612
+ case TextAnchor.BOTTOM:
6613
+ return { x: -50, y: -100 };
6614
+ case TextAnchor.LEFT:
6615
+ return { x: 0, y: -50 };
6616
+ case TextAnchor.RIGHT:
6617
+ return { x: -100, y: -50 };
6618
+ case TextAnchor.CENTER:
6619
+ return { x: -50, y: -50 };
6620
+ case TextAnchor.TOP_LEFT:
6621
+ return { x: 0, y: 0 };
6622
+ case TextAnchor.TOP_RIGHT:
6623
+ return { x: 100, y: 0 };
6624
+ case TextAnchor.BOTTOM_LEFT:
6625
+ return { x: 0, y: 100 };
6626
+ case TextAnchor.BOTTOM_RIGHT:
6627
+ return { x: 100, y: 100 };
6628
+ default:
6629
+ return { x: 0, y: 0 };
6703
6630
  }
6704
- context.restore();
6705
- return container2;
6706
- }
6707
- getOrCreateContainer(targetElement, sizeInPixels) {
6708
- let container2 = null;
6709
- let containerReused = false;
6710
- let canvas = targetElement && targetElement.firstElementChild;
6711
- if (canvas instanceof HTMLCanvasElement) {
6712
- container2 = targetElement;
6713
- containerReused = true;
6714
- } else if (this._container) {
6715
- container2 = this._container;
6716
- } else {
6717
- container2 = this.createContainer();
6718
- }
6719
- if (!containerReused) {
6720
- const canvas2 = container2.firstElementChild;
6721
- canvas2.width = sizeInPixels[0];
6722
- canvas2.height = sizeInPixels[1];
6723
- }
6724
- this._container = container2;
6725
- return container2;
6726
- }
6727
- createContainer() {
6728
- const container2 = document.createElement("div");
6729
- container2.className = "gv-map-layer";
6730
- let style2 = container2.style;
6731
- style2.position = "absolute";
6732
- style2.width = "100%";
6733
- style2.height = "100%";
6734
- const canvas = document.createElement("canvas");
6735
- style2 = canvas.style;
6736
- style2.position = "absolute";
6737
- style2.width = "100%";
6738
- style2.height = "100%";
6739
- container2.appendChild(canvas);
6740
- return container2;
6741
- }
6742
- }
6743
- class VectorLayer {
6744
- /** @param {VectorLayerComponentProps} props */
6745
- static Component({ features, style: style2, minZoom, opacity, hitDetectionEnabled }) {
6746
- const { registerLayer } = hooks.useContext(MapContext);
6747
- const layer = hooks.useMemo(
6748
- () => VectorLayer.with(features, {
6749
- style: style2,
6750
- minZoom,
6751
- opacity,
6752
- hitDetectionEnabled
6753
- }),
6754
- // eslint-disable-next-line react-hooks/exhaustive-deps
6755
- [features, minZoom, opacity, hitDetectionEnabled]
6756
- );
6757
- registerLayer(layer);
6758
- hooks.useEffect(() => {
6759
- layer.style = style2;
6760
- }, [style2]);
6761
- return null;
6762
6631
  }
6763
6632
  /**
6764
- * @param {import("../Feature").Feature[]} features
6765
- * @param {VectorLayerOptions} options
6633
+ * Get the translation for the text element in pixels
6634
+ * @param {number} elementWidth - The width of the element
6635
+ * @param {number} elementHeight - The height of the element
6636
+ * @return {{x: number, y: number}} - The x and y translation in pixels
6766
6637
  */
6767
- static with(features, options) {
6768
- const source = new VectorSource({ features });
6769
- return new VectorLayer({ source, ...options });
6638
+ getTranslation(elementWidth, elementHeight) {
6639
+ const translate = this._getRelativeTranslation();
6640
+ let x = translate.x / 100 * elementWidth;
6641
+ let y = translate.y / 100 * elementHeight;
6642
+ const radialOffsetInPixels = this.radialOffset * this.fontSize.replace("px", "");
6643
+ switch (this.anchor) {
6644
+ case TextAnchor.TOP:
6645
+ y += radialOffsetInPixels;
6646
+ break;
6647
+ case TextAnchor.BOTTOM:
6648
+ y -= radialOffsetInPixels;
6649
+ break;
6650
+ case TextAnchor.LEFT:
6651
+ x += radialOffsetInPixels;
6652
+ break;
6653
+ case TextAnchor.RIGHT:
6654
+ x -= radialOffsetInPixels;
6655
+ break;
6656
+ case TextAnchor.CENTER:
6657
+ break;
6658
+ case TextAnchor.TOP_LEFT:
6659
+ x += radialOffsetInPixels;
6660
+ y += radialOffsetInPixels;
6661
+ break;
6662
+ case TextAnchor.TOP_RIGHT:
6663
+ x -= radialOffsetInPixels;
6664
+ y += radialOffsetInPixels;
6665
+ break;
6666
+ case TextAnchor.BOTTOM_LEFT:
6667
+ x += radialOffsetInPixels;
6668
+ y -= radialOffsetInPixels;
6669
+ break;
6670
+ case TextAnchor.BOTTOM_RIGHT:
6671
+ x -= radialOffsetInPixels;
6672
+ y -= radialOffsetInPixels;
6673
+ break;
6674
+ }
6675
+ return { x, y };
6770
6676
  }
6771
6677
  /**
6772
- * @param {Object} params
6773
- * @param {VectorSource} params.source
6774
- * @param {Style | (() => Style)} [params.style=undefined]
6775
- * @param {number} [params.minZoom=0]
6776
- * @param {number} [params.opacity=1]
6777
- * @param {boolean} [params.hitDetectionEnabled=false]
6678
+ * Get the transform for the text element
6679
+ * @param {number} elementWidth - The width of the element
6680
+ * @param {number} elementHeight - The height of the element
6681
+ * @return {string} - The transform for the text element
6778
6682
  */
6779
- constructor({
6780
- source,
6781
- style: style2,
6782
- minZoom = 0,
6783
- opacity = 1,
6784
- hitDetectionEnabled = true
6785
- }) {
6786
- this.dispatcher = new Dispatcher(this);
6787
- this.renderer = new VectorLayerRenderer(this);
6788
- this.source = source;
6789
- this._style = style2;
6790
- this.minZoom = minZoom;
6791
- this.opacity = opacity;
6792
- this.hitDetectionEnabled = hitDetectionEnabled;
6793
- }
6794
- get source() {
6795
- return this._source;
6683
+ getTransform(elementWidth, elementHeight) {
6684
+ const { x, y } = this.getTranslation(elementWidth, elementHeight);
6685
+ return `translate(${x}px, ${y}px)`;
6796
6686
  }
6797
- set source(source) {
6798
- if (this._source && source !== this._source) {
6799
- this._source.tearDown();
6800
- }
6801
- this._source = source;
6802
- source.on(MapEvent.CHANGE, () => {
6803
- this._extent = null;
6804
- this.dispatcher.dispatch(MapEvent.CHANGE);
6805
- });
6687
+ }
6688
+ class FeatureRenderer {
6689
+ constructor() {
6690
+ this.drawingFunction = d3Geo.geoPath();
6806
6691
  }
6807
- setRawProjection(projection) {
6808
- this.projection = projection;
6692
+ setStyle(style2) {
6693
+ this.style = style2;
6809
6694
  }
6810
- tearDown() {
6811
- this.dispatcher = null;
6695
+ setFeature(feature) {
6696
+ this.feature = feature;
6812
6697
  }
6813
- get style() {
6814
- if (this._style) return this._style;
6815
- const defaultStyle = new Style({
6816
- stroke: new Stroke()
6817
- });
6818
- return defaultStyle;
6698
+ render(frameState, context) {
6699
+ if (!this.style) {
6700
+ return;
6701
+ }
6702
+ const feature = this.feature;
6703
+ const { projection, transform, pixelRatio } = frameState.viewState;
6704
+ const { stroke, fill } = this.style;
6705
+ const geometries = feature.getProjectedGeometries(projection);
6706
+ if (frameState.debug) {
6707
+ try {
6708
+ validateGeometries(geometries);
6709
+ } catch {
6710
+ console.error(
6711
+ `Invalid geometry. Feature skipped during rendering. Click here to inspect geometry: ${generateDebugUrl(feature)}
6712
+ `,
6713
+ feature
6714
+ );
6715
+ }
6716
+ }
6717
+ this.drawPath(geometries, context);
6718
+ if (fill) {
6719
+ context.fillStyle = fill.getRgba();
6720
+ context.fill();
6721
+ }
6722
+ if (stroke) {
6723
+ context.save();
6724
+ this.drawStroke(frameState, context, {
6725
+ style: stroke.getRgba(),
6726
+ width: stroke.width / transform.k * pixelRatio,
6727
+ position: stroke.position
6728
+ });
6729
+ context.restore();
6730
+ }
6819
6731
  }
6820
- set style(style2) {
6821
- this._style = style2;
6822
- this.dispatcher.dispatch(MapEvent.CHANGE);
6732
+ drawPath(geometries, context, clipPath = false) {
6733
+ this.drawingFunction.context(context);
6734
+ context.beginPath();
6735
+ for (const geometry of geometries) {
6736
+ this.drawingFunction(geometry);
6737
+ }
6738
+ if (clipPath) {
6739
+ context.clip();
6740
+ } else {
6741
+ context.closePath();
6742
+ }
6823
6743
  }
6824
- getStyleFunction() {
6825
- const style2 = this.style;
6826
- if (typeof style2 === "function") return style2;
6827
- return () => {
6828
- return style2;
6829
- };
6744
+ drawStroke(frameState, context, { style: style2, width: strokeWidth, position }) {
6745
+ const { projection } = frameState.viewState;
6746
+ context.lineWidth = strokeWidth;
6747
+ context.strokeStyle = style2;
6748
+ if (position === StrokePosition.INSIDE) {
6749
+ context.lineWidth = strokeWidth * 2;
6750
+ const geometries = this.feature.getProjectedGeometries(projection);
6751
+ this.drawPath(geometries, context, true);
6752
+ }
6753
+ context.stroke();
6830
6754
  }
6831
- getExtent() {
6832
- if (this._extent) return this._extent;
6833
- const features = this.source.getFeatures();
6834
- const extent = features.reduce((combinedExtent, feature) => {
6835
- const featureExtent = feature.getExtent();
6836
- if (!combinedExtent) return featureExtent;
6837
- return combineExtents(featureExtent, combinedExtent);
6755
+ createCanvas(width, height) {
6756
+ const canvas = document.createElement("canvas");
6757
+ canvas.width = width;
6758
+ canvas.height = height;
6759
+ return canvas;
6760
+ }
6761
+ getProjectedExtent(projection) {
6762
+ const geometries = this.feature.getProjectedGeometries(projection);
6763
+ const extent = geometries.reduce((combinedExtent, geometry) => {
6764
+ const bounds = this.drawingFunction.bounds(geometry);
6765
+ if (!combinedExtent) return bounds;
6766
+ return combineExtents(bounds, combinedExtent);
6838
6767
  }, null);
6839
- this._extent = extent;
6840
6768
  return extent;
6841
6769
  }
6842
- findFeatures(coordinate) {
6843
- if (!this.hitDetectionEnabled) return;
6844
- return this.source.getFeaturesAtCoordinate(coordinate);
6845
- }
6846
- renderFrame(frameState, targetElement) {
6847
- return this.renderer.renderFrame(frameState, targetElement);
6848
- }
6849
6770
  }
6850
- function interpolateFeatures(currentFeatures, newFeatures, { interpolate: interpolate2, separate, combine }) {
6851
- if (currentFeatures.length !== newFeatures.length) {
6852
- throw new Error(
6853
- "interpolateFeatures expects an equal number of features for start and end"
6854
- );
6771
+ class TextLayerRenderer {
6772
+ constructor(layer) {
6773
+ this.layer = layer;
6774
+ this.featureRenderer = new FeatureRenderer();
6775
+ this._element = document.createElement("div");
6776
+ this._element.className = "gv-text-layer";
6777
+ const style2 = this._element.style;
6778
+ style2.position = "absolute";
6779
+ style2.width = "100%";
6780
+ style2.height = "100%";
6781
+ style2.pointerEvents = "none";
6782
+ style2.overflow = "hidden";
6855
6783
  }
6856
- const featureInterpolators = [];
6857
- for (let i = 0; i < currentFeatures.length; i++) {
6858
- const geometryInterpolators = [];
6859
- const currentGeometries = currentFeatures[i].geometries;
6860
- const newGeometries = newFeatures[i].geometries;
6861
- if (newGeometries.length === currentGeometries.length) {
6862
- for (let e = 0; e < currentGeometries.length; e++) {
6863
- const currentGeometry = currentGeometries[e];
6864
- const newGeometry = newGeometries[e];
6865
- if (currentGeometry.type !== "Polygon" || newGeometry.type !== "Polygon") {
6866
- throw new Error("interpolateFeatures expects only Polygon geometry");
6867
- }
6868
- const shapeInterpolator = interpolate2(
6869
- currentGeometries[e].getOuterRing(),
6870
- newGeometries[e].getOuterRing(),
6871
- { string: false }
6784
+ renderFrame(frameState, targetElement) {
6785
+ if (this.layer.opacity === 0) return targetElement;
6786
+ const { declutterTree } = frameState;
6787
+ const { projection, viewPortSize, sizeInPixels, visibleExtent, transform } = frameState.viewState;
6788
+ this._element.style.opacity = this.layer.opacity;
6789
+ const source = this.layer.source;
6790
+ const features = source.getFeaturesInExtent(visibleExtent);
6791
+ const textElements = [];
6792
+ for (const feature of features) {
6793
+ const geometries = feature.getProjectedGeometries(projection);
6794
+ const point = geometries.find((d2) => d2.type === "Point");
6795
+ if (!point) {
6796
+ throw new Error(
6797
+ `Expected Point geometry for feature in TextLayer: ${feature}`
6872
6798
  );
6873
- geometryInterpolators.push({
6874
- type: "default",
6875
- interpolator: shapeInterpolator
6876
- });
6877
6799
  }
6878
- } else if (currentGeometries.length === 1 && newGeometries.length > 1) {
6879
- const separationInterpolator = separate(
6880
- currentGeometries[0].getOuterRing(),
6881
- newGeometries.map((geometry) => geometry.getOuterRing()),
6882
- { string: false, single: true }
6883
- );
6884
- geometryInterpolators.push({
6885
- type: "separate",
6886
- interpolator: separationInterpolator
6887
- });
6888
- } else if (currentGeometries.length > 1 && newGeometries.length === 1) {
6889
- const combinationInterpolator = combine(
6890
- currentGeometries.map((geometry) => geometry.getOuterRing()),
6891
- newGeometries[0].getOuterRing(),
6892
- { string: false, single: true }
6893
- );
6894
- geometryInterpolators.push({
6895
- type: "combine",
6896
- interpolator: combinationInterpolator
6800
+ const styleFunction2 = feature.getStyleFunction() || this.layer.getStyleFunction();
6801
+ const featureStyle = styleFunction2(feature);
6802
+ const textElement = this.getTextElementWithID(feature.uid);
6803
+ textElement.innerText = featureStyle.text.content;
6804
+ const [relativeX, relativeY] = transform.apply(point.coordinates).map((d2, i) => d2 / sizeInPixels[i]);
6805
+ const position = {
6806
+ left: `${relativeX * 100}%`,
6807
+ top: `${relativeY * 100}%`
6808
+ };
6809
+ this.styleTextElement(textElement, featureStyle.text, position);
6810
+ const bbox = this.getElementBBox(textElement, featureStyle.text, {
6811
+ x: relativeX * viewPortSize[0],
6812
+ y: relativeY * viewPortSize[1]
6897
6813
  });
6898
- } else {
6899
- throw new Error(
6900
- `Encountered an unexpected number of geometries: ${currentGeometries.length} and ${newGeometries.length}`
6901
- );
6814
+ if (declutterTree.collides(bbox)) {
6815
+ continue;
6816
+ }
6817
+ declutterTree.insert(bbox);
6818
+ if (this.layer.drawCollisionBoxes) {
6819
+ const collisionBoxDebugElement = this.getCollisionBoxElement(bbox);
6820
+ textElements.push(collisionBoxDebugElement);
6821
+ }
6822
+ textElements.push(textElement);
6902
6823
  }
6903
- featureInterpolators.push(geometryInterpolators);
6824
+ replaceChildren(this._element, textElements);
6825
+ return this._element;
6904
6826
  }
6905
- return (t) => {
6906
- if (t >= 1) {
6907
- return newFeatures;
6908
- }
6909
- const features = [];
6910
- for (let i = 0; i < featureInterpolators.length; i++) {
6911
- const feature = newFeatures[i].clone();
6912
- const geometries = [];
6913
- const geometryInterpolators = featureInterpolators[i];
6914
- for (const [
6915
- index,
6916
- { type, interpolator }
6917
- ] of geometryInterpolators.entries()) {
6918
- let geometry = feature.geometries[index].clone();
6919
- let interpolated;
6920
- switch (type) {
6921
- case "separate":
6922
- case "combine":
6923
- interpolated = interpolator(t);
6924
- interpolated.forEach((d2) => {
6925
- const polygon = geometry.clone();
6926
- polygon.setCoordinates([d2]);
6927
- geometries.push(polygon);
6928
- });
6929
- break;
6930
- default:
6931
- geometry.setOuterRing(interpolator(t));
6932
- geometries.push(geometry);
6933
- break;
6934
- }
6827
+ getTextElementWithID(id2) {
6828
+ const elementId = `text-feature-${id2}`;
6829
+ let textElement = this._element.querySelector(`#${elementId}`);
6830
+ if (!textElement) {
6831
+ textElement = document.createElement("div");
6832
+ textElement.id = elementId;
6833
+ }
6834
+ return textElement;
6835
+ }
6836
+ styleTextElement(element, textStyle, position) {
6837
+ const style2 = element.style;
6838
+ style2.position = "absolute";
6839
+ style2.left = position.left;
6840
+ style2.top = position.top;
6841
+ style2.textAlign = "center";
6842
+ style2.whiteSpace = "nowrap";
6843
+ style2.fontFamily = textStyle.fontFamily;
6844
+ style2.fontSize = textStyle.fontSize;
6845
+ style2.fontWeight = textStyle.fontWeight;
6846
+ style2.lineHeight = textStyle.lineHeight;
6847
+ style2.color = textStyle.color;
6848
+ style2.textShadow = textStyle.textShadow;
6849
+ const { width, height } = this.getElementSize(element);
6850
+ style2.transform = textStyle.getTransform(width, height);
6851
+ }
6852
+ getElementSize(element) {
6853
+ if (!element.parentElement) {
6854
+ document.body.appendChild(element);
6855
+ }
6856
+ const { width, height } = element.getBoundingClientRect();
6857
+ if (element.parentElement !== this._element) {
6858
+ element.remove();
6859
+ }
6860
+ return { width, height };
6861
+ }
6862
+ getElementBBox(element, textStyle, position) {
6863
+ const collissionPadding = {
6864
+ top: 2,
6865
+ right: 2,
6866
+ bottom: 2,
6867
+ left: 2
6868
+ };
6869
+ const { width, height } = this.getElementSize(element);
6870
+ const { x: translateX, y: translateY } = textStyle.getTranslation(
6871
+ width,
6872
+ height
6873
+ );
6874
+ const minX = Math.floor(position.x + translateX - collissionPadding.left);
6875
+ const minY = Math.floor(position.y + translateY - collissionPadding.top);
6876
+ return {
6877
+ minX,
6878
+ minY,
6879
+ maxX: Math.ceil(
6880
+ minX + width + collissionPadding.left + collissionPadding.right
6881
+ ),
6882
+ maxY: Math.ceil(
6883
+ minY + height + collissionPadding.top + collissionPadding.bottom
6884
+ )
6885
+ };
6886
+ }
6887
+ getCollisionBoxElement(bbox) {
6888
+ const element = document.createElement("div");
6889
+ const style2 = element.style;
6890
+ style2.position = "absolute";
6891
+ style2.left = `${bbox.minX}px`;
6892
+ style2.top = `${bbox.minY}px`;
6893
+ style2.width = `${bbox.maxX - bbox.minX}px`;
6894
+ style2.height = `${bbox.maxY - bbox.minY}px`;
6895
+ style2.border = "1px solid red";
6896
+ return element;
6897
+ }
6898
+ }
6899
+ class TinyQueue {
6900
+ constructor(data = [], compare = defaultCompare) {
6901
+ this.data = data;
6902
+ this.length = this.data.length;
6903
+ this.compare = compare;
6904
+ if (this.length > 0) {
6905
+ for (let i = (this.length >> 1) - 1; i >= 0; i--) this._down(i);
6906
+ }
6907
+ }
6908
+ push(item) {
6909
+ this.data.push(item);
6910
+ this.length++;
6911
+ this._up(this.length - 1);
6912
+ }
6913
+ pop() {
6914
+ if (this.length === 0) return void 0;
6915
+ const top = this.data[0];
6916
+ const bottom = this.data.pop();
6917
+ this.length--;
6918
+ if (this.length > 0) {
6919
+ this.data[0] = bottom;
6920
+ this._down(0);
6921
+ }
6922
+ return top;
6923
+ }
6924
+ peek() {
6925
+ return this.data[0];
6926
+ }
6927
+ _up(pos) {
6928
+ const { data, compare } = this;
6929
+ const item = data[pos];
6930
+ while (pos > 0) {
6931
+ const parent = pos - 1 >> 1;
6932
+ const current = data[parent];
6933
+ if (compare(item, current) >= 0) break;
6934
+ data[pos] = current;
6935
+ pos = parent;
6936
+ }
6937
+ data[pos] = item;
6938
+ }
6939
+ _down(pos) {
6940
+ const { data, compare } = this;
6941
+ const halfLength = this.length >> 1;
6942
+ const item = data[pos];
6943
+ while (pos < halfLength) {
6944
+ let left = (pos << 1) + 1;
6945
+ let best = data[left];
6946
+ const right = left + 1;
6947
+ if (right < this.length && compare(data[right], best) < 0) {
6948
+ left = right;
6949
+ best = data[right];
6935
6950
  }
6936
- feature.setGeometry(geometries);
6937
- features.push(feature);
6951
+ if (compare(best, item) >= 0) break;
6952
+ data[pos] = best;
6953
+ pos = left;
6938
6954
  }
6939
- return features;
6940
- };
6955
+ data[pos] = item;
6956
+ }
6941
6957
  }
6942
- function interpolateStyles(firstStyle, secondStyle, interpolateColors, interpolateNumbers) {
6943
- const fillInterpolator = interpolateFill(
6944
- firstStyle.fill,
6945
- secondStyle.fill,
6946
- interpolateColors,
6947
- interpolateNumbers
6948
- );
6949
- const strokeInterpolator = interpolateStroke(
6950
- firstStyle.stroke,
6951
- secondStyle.stroke,
6952
- interpolateColors,
6953
- interpolateNumbers
6954
- );
6955
- return (t) => {
6956
- return new Style({
6957
- fill: fillInterpolator(t),
6958
- stroke: strokeInterpolator(t)
6959
- });
6960
- };
6958
+ function defaultCompare(a, b) {
6959
+ return a < b ? -1 : a > b ? 1 : 0;
6961
6960
  }
6962
- function interpolateFill(fillA, fillB, interpolateColors, interpolateNumbers) {
6963
- const colorInterpolator = interpolateColors(
6964
- (fillA == null ? void 0 : fillA.color) ?? "transparent",
6965
- (fillB == null ? void 0 : fillB.color) ?? "transparent"
6966
- );
6967
- const opacityInterpolator = interpolateNumbers(
6968
- (fillA == null ? void 0 : fillA.opacity) ?? 1,
6969
- (fillB == null ? void 0 : fillB.opacity) ?? 1
6970
- );
6971
- return (t) => {
6972
- return new Fill({
6973
- color: colorInterpolator(t),
6974
- opacity: opacityInterpolator(t)
6975
- });
6976
- };
6961
+ function knn(tree, x, y, n2, predicate, maxDistance) {
6962
+ let node = tree.data;
6963
+ const result = [];
6964
+ const toBBox = tree.toBBox;
6965
+ const queue = new TinyQueue(void 0, compareDist);
6966
+ while (node) {
6967
+ for (let i = 0; i < node.children.length; i++) {
6968
+ const child = node.children[i];
6969
+ const dist = boxDist(x, y, node.leaf ? toBBox(child) : child);
6970
+ {
6971
+ queue.push({
6972
+ node: child,
6973
+ isItem: node.leaf,
6974
+ dist
6975
+ });
6976
+ }
6977
+ }
6978
+ while (queue.length && queue.peek().isItem) {
6979
+ const candidate = queue.pop().node;
6980
+ if (!predicate || predicate(candidate))
6981
+ result.push(candidate);
6982
+ if (result.length === n2) return result;
6983
+ }
6984
+ node = queue.pop();
6985
+ if (node) node = node.node;
6986
+ }
6987
+ return result;
6977
6988
  }
6978
- function interpolateStroke(strokeA, strokeB, interpolateColors, interpolateNumbers) {
6979
- const colorInterpolator = interpolateColors(
6980
- (strokeA == null ? void 0 : strokeA.color) ?? "transparent",
6981
- (strokeB == null ? void 0 : strokeB.color) ?? "transparent"
6982
- );
6983
- const opacityInterpolator = interpolateNumbers(
6984
- (strokeA == null ? void 0 : strokeA.opacity) ?? 1,
6985
- (strokeB == null ? void 0 : strokeB.opacity) ?? 1
6986
- );
6987
- const widthInterpolator = interpolateNumbers(
6988
- (strokeA == null ? void 0 : strokeA.width) ?? 1,
6989
- (strokeB == null ? void 0 : strokeB.width) ?? 1
6990
- );
6991
- return (t) => {
6992
- return new Stroke({
6993
- color: colorInterpolator(t),
6994
- opacity: opacityInterpolator(t),
6995
- width: widthInterpolator(t)
6989
+ function compareDist(a, b) {
6990
+ return a.dist - b.dist;
6991
+ }
6992
+ function boxDist(x, y, box) {
6993
+ const dx = axisDist(x, box.minX, box.maxX), dy = axisDist(y, box.minY, box.maxY);
6994
+ return dx * dx + dy * dy;
6995
+ }
6996
+ function axisDist(k, min, max) {
6997
+ return k < min ? min - k : k <= max ? 0 : k - max;
6998
+ }
6999
+ class VectorSource {
7000
+ constructor({ features }) {
7001
+ this.dispatcher = new Dispatcher(this);
7002
+ this._featuresRtree = new RBush();
7003
+ this.setFeatures(features);
7004
+ }
7005
+ tearDown() {
7006
+ this.dispatcher = null;
7007
+ }
7008
+ getFeatures() {
7009
+ return this._features;
7010
+ }
7011
+ getFeaturesAtCoordinate(coordinate) {
7012
+ const [lon, lat] = coordinate;
7013
+ const features = knn(
7014
+ this._featuresRtree,
7015
+ lon,
7016
+ lat,
7017
+ 10,
7018
+ (d2) => d2.feature.containsCoordinate(coordinate)
7019
+ ).map((d2) => {
7020
+ const midX = d2.minX + (d2.minX + d2.maxX) / 2;
7021
+ const midY = d2.minY + (d2.minY + d2.maxY) / 2;
7022
+ d2.distance = Math.hypot(midX - lon, midY - lat);
7023
+ return d2;
6996
7024
  });
6997
- };
7025
+ features.sort((a, b) => a.distance - b.distance);
7026
+ return features.map((d2) => d2.feature);
7027
+ }
7028
+ getFeaturesInExtent(extent) {
7029
+ const [minX, minY, maxX, maxY] = extent;
7030
+ return this._featuresRtree.search({ minX, minY, maxX, maxY }).map((d2) => d2.feature);
7031
+ }
7032
+ setFeatures(features) {
7033
+ this._featuresRtree.clear();
7034
+ for (const feature of features) {
7035
+ const [minX, minY, maxX, maxY] = feature.getExtent();
7036
+ this._featuresRtree.insert({
7037
+ minX: Math.floor(minX),
7038
+ minY: Math.floor(minY),
7039
+ maxX: Math.ceil(maxX),
7040
+ maxY: Math.ceil(maxY),
7041
+ feature
7042
+ });
7043
+ }
7044
+ this._features = features;
7045
+ this.dispatcher.dispatch(MapEvent.CHANGE);
7046
+ }
6998
7047
  }
6999
- class Feature {
7048
+ class TextLayer {
7049
+ /** @param {TextLayerComponentProps} props */
7050
+ static Component({
7051
+ features: featureCollection,
7052
+ style: style2,
7053
+ minZoom,
7054
+ opacity,
7055
+ declutter,
7056
+ drawCollisionBoxes
7057
+ }) {
7058
+ const { registerLayer } = hooks.useContext(MapContext);
7059
+ const layer = hooks.useMemo(
7060
+ () => {
7061
+ const features = featureCollection instanceof FeatureCollection ? featureCollection.features : (
7062
+ /** @type {import("../Feature").Feature[]} */
7063
+ featureCollection
7064
+ );
7065
+ return TextLayer.with(features, {
7066
+ style: style2,
7067
+ minZoom,
7068
+ opacity,
7069
+ declutter,
7070
+ drawCollisionBoxes
7071
+ });
7072
+ },
7073
+ // eslint-disable-next-line react-hooks/exhaustive-deps
7074
+ [featureCollection, minZoom, opacity, declutter, drawCollisionBoxes]
7075
+ );
7076
+ registerLayer(layer);
7077
+ hooks.useEffect(() => {
7078
+ layer.style = style2;
7079
+ }, [style2]);
7080
+ return null;
7081
+ }
7082
+ /**
7083
+ * @param {import("../Feature").Feature[]} features
7084
+ * @param {TextLayerOptions} options
7085
+ */
7086
+ static with(features, options) {
7087
+ const source = new VectorSource({ features });
7088
+ return new TextLayer({ source, ...options });
7089
+ }
7000
7090
  /**
7001
- * Represents a feature on the map
7002
7091
  * @constructor
7003
- * @param {Object} props - The properties for the feature.
7004
- * @property {string} id - The unique identifier of the feature
7005
- * @property {Array} geometries - The geometries of the feature
7006
- * @property {Object} properties - The properties of the feature
7007
- * @property {import("./styles").Style | import("./styles").StyleFunction} style - The style of the feature
7092
+ * @param {Object} params
7093
+ * @param {VectorSource} params.source
7094
+ * @param {Style | (() => Style)} [params.style=undefined]
7095
+ * @param {number} [params.minZoom=0]
7096
+ * @param {number} [params.opacity=1]
7097
+ * @param {boolean} [params.declutter=true]
7098
+ * @param {boolean} [params.drawCollisionBoxes=false]
7008
7099
  */
7009
- constructor({ id: id2, geometries, properties, style: style2 }) {
7010
- this.id = id2;
7011
- this.geometries = geometries;
7012
- this.properties = properties;
7013
- this.style = style2;
7014
- this.uid = createUid();
7100
+ constructor({
7101
+ source,
7102
+ style: style2,
7103
+ minZoom = 0,
7104
+ opacity = 1,
7105
+ declutter = true,
7106
+ drawCollisionBoxes = false
7107
+ }) {
7108
+ this.source = source;
7109
+ this._style = style2;
7110
+ this.minZoom = minZoom;
7111
+ this.opacity = opacity;
7112
+ this.declutter = declutter;
7113
+ this.drawCollisionBoxes = drawCollisionBoxes;
7114
+ this.renderer = new TextLayerRenderer(this);
7115
+ this.dispatcher = new Dispatcher(this);
7116
+ }
7117
+ tearDown() {
7118
+ this.dispatcher = null;
7119
+ }
7120
+ get style() {
7121
+ if (this._style) return this._style;
7122
+ const defaultStyle = new Style({
7123
+ text: new Text()
7124
+ });
7125
+ return defaultStyle;
7126
+ }
7127
+ set style(style2) {
7128
+ this._style = style2;
7129
+ this.dispatcher.dispatch(MapEvent.CHANGE);
7015
7130
  }
7016
7131
  getExtent() {
7017
7132
  if (this._extent) return this._extent;
7018
- const extent = this.geometries.reduce((combinedExtent, geometries) => {
7019
- if (!combinedExtent) return geometries.extent;
7020
- return combineExtents(geometries.extent, combinedExtent);
7133
+ const features = this.source.getFeatures();
7134
+ const extent = features.reduce((combinedExtent, feature) => {
7135
+ const featureExtent = feature.getExtent();
7136
+ if (!combinedExtent) return featureExtent;
7137
+ return combineExtents(featureExtent, combinedExtent);
7021
7138
  }, null);
7022
7139
  this._extent = extent;
7023
7140
  return extent;
7024
7141
  }
7025
- setgeometries(geometries) {
7026
- this.geometries = geometries;
7027
- this._extent = void 0;
7028
- }
7029
- getProjectedGeometries(projection) {
7030
- return this.geometries.map(
7031
- (d2) => d2.getProjected(projection, projection.revision)
7032
- );
7033
- }
7034
7142
  getStyleFunction() {
7035
7143
  const style2 = this.style;
7036
- if (!style2) return null;
7037
7144
  if (typeof style2 === "function") return style2;
7038
7145
  return () => {
7039
7146
  return style2;
7040
7147
  };
7041
7148
  }
7042
- containsCoordinate(coordinate) {
7043
- if (!containsCoordinate(this.getExtent(), coordinate)) {
7044
- return false;
7045
- }
7046
- for (const geometries of this.geometries) {
7047
- if (d3Geo.geoContains(geometries.getGeoJSON(), coordinate)) {
7048
- return true;
7149
+ renderFrame(frameState, targetElement) {
7150
+ return this.renderer.renderFrame(frameState, targetElement);
7151
+ }
7152
+ }
7153
+ class VectorLayerRenderer {
7154
+ constructor(layer) {
7155
+ this.layer = layer;
7156
+ this.featureRenderer = new FeatureRenderer();
7157
+ }
7158
+ renderFrame(frameState, targetElement) {
7159
+ if (this.layer.opacity === 0) return targetElement;
7160
+ const { projection, sizeInPixels, visibleExtent, transform } = frameState.viewState;
7161
+ const container2 = this.getOrCreateContainer(targetElement, sizeInPixels);
7162
+ const context = container2.firstElementChild.getContext("2d");
7163
+ context.save();
7164
+ context.translate(transform.x, transform.y);
7165
+ context.scale(transform.k, transform.k);
7166
+ context.lineJoin = "round";
7167
+ context.lineCap = "round";
7168
+ context.globalAlpha = this.layer.opacity;
7169
+ const source = this.layer.source;
7170
+ const features = source.getFeaturesInExtent(visibleExtent);
7171
+ for (const feature of features) {
7172
+ const styleFunction2 = feature.getStyleFunction() || this.layer.getStyleFunction();
7173
+ const featureStyle = styleFunction2(feature);
7174
+ if ((featureStyle == null ? void 0 : featureStyle.stroke) || (featureStyle == null ? void 0 : featureStyle.fill)) {
7175
+ context.save();
7176
+ this.featureRenderer.setStyle(featureStyle);
7177
+ this.featureRenderer.setFeature(feature);
7178
+ this.featureRenderer.render(frameState, context);
7179
+ context.restore();
7049
7180
  }
7050
7181
  }
7051
- return false;
7052
- }
7053
- clone() {
7054
- return new Feature({
7055
- id: this.id,
7056
- geometries: this.geometries.map((d2) => d2.clone()),
7057
- properties: this.properties,
7058
- style: this.style
7059
- });
7060
- }
7061
- /**
7062
- * Returns the geometries as a GeoJSON object
7063
- * @returns {Object} The GeoJSON representation of the geometries
7064
- */
7065
- getGeoJSON() {
7066
- const geometries = this.geometries.map((d2) => d2.getGeoJSON());
7067
- if (geometries.length === 1) return geometries[0];
7068
- return {
7069
- type: "Feature",
7070
- geometry: this._getGeometryGeoJSON(),
7071
- properties: this.properties
7072
- };
7182
+ if (Object.prototype.hasOwnProperty.call(projection, "getCompositionBorders")) {
7183
+ context.beginPath();
7184
+ context.lineWidth = 1 / transform.k;
7185
+ context.strokeStyle = "#999";
7186
+ projection.drawCompositionBorders(context);
7187
+ context.stroke();
7188
+ }
7189
+ context.restore();
7190
+ return container2;
7073
7191
  }
7074
- _getGeometryGeoJSON() {
7075
- const geometries = this.geometries.map((d2) => d2.getGeoJSON());
7076
- if (geometries.length === 0) throw new Error("Feature has no geometries");
7077
- if (geometries.length === 1) return geometries[0];
7078
- if (geometries[0].type === "Polygon") {
7079
- return {
7080
- type: "MultiPolygon",
7081
- coordinates: geometries.map((d2) => d2.coordinates)
7082
- };
7083
- } else if (geometries[0].type === "LineString") {
7084
- return {
7085
- type: "MultiLineString",
7086
- coordinates: geometries.map((d2) => d2.coordinates)
7087
- };
7088
- } else if (geometries[0].type === "Point") {
7089
- return {
7090
- type: "MultiPoint",
7091
- coordinates: geometries.map((d2) => d2.coordinates)
7092
- };
7192
+ getOrCreateContainer(targetElement, sizeInPixels) {
7193
+ let container2 = null;
7194
+ let containerReused = false;
7195
+ let canvas = targetElement && targetElement.firstElementChild;
7196
+ if (canvas instanceof HTMLCanvasElement) {
7197
+ container2 = targetElement;
7198
+ containerReused = true;
7199
+ } else if (this._container) {
7200
+ container2 = this._container;
7201
+ } else {
7202
+ container2 = this.createContainer();
7093
7203
  }
7094
- throw new Error("Could not determine geometry type");
7204
+ if (!containerReused) {
7205
+ const canvas2 = container2.firstElementChild;
7206
+ canvas2.width = sizeInPixels[0];
7207
+ canvas2.height = sizeInPixels[1];
7208
+ }
7209
+ this._container = container2;
7210
+ return container2;
7211
+ }
7212
+ createContainer() {
7213
+ const container2 = document.createElement("div");
7214
+ container2.className = "gv-map-layer";
7215
+ let style2 = container2.style;
7216
+ style2.position = "absolute";
7217
+ style2.width = "100%";
7218
+ style2.height = "100%";
7219
+ const canvas = document.createElement("canvas");
7220
+ style2 = canvas.style;
7221
+ style2.position = "absolute";
7222
+ style2.width = "100%";
7223
+ style2.height = "100%";
7224
+ container2.appendChild(canvas);
7225
+ return container2;
7095
7226
  }
7096
7227
  }
7097
- class Geometry {
7098
- /**
7099
- * Represents vector geometry
7100
- * @constructor
7101
- * @param {Object} options
7102
- * @param {string} options.type - The type of geometry (e.g., 'Point', 'LineString', 'Polygon')
7103
- * @param {Array} options.extent - The extent of the geometry (e.g., [xmin, ymin, xmax, ymax])
7104
- * @param {Array} options.coordinates - The coordinates of the geometry (e.g., [[x1, y1], [x2, y2], ...])
7105
- */
7106
- constructor({ type, extent, coordinates }) {
7107
- this.type = type;
7108
- this.extent = extent;
7109
- this.coordinates = coordinates;
7110
- this.getProjected = memoise(this._getProjected).bind(this);
7228
+ class VectorLayer {
7229
+ /** @param {VectorLayerComponentProps} props */
7230
+ static Component({
7231
+ features: featureCollection,
7232
+ style: style2,
7233
+ minZoom,
7234
+ opacity,
7235
+ hitDetectionEnabled = true
7236
+ }) {
7237
+ const { registerLayer } = hooks.useContext(MapContext);
7238
+ const layer = hooks.useMemo(
7239
+ () => {
7240
+ const features = featureCollection instanceof FeatureCollection ? featureCollection.features : (
7241
+ /** @type {import("../Feature").Feature[]} */
7242
+ featureCollection
7243
+ );
7244
+ return VectorLayer.with(features, {
7245
+ style: style2,
7246
+ minZoom,
7247
+ opacity,
7248
+ hitDetectionEnabled
7249
+ });
7250
+ },
7251
+ // eslint-disable-next-line react-hooks/exhaustive-deps
7252
+ [featureCollection, minZoom, opacity, hitDetectionEnabled]
7253
+ );
7254
+ registerLayer(layer);
7255
+ hooks.useEffect(() => {
7256
+ layer.style = style2;
7257
+ }, [style2]);
7258
+ return null;
7111
7259
  }
7112
7260
  /**
7113
- * Returns the geometry as a GeoJSON object
7114
- * @function
7115
- * @param {import("../projection").Projection} projection - The projection to use for the geometry
7116
- * @returns {Object} A GeoJSON representation of the projected geometry
7117
- * @private
7261
+ * @param {import("../Feature").Feature[]} features
7262
+ * @param {VectorLayerOptions} options
7118
7263
  */
7119
- // eslint-disable-next-line no-unused-vars
7120
- _getProjected(projection) {
7121
- throw new Error("Not implemented");
7264
+ static with(features, options) {
7265
+ const source = new VectorSource({ features });
7266
+ return new VectorLayer({ source, ...options });
7122
7267
  }
7123
7268
  /**
7124
- * Returns the geometry as a GeoJSON object
7125
- * @returns {Object} The GeoJSON representation of the geometry
7269
+ * @param {Object} params
7270
+ * @param {VectorSource} params.source
7271
+ * @param {Style | (() => Style)} [params.style=undefined]
7272
+ * @param {number} [params.minZoom=0]
7273
+ * @param {number} [params.opacity=1]
7274
+ * @param {boolean} [params.hitDetectionEnabled=true]
7126
7275
  */
7127
- getGeoJSON() {
7128
- return {
7129
- type: this.type,
7130
- coordinates: this.coordinates
7131
- };
7276
+ constructor({
7277
+ source,
7278
+ style: style2,
7279
+ minZoom = 0,
7280
+ opacity = 1,
7281
+ hitDetectionEnabled = true
7282
+ }) {
7283
+ this.dispatcher = new Dispatcher(this);
7284
+ this.renderer = new VectorLayerRenderer(this);
7285
+ this.source = source;
7286
+ this._style = style2;
7287
+ this.minZoom = minZoom;
7288
+ this.opacity = opacity;
7289
+ this.hitDetectionEnabled = hitDetectionEnabled;
7132
7290
  }
7133
- }
7134
- class LineString extends Geometry {
7135
- constructor({ type = "LineString", extent, coordinates }) {
7136
- super({ type, extent, coordinates });
7291
+ get source() {
7292
+ return this._source;
7137
7293
  }
7138
- _getProjected(projection) {
7139
- const projected = [];
7140
- for (const point of this.coordinates) {
7141
- projected.push(projection(point));
7294
+ set source(source) {
7295
+ if (this._source && source !== this._source) {
7296
+ this._source.tearDown();
7142
7297
  }
7143
- return {
7144
- type: this.type,
7145
- coordinates: projected
7146
- };
7298
+ this._source = source;
7299
+ source.on(MapEvent.CHANGE, () => {
7300
+ this._extent = null;
7301
+ this.dispatcher.dispatch(MapEvent.CHANGE);
7302
+ });
7147
7303
  }
7148
- }
7149
- class Polygon extends Geometry {
7150
- constructor({ type = "Polygon", extent, coordinates }) {
7151
- super({ type, extent, coordinates });
7304
+ setRawProjection(projection) {
7305
+ this.projection = projection;
7152
7306
  }
7153
- _getProjected(projection) {
7154
- const projected = [];
7155
- const rings = this.coordinates;
7156
- for (const ring of rings) {
7157
- const projectedRing = [];
7158
- for (const point of ring) {
7159
- const projectedPoint = projection(point);
7160
- if (projectedPoint) {
7161
- projectedRing.push(projectedPoint);
7162
- } else {
7163
- break;
7164
- }
7165
- }
7166
- if (projectedRing.length > 0) {
7167
- projected.push(projectedRing);
7168
- }
7169
- }
7170
- return {
7171
- type: this.type,
7172
- coordinates: projected
7307
+ tearDown() {
7308
+ this.dispatcher = null;
7309
+ }
7310
+ get style() {
7311
+ if (this._style) return this._style;
7312
+ const defaultStyle = new Style({
7313
+ stroke: new Stroke()
7314
+ });
7315
+ return defaultStyle;
7316
+ }
7317
+ set style(style2) {
7318
+ this._style = style2;
7319
+ this.dispatcher.dispatch(MapEvent.CHANGE);
7320
+ }
7321
+ getStyleFunction() {
7322
+ const style2 = this.style;
7323
+ if (typeof style2 === "function") return style2;
7324
+ return () => {
7325
+ return style2;
7173
7326
  };
7174
7327
  }
7175
- getOuterRing() {
7176
- return this.coordinates[0];
7177
- }
7178
- setOuterRing(coordinates) {
7179
- this.coordinates[0] = coordinates;
7328
+ getExtent() {
7329
+ if (this._extent) return this._extent;
7330
+ const features = this.source.getFeatures();
7331
+ const extent = features.reduce((combinedExtent, feature) => {
7332
+ const featureExtent = feature.getExtent();
7333
+ if (!combinedExtent) return featureExtent;
7334
+ return combineExtents(featureExtent, combinedExtent);
7335
+ }, null);
7336
+ this._extent = extent;
7337
+ return extent;
7180
7338
  }
7181
- setCoordinates(coordinates) {
7182
- this.coordinates = coordinates;
7339
+ findFeatures(coordinate) {
7340
+ if (!this.hitDetectionEnabled) return;
7341
+ return this.source.getFeaturesAtCoordinate(coordinate);
7183
7342
  }
7184
- clone() {
7185
- return new Polygon({
7186
- extent: this.extent,
7187
- coordinates: JSON.parse(JSON.stringify(this.coordinates))
7188
- });
7343
+ renderFrame(frameState, targetElement) {
7344
+ return this.renderer.renderFrame(frameState, targetElement);
7189
7345
  }
7190
7346
  }
7191
- class Point extends Geometry {
7192
- constructor({ type = "Point", coordinates }) {
7193
- super({ type, extent: null, coordinates });
7194
- this.extent = [...coordinates, ...coordinates];
7195
- }
7196
- _getProjected(projection) {
7197
- return {
7198
- type: this.type,
7199
- coordinates: projection(this.coordinates)
7200
- };
7347
+ function interpolateFeatures(currentFeatures, newFeatures, { interpolate: interpolate2, separate, combine }) {
7348
+ if (currentFeatures.length !== newFeatures.length) {
7349
+ throw new Error(
7350
+ "interpolateFeatures expects an equal number of features for start and end"
7351
+ );
7201
7352
  }
7202
- }
7203
- class GeoJSON {
7204
- readFeaturesFromObject(object) {
7205
- const geoJSONObject = object;
7206
- let features = null;
7207
- if (geoJSONObject["type"] === "FeatureCollection") {
7208
- const geoJSONFeatureCollection = object;
7209
- features = [];
7210
- const geoJSONFeatures = geoJSONFeatureCollection["features"];
7211
- for (let i = 0, ii = geoJSONFeatures.length; i < ii; ++i) {
7212
- const featureObject = this.readFeatureFromObject(geoJSONFeatures[i]);
7213
- if (!featureObject) {
7214
- continue;
7215
- }
7216
- features.push(featureObject);
7217
- }
7218
- } else if (geoJSONObject["type"] === "Feature") {
7219
- features = [this.readFeatureFromObject(geoJSONObject)];
7220
- } else if (Array.isArray(geoJSONObject)) {
7221
- features = [];
7222
- for (let i = 0, ii = geoJSONObject.length; i < ii; ++i) {
7223
- const featureObject = this.readFeatureFromObject(geoJSONObject[i]);
7224
- if (!featureObject) {
7225
- continue;
7353
+ const featureInterpolators = [];
7354
+ for (let i = 0; i < currentFeatures.length; i++) {
7355
+ const geometryInterpolators = [];
7356
+ const currentGeometries = currentFeatures[i].geometries;
7357
+ const newGeometries = newFeatures[i].geometries;
7358
+ if (newGeometries.length === currentGeometries.length) {
7359
+ for (let e = 0; e < currentGeometries.length; e++) {
7360
+ const currentGeometry = currentGeometries[e];
7361
+ const newGeometry = newGeometries[e];
7362
+ if (currentGeometry.type !== "Polygon" || newGeometry.type !== "Polygon") {
7363
+ throw new Error("interpolateFeatures expects only Polygon geometry");
7226
7364
  }
7227
- features.push(featureObject);
7365
+ const shapeInterpolator = interpolate2(
7366
+ currentGeometries[e].getOuterRing(),
7367
+ newGeometries[e].getOuterRing(),
7368
+ { string: false }
7369
+ );
7370
+ geometryInterpolators.push({
7371
+ type: "default",
7372
+ interpolator: shapeInterpolator
7373
+ });
7228
7374
  }
7375
+ } else if (currentGeometries.length === 1 && newGeometries.length > 1) {
7376
+ const separationInterpolator = separate(
7377
+ currentGeometries[0].getOuterRing(),
7378
+ newGeometries.map((geometry) => geometry.getOuterRing()),
7379
+ { string: false, single: true }
7380
+ );
7381
+ geometryInterpolators.push({
7382
+ type: "separate",
7383
+ interpolator: separationInterpolator
7384
+ });
7385
+ } else if (currentGeometries.length > 1 && newGeometries.length === 1) {
7386
+ const combinationInterpolator = combine(
7387
+ currentGeometries.map((geometry) => geometry.getOuterRing()),
7388
+ newGeometries[0].getOuterRing(),
7389
+ { string: false, single: true }
7390
+ );
7391
+ geometryInterpolators.push({
7392
+ type: "combine",
7393
+ interpolator: combinationInterpolator
7394
+ });
7229
7395
  } else {
7230
- try {
7231
- const geometries = this.readGeometriesFromObject(geoJSONObject);
7232
- const feature = new Feature({ geometries });
7233
- features = [feature];
7234
- } catch {
7235
- console.warn("Unable to interpret GeoJSON:", geoJSONObject);
7236
- return;
7237
- }
7396
+ throw new Error(
7397
+ `Encountered an unexpected number of geometries: ${currentGeometries.length} and ${newGeometries.length}`
7398
+ );
7238
7399
  }
7239
- return features.flat();
7400
+ featureInterpolators.push(geometryInterpolators);
7240
7401
  }
7241
- readFeatureFromObject(geoJSONObject) {
7242
- const geometries = this.readGeometriesFromObject(geoJSONObject["geometry"]);
7243
- if (geometries.length > 0) {
7244
- return new Feature({
7245
- id: geoJSONObject["id"],
7246
- geometries,
7247
- properties: geoJSONObject["properties"]
7248
- });
7402
+ return (t) => {
7403
+ if (t >= 1) {
7404
+ return newFeatures;
7249
7405
  }
7250
- return null;
7251
- }
7252
- readGeometriesFromObject(geometry) {
7253
- const geometries = [];
7254
- if (geometry.type === "Polygon") {
7255
- const polygon = this.readPolygonForCoordinates(geometry.coordinates);
7256
- geometries.push(polygon);
7257
- } else if (geometry.type === "MultiPolygon") {
7258
- for (const polygonCoordinates of geometry.coordinates) {
7259
- const polygon = this.readPolygonForCoordinates(polygonCoordinates);
7260
- geometries.push(polygon);
7261
- }
7262
- } else if (geometry.type === "LineString") {
7263
- const lineString = this.readLineStringForCoordinates(geometry.coordinates);
7264
- geometries.push(lineString);
7265
- } else if (geometry.type === "MultiLineString") {
7266
- for (const lineStringCoordinates of geometry.coordinates) {
7267
- const lineString = this.readLineStringForCoordinates(
7268
- lineStringCoordinates
7269
- );
7270
- geometries.push(lineString);
7406
+ const features = [];
7407
+ for (let i = 0; i < featureInterpolators.length; i++) {
7408
+ const feature = newFeatures[i].clone();
7409
+ const geometries = [];
7410
+ const geometryInterpolators = featureInterpolators[i];
7411
+ for (const [
7412
+ index,
7413
+ { type, interpolator }
7414
+ ] of geometryInterpolators.entries()) {
7415
+ let geometry = feature.geometries[index].clone();
7416
+ let interpolated;
7417
+ switch (type) {
7418
+ case "separate":
7419
+ case "combine":
7420
+ interpolated = interpolator(t);
7421
+ interpolated.forEach((d2) => {
7422
+ const polygon = geometry.clone();
7423
+ polygon.setCoordinates([d2]);
7424
+ geometries.push(polygon);
7425
+ });
7426
+ break;
7427
+ default:
7428
+ geometry.setOuterRing(interpolator(t));
7429
+ geometries.push(geometry);
7430
+ break;
7431
+ }
7271
7432
  }
7272
- } else if (geometry.type === "Point") {
7273
- const point = this.readPointForCoordinates(geometry.coordinates);
7274
- geometries.push(point);
7433
+ feature.setGeometry(geometries);
7434
+ features.push(feature);
7275
7435
  }
7276
- return geometries;
7277
- }
7278
- readPolygonForCoordinates(coordinates) {
7279
- const outerRing = coordinates[0];
7280
- const extent = extentForCoordinates(outerRing);
7281
- return new Polygon({ extent, coordinates });
7282
- }
7283
- readLineStringForCoordinates(coordinates) {
7284
- const extent = extentForCoordinates(coordinates);
7285
- return new LineString({ extent, coordinates });
7286
- }
7287
- readPointForCoordinates(coordinates) {
7288
- return new Point({ coordinates });
7289
- }
7436
+ return features;
7437
+ };
7438
+ }
7439
+ function interpolateStyles(firstStyle, secondStyle, interpolateColors, interpolateNumbers) {
7440
+ const fillInterpolator = interpolateFill(
7441
+ firstStyle.fill,
7442
+ secondStyle.fill,
7443
+ interpolateColors,
7444
+ interpolateNumbers
7445
+ );
7446
+ const strokeInterpolator = interpolateStroke(
7447
+ firstStyle.stroke,
7448
+ secondStyle.stroke,
7449
+ interpolateColors,
7450
+ interpolateNumbers
7451
+ );
7452
+ return (t) => {
7453
+ return new Style({
7454
+ fill: fillInterpolator(t),
7455
+ stroke: strokeInterpolator(t)
7456
+ });
7457
+ };
7458
+ }
7459
+ function interpolateFill(fillA, fillB, interpolateColors, interpolateNumbers) {
7460
+ const colorInterpolator = interpolateColors(
7461
+ (fillA == null ? void 0 : fillA.color) ?? "transparent",
7462
+ (fillB == null ? void 0 : fillB.color) ?? "transparent"
7463
+ );
7464
+ const opacityInterpolator = interpolateNumbers(
7465
+ (fillA == null ? void 0 : fillA.opacity) ?? 1,
7466
+ (fillB == null ? void 0 : fillB.opacity) ?? 1
7467
+ );
7468
+ return (t) => {
7469
+ return new Fill({
7470
+ color: colorInterpolator(t),
7471
+ opacity: opacityInterpolator(t)
7472
+ });
7473
+ };
7474
+ }
7475
+ function interpolateStroke(strokeA, strokeB, interpolateColors, interpolateNumbers) {
7476
+ const colorInterpolator = interpolateColors(
7477
+ (strokeA == null ? void 0 : strokeA.color) ?? "transparent",
7478
+ (strokeB == null ? void 0 : strokeB.color) ?? "transparent"
7479
+ );
7480
+ const opacityInterpolator = interpolateNumbers(
7481
+ (strokeA == null ? void 0 : strokeA.opacity) ?? 1,
7482
+ (strokeB == null ? void 0 : strokeB.opacity) ?? 1
7483
+ );
7484
+ const widthInterpolator = interpolateNumbers(
7485
+ (strokeA == null ? void 0 : strokeA.width) ?? 1,
7486
+ (strokeB == null ? void 0 : strokeB.width) ?? 1
7487
+ );
7488
+ return (t) => {
7489
+ return new Stroke({
7490
+ color: colorInterpolator(t),
7491
+ opacity: opacityInterpolator(t),
7492
+ width: widthInterpolator(t)
7493
+ });
7494
+ };
7290
7495
  }
7291
7496
  function useWindowSize() {
7292
7497
  const [windowSize, setWindowSize] = hooks.useState(() => {
@@ -7725,6 +7930,8 @@
7725
7930
  exports2.ControlChange = ControlChange;
7726
7931
  exports2.Dispatcher = Dispatcher;
7727
7932
  exports2.Dropdown = Dropdown;
7933
+ exports2.Feature = Feature;
7934
+ exports2.FeatureCollection = FeatureCollection;
7728
7935
  exports2.Fill = Fill;
7729
7936
  exports2.FirstPastThePostWaffle = FirstPastThePostWaffle;
7730
7937
  exports2.GeoJSON = GeoJSON;
@@ -7755,6 +7962,7 @@
7755
7962
  exports2.StackedBar = StackedBar;
7756
7963
  exports2.StackedGrid = StackedGrid;
7757
7964
  exports2.Stroke = Stroke;
7965
+ exports2.StrokePosition = StrokePosition;
7758
7966
  exports2.Style = Style;
7759
7967
  exports2.Table = Table;
7760
7968
  exports2.Text = Text;