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