@guardian/interactive-component-library 0.1.0-alpha.59 → 0.1.0

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