bruce-cesium 7.4.1 → 7.4.3

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.
@@ -1,6 +1,6 @@
1
1
  import * as Cesium from 'cesium';
2
- import { Cartographic, ColorMaterialProperty, Entity, Color, ConstantProperty, CallbackProperty, Primitive, Cesium3DTileFeature, HeightReference, DistanceDisplayCondition, ShaderSource, Cartesian3, Math as Math$1, Ellipsoid, Transforms, Matrix4, Geometry, GeometryAttribute, ComponentDatatype, PrimitiveType, BoundingSphere, GeometryPipeline, Cartesian2, Cartesian4, sampleTerrain, Texture, PixelFormat, Sampler, TextureWrap, TextureMinificationFilter, TextureMagnificationFilter, VertexArray, BufferUsage, ShaderProgram, RenderState, BlendingState, DrawCommand, Pass, JulianDate, Quaternion, HeadingPitchRoll, ColorBlendMode, ShadowMode, ClassificationType, Model, HorizontalOrigin, VerticalOrigin, ConstantPositionProperty, ArcType, CornerType, ImageMaterialProperty, SceneTransforms, PolygonHierarchy, PolylineGraphics, Rectangle, FeatureDetection, NearFarScalar, Matrix3, KmlDataSource, GeoJsonDataSource, SceneMode, HeadingPitchRange, Cesium3DTileStyle, Ion, IonResource, Cesium3DTileset, EllipsoidTerrainProvider, IonImageryProvider, createWorldImagery, createWorldImageryAsync, BingMapsImageryProvider, BingMapsStyle, MapboxImageryProvider, MapboxStyleImageryProvider, ArcGisMapServerImageryProvider, OpenStreetMapImageryProvider, UrlTemplateImageryProvider, GridImageryProvider, GeographicTilingScheme, ImageryLayer, TileMapServiceImageryProvider, CesiumTerrainProvider, Intersect, OrthographicFrustum, EasingFunction, Cesium3DTileColorBlendMode, ModelGraphics, PolygonGraphics, CorridorGraphics, PointGraphics, BillboardGraphics, EllipseGraphics, PolylineDashMaterialProperty, PolygonPipeline, EllipsoidGeodesic, sampleTerrainMostDetailed, defined, CustomDataSource, GeometryInstance, CesiumInspector, ClockRange, DynamicEnvironmentMapManager, ScreenSpaceEventHandler, ScreenSpaceEventType, Fullscreen, CzmlDataSource } from 'cesium';
3
- import { Cartes, LRUCache, ProjectViewTile, ENVIRONMENT, Calculator, ClientFile, Entity as Entity$1, EntityLod, EntityTag, EntityType, ObjectUtils, Style, Api, Carto, Geometry as Geometry$1, ClientFileValueMap, Color as Color$1, MenuItem, ProjectView, ProjectViewBookmark, Tileset, ZoomControl, Bounds, BruceEvent, EntityCoords, DataLab, DelayQueue, AccountConcept, BruceApi, EntityRelation, RecordChangeFeed, EntitySource, ProgramKey, ProjectViewLegacyTile, Camera, ProjectViewBookmarkGroup, Session, Account, AccountLimits, UrlUtils, EntityAttachment, EntityAttachmentType, EntityAttribute, MathUtils, EntityRelationType, EntityHistoricData } from 'bruce-models';
2
+ import { Cartographic, ColorMaterialProperty, Entity, Color, ConstantProperty, CallbackProperty, Primitive, Cesium3DTileFeature, ConstantPositionProperty, HeightReference, DistanceDisplayCondition, ColorBlendMode, HeadingPitchRoll, Math as Math$1, Transforms, ShadowMode, Cartesian3, ClassificationType, Model, JulianDate, Quaternion, Matrix4, ShaderSource, Ellipsoid, Geometry, GeometryAttribute, ComponentDatatype, PrimitiveType, BoundingSphere, GeometryPipeline, Cartesian2, Cartesian4, sampleTerrain, Texture, PixelFormat, Sampler, TextureWrap, TextureMinificationFilter, TextureMagnificationFilter, VertexArray, BufferUsage, ShaderProgram, RenderState, BlendingState, DrawCommand, Pass, ArcType, CornerType, HorizontalOrigin, VerticalOrigin, ImageMaterialProperty, PolygonHierarchy, PolylineGraphics, Rectangle, SceneTransforms, FeatureDetection, NearFarScalar, Matrix3, KmlDataSource, SceneMode, GeoJsonDataSource, Cesium3DTileStyle, HeadingPitchRange, Ion, IonResource, Cesium3DTileColorBlendMode, Cesium3DTileset, EllipsoidTerrainProvider, IonImageryProvider, createWorldImagery, createWorldImageryAsync, BingMapsImageryProvider, BingMapsStyle, MapboxImageryProvider, MapboxStyleImageryProvider, ArcGisMapServerImageryProvider, OpenStreetMapImageryProvider, UrlTemplateImageryProvider, GridImageryProvider, GeographicTilingScheme, ImageryLayer, TileMapServiceImageryProvider, CesiumTerrainProvider, OrthographicFrustum, EasingFunction, Intersect, ModelGraphics, PolygonGraphics, CorridorGraphics, PointGraphics, BillboardGraphics, EllipseGraphics, PolylineDashMaterialProperty, PolygonPipeline, EllipsoidGeodesic, sampleTerrainMostDetailed, defined, CesiumInspector, ClockRange, DynamicEnvironmentMapManager, GeometryInstance, CustomDataSource, ScreenSpaceEventHandler, ScreenSpaceEventType, Fullscreen, CzmlDataSource } from 'cesium';
3
+ import { Cartes, LRUCache, ProjectViewTile, ENVIRONMENT, Calculator, ClientFile, Entity as Entity$1, EntityLod, EntityTag, EntityType, ObjectUtils, Style, Carto, Geometry as Geometry$1, Api, Bounds, ClientFileValueMap, Color as Color$1, MenuItem, ProjectView, ProjectViewBookmark, Tileset, ZoomControl, DataLab, BruceEvent, EntityCoords, AccountConcept, DelayQueue, BruceApi, EntityRelation, RecordChangeFeed, EntitySource, ProgramKey, ProjectViewLegacyTile, Camera, Account, AccountLimits, Session, UrlUtils, ProjectViewBookmarkGroup, EntityAttachment, EntityAttachmentType, EntityAttribute, MathUtils, EntityRelationType, EntityHistoricData } from 'bruce-models';
4
4
 
5
5
  /**
6
6
  * Ensures a number is returned from a given value.
@@ -5399,7 +5399,14 @@ var EntityLabel;
5399
5399
  (function (EntityLabel) {
5400
5400
  const ICON_BACKGROUND_BLEND = 0.8;
5401
5401
  const ICON_BACKGROUND_ALPHA = 0.45;
5402
+ // Under this much difference between the glyph's width and height the chip is squared off,
5403
+ // so a row of labels reads as one size rather than a set of differently shaped pills.
5404
+ const ICON_BACKGROUND_UNIFORM_TOLERANCE = 0.3;
5402
5405
  const TEXT_MAX_WIDTH = 200;
5406
+ // How far an angled leader runs sideways before the label starts.
5407
+ const ANGLED_LEADER_RUN = 28;
5408
+ // One per tinted icon, since an SVG mask is addressed by id across the whole document.
5409
+ let tintCounter = 0;
5403
5410
  // Key where the label instance is stored within the cesium entity.
5404
5411
  // Eg: rego[ATTACH_KEY] = new EntityLabel(...);
5405
5412
  EntityLabel.ATTACH_KEY = "nextspace-navigator-entity-label";
@@ -5498,7 +5505,7 @@ var EntityLabel;
5498
5505
  this.nextOrder = 0;
5499
5506
  }
5500
5507
  Resolve(frames) {
5501
- var _a, _b;
5508
+ var _a, _b, _c;
5502
5509
  const live = new Set(frames.map(frame => frame.id));
5503
5510
  const moved = (frame) => {
5504
5511
  var _a;
@@ -5525,10 +5532,17 @@ var EntityLabel;
5525
5532
  const placed = new Map();
5526
5533
  const next = new Map();
5527
5534
  const candidate = (frame, side) => {
5528
- var _a;
5529
- return (side === "left" && frame.canFlip ?
5530
- { ...frame, x: Math.max(0, Math.min(frame.anchorX - frame.width, ((_a = frame.viewportWidth) !== null && _a !== void 0 ? _a : Infinity) - frame.width)) } :
5531
- frame);
5535
+ var _a, _b;
5536
+ if (!frame.canFlip) {
5537
+ return frame;
5538
+ }
5539
+ if (frame.leaderRun != null) {
5540
+ const x = side === "left" ? frame.anchorX - frame.leaderRun - frame.width : frame.anchorX + frame.leaderRun;
5541
+ return { ...frame, x: Math.max(0, Math.min(x, ((_a = frame.viewportWidth) !== null && _a !== void 0 ? _a : Infinity) - frame.width)) };
5542
+ }
5543
+ return (side === "left" ?
5544
+ { ...frame, x: Math.max(0, Math.min(frame.anchorX - frame.width, ((_b = frame.viewportWidth) !== null && _b !== void 0 ? _b : Infinity) - frame.width)) }
5545
+ : frame);
5532
5546
  };
5533
5547
  const score = (frame, padding = 0) => {
5534
5548
  const box = {
@@ -5544,7 +5558,7 @@ var EntityLabel;
5544
5558
  };
5545
5559
  for (const frame of ordered) {
5546
5560
  const old = this.previous.get(frame.id);
5547
- let side = frame.canFlip ? (_a = old === null || old === void 0 ? void 0 : old.side) !== null && _a !== void 0 ? _a : "right" : "right";
5561
+ let side = frame.canFlip ? (_b = (_a = old === null || old === void 0 ? void 0 : old.side) !== null && _a !== void 0 ? _a : frame.leaderSide) !== null && _b !== void 0 ? _b : "right" : "right";
5548
5562
  let chosen = candidate(frame, side);
5549
5563
  if (!fixed(frame) && frame.canFlip) {
5550
5564
  const otherSide = side === "left" ? "right" : "left";
@@ -5556,7 +5570,7 @@ var EntityLabel;
5556
5570
  }
5557
5571
  }
5558
5572
  placed.set(frame.id, { frame: chosen, side });
5559
- next.set(frame.id, { frame: fixed(frame) ? old.frame : { ...frame }, side, order: (_b = old === null || old === void 0 ? void 0 : old.order) !== null && _b !== void 0 ? _b : this.nextOrder++ });
5573
+ next.set(frame.id, { frame: fixed(frame) ? old.frame : { ...frame }, side, order: (_c = old === null || old === void 0 ? void 0 : old.order) !== null && _c !== void 0 ? _c : this.nextOrder++ });
5560
5574
  text.Add(chosen);
5561
5575
  const stem = leaderBounds(chosen);
5562
5576
  if (stem) {
@@ -5576,7 +5590,7 @@ var EntityLabel;
5576
5590
  * Resolves clustering and stable text placement before depth fading; highlighted labels receive priority.
5577
5591
  */
5578
5592
  function Layout(input, memory = new PlacementMemory()) {
5579
- var _a, _b, _c, _d, _e, _f, _g, _h;
5593
+ var _a, _b, _c, _d, _e, _f, _g;
5580
5594
  const frames = input.filter(frame => [frame.x, frame.y, frame.width, frame.height, frame.depth].every(Number.isFinite))
5581
5595
  .sort((a, b) => (b.priority || 0) - (a.priority || 0) || a.depth - b.depth || a.id.localeCompare(b.id));
5582
5596
  const result = new Map();
@@ -5642,17 +5656,13 @@ var EntityLabel;
5642
5656
  }
5643
5657
  }
5644
5658
  if (values.size > 1) {
5645
- const description = stableMembers.find(member => { var _a, _b; return member.textWrapped && ((_b = (member.text || ((_a = member.options) === null || _a === void 0 ? void 0 : _a.text))) === null || _b === void 0 ? void 0 : _b.trim()); });
5646
- if (description) {
5647
- representative.text = ((_f = description.text) !== null && _f !== void 0 ? _f : description.options.text).trim();
5648
- representative.summaryMore = true;
5649
- }
5650
- else {
5651
- const rows = Array.from(values).slice(0, 2);
5652
- if (values.size > 2)
5653
- rows.push(`+${values.size - 2} more…`);
5654
- representative.text = rows.join("\n");
5655
- }
5659
+ // A value long enough to wrap fills the box on its own, so only one is listed.
5660
+ const wrapped = stableMembers.some(member => { var _a, _b; return member.textWrapped && ((_b = (member.text || ((_a = member.options) === null || _a === void 0 ? void 0 : _a.text))) === null || _b === void 0 ? void 0 : _b.trim()); });
5661
+ const limit = wrapped ? 1 : 2;
5662
+ const rows = Array.from(values).slice(0, limit);
5663
+ if (values.size > limit)
5664
+ rows.push(`+${values.size - limit} more…`);
5665
+ representative.text = rows.join("\n");
5656
5666
  }
5657
5667
  }
5658
5668
  for (const frame of cluster.members) {
@@ -5676,7 +5686,7 @@ var EntityLabel;
5676
5686
  const box = { ...frame, x: frame.x + .01, y: frame.y + .01, width: Math.max(0, frame.width - .02), height: Math.max(0, frame.height - .02) };
5677
5687
  const overlap = foreground.HasOverlap(box) || foregroundLeaders.HasOverlap(box) || Boolean(stem && foreground.HasOverlap(stem));
5678
5688
  if (overlap) {
5679
- placement.opacity *= Math.max(0, Math.min(1, (_h = (_g = frame.options) === null || _g === void 0 ? void 0 : _g.overlapOpacity) !== null && _h !== void 0 ? _h : 0.25));
5689
+ placement.opacity *= Math.max(0, Math.min(1, (_g = (_f = frame.options) === null || _f === void 0 ? void 0 : _f.overlapOpacity) !== null && _g !== void 0 ? _g : 0.25));
5680
5690
  }
5681
5691
  placement.zIndex = zIndex--;
5682
5692
  foreground.Add(frame);
@@ -5998,6 +6008,8 @@ var EntityLabel;
5998
6008
  this.loadTimeout = null;
5999
6009
  this.loadedName = false;
6000
6010
  this.line = null;
6011
+ // The sideways half of an angled leader; the rise is drawn by line.
6012
+ this.leaderRun = null;
6001
6013
  this.anchor = null;
6002
6014
  // Null = not attempted to load.
6003
6015
  this.additionalTexts = null;
@@ -6394,6 +6406,7 @@ var EntityLabel;
6394
6406
  }
6395
6407
  };
6396
6408
  const normalLeader = (_k = this.options.leader) !== null && _k !== void 0 ? _k : presentation === "text";
6409
+ const angled = this.options.leaderStyle === "angled";
6397
6410
  let leader = normalLeader;
6398
6411
  const content = document.createElement("span");
6399
6412
  const originalText = (_l = this.options.text) !== null && _l !== void 0 ? _l : [text, ...additionalTexts].join("\n");
@@ -6402,6 +6415,7 @@ var EntityLabel;
6402
6415
  let sourceNaturalText;
6403
6416
  let iconElement;
6404
6417
  let iconContainer;
6418
+ let fitIconBackground;
6405
6419
  content.className = "entity-label-content";
6406
6420
  content.style.cssText = "display:flex;align-items:center;gap:6px;";
6407
6421
  if (presentation === "icon-text-above") {
@@ -6424,34 +6438,49 @@ var EntityLabel;
6424
6438
  image.alt = this.options.icon || "";
6425
6439
  const scale = (_o = this.options.iconScale) !== null && _o !== void 0 ? _o : 1;
6426
6440
  const size = this.options.iconSize;
6427
- const mask = iconColor && iconBackground ? document.createElement("span") : icon;
6428
- if (mask !== icon) {
6429
- icon.appendChild(mask);
6430
- }
6431
- const target = iconColor ? mask : image;
6432
- if (iconColor) {
6433
- mask.style.display = "inline-block";
6434
- mask.style.backgroundColor = iconColor;
6435
- for (const prefix of ["", "-webkit-"]) {
6436
- mask.style.setProperty(`${prefix}mask-image`, `url(${JSON.stringify(this.options.iconUrl)})`);
6437
- mask.style.setProperty(`${prefix}mask-size`, "contain");
6438
- mask.style.setProperty(`${prefix}mask-repeat`, "no-repeat");
6439
- mask.style.setProperty(`${prefix}mask-position`, "center");
6440
- }
6441
+ const svgNs = "http://www.w3.org/2000/svg";
6442
+ const tint = iconColor ? document.createElementNS(svgNs, "svg") : null;
6443
+ const painted = tint ? document.createElementNS(svgNs, "image") : null;
6444
+ const inked = tint ? document.createElementNS(svgNs, "rect") : null;
6445
+ if (tint) {
6446
+ const maskId = `entity-label-tint-${++tintCounter}`;
6447
+ const mask = document.createElementNS(svgNs, "mask");
6448
+ mask.setAttribute("id", maskId);
6449
+ mask.setAttribute("maskUnits", "userSpaceOnUse");
6450
+ painted.setAttribute("href", this.options.iconUrl);
6451
+ painted.setAttribute("preserveAspectRatio", "xMidYMid meet");
6452
+ mask.appendChild(painted);
6453
+ inked.setAttribute("fill", iconColor);
6454
+ inked.setAttribute("mask", `url(#${maskId})`);
6455
+ tint.append(mask, inked);
6456
+ tint.style.display = "block";
6457
+ icon.style.display = "inline-flex";
6458
+ icon.appendChild(tint);
6441
6459
  }
6442
6460
  const resize = () => {
6443
6461
  if (this.disposed || this.counter !== counter) {
6444
6462
  return;
6445
6463
  }
6446
6464
  const intrinsic = this.options.iconScale != null && size == null;
6447
- target.style.width = `${(intrinsic ? image.naturalWidth || 24 : size || 24) * scale}px`;
6448
- target.style.height = `${(intrinsic ? image.naturalHeight || 24 : size || 24) * scale}px`;
6465
+ const width = (intrinsic ? image.naturalWidth || 24 : size || 24) * scale;
6466
+ const height = (intrinsic ? image.naturalHeight || 24 : size || 24) * scale;
6467
+ if (tint) {
6468
+ tint.setAttribute("viewBox", `0 0 ${width} ${height}`);
6469
+ for (const node of [tint, painted, inked]) {
6470
+ node.setAttribute("width", String(width));
6471
+ node.setAttribute("height", String(height));
6472
+ }
6473
+ }
6474
+ else {
6475
+ image.style.width = `${width}px`;
6476
+ image.style.height = `${height}px`;
6477
+ }
6449
6478
  this.viewer.scene.requestRender();
6450
6479
  };
6451
6480
  image.onload = resize;
6452
6481
  resize();
6453
6482
  image.src = this.options.iconUrl;
6454
- if (!iconColor) {
6483
+ if (!tint) {
6455
6484
  icon.appendChild(image);
6456
6485
  }
6457
6486
  }
@@ -6476,6 +6505,27 @@ var EntityLabel;
6476
6505
  icon.style.minWidth = `${size}px`;
6477
6506
  icon.style.minHeight = `${size}px`;
6478
6507
  icon.style.boxSizing = "content-box";
6508
+ let fitted = false;
6509
+ fitIconBackground = () => {
6510
+ if (fitted) {
6511
+ return;
6512
+ }
6513
+ const padding = size / 3;
6514
+ const width = icon.offsetWidth - padding * 2;
6515
+ const height = icon.offsetHeight - padding * 2;
6516
+ if (width <= 0 || height <= 0) {
6517
+ return;
6518
+ }
6519
+ fitted = true;
6520
+ const longest = Math.max(width, height);
6521
+ const difference = (longest - Math.min(width, height)) / longest;
6522
+ if (difference > ICON_BACKGROUND_UNIFORM_TOLERANCE) {
6523
+ return;
6524
+ }
6525
+ icon.style.minWidth = `${longest}px`;
6526
+ icon.style.minHeight = `${longest}px`;
6527
+ this.viewer.scene.requestRender();
6528
+ };
6479
6529
  }
6480
6530
  if (this.options.iconScale === 0) {
6481
6531
  icon.style.display = "none";
@@ -6514,8 +6564,10 @@ var EntityLabel;
6514
6564
  const countElement = document.createElement("span");
6515
6565
  countElement.className = "entity-label-count";
6516
6566
  countElement.style.cssText = "flex:none;align-self:center;white-space:nowrap;background:#fff;color:#000;text-shadow:none;border-radius:8px;padding:0 3px;display:none;";
6567
+ // Text above the icon would have the badge land in it, so the free corner is the lower one.
6568
+ const badgeBelow = presentation === "icon-text-above";
6517
6569
  if (iconContainer) {
6518
- countElement.style.cssText += "position:absolute;left:100%;top:0;transform:translate(-50%,-50%);";
6570
+ countElement.style.cssText += `position:absolute;left:100%;top:${badgeBelow ? "100%" : "0"};transform:translate(-50%,-50%);`;
6519
6571
  iconContainer.appendChild(countElement);
6520
6572
  }
6521
6573
  if (!layers.has(this.viewer)) {
@@ -6524,6 +6576,7 @@ var EntityLabel;
6524
6576
  const layer = layers.get(this.viewer);
6525
6577
  let ele = null;
6526
6578
  let lineEle = null;
6579
+ let runEle = null;
6527
6580
  if (this.label) {
6528
6581
  ele = this.label;
6529
6582
  }
@@ -6560,6 +6613,7 @@ var EntityLabel;
6560
6613
  }
6561
6614
  if (this.line) {
6562
6615
  lineEle = this.line;
6616
+ runEle = this.leaderRun;
6563
6617
  }
6564
6618
  else {
6565
6619
  lineEle = document.createElement("div");
@@ -6603,6 +6657,14 @@ var EntityLabel;
6603
6657
  layer.element.appendChild(lineEleNode);
6604
6658
  layer.element.appendChild(lineEle);
6605
6659
  }
6660
+ // Built here rather than beside the stem, since the style can change on a label that is reused.
6661
+ if (angled && !runEle) {
6662
+ runEle = lineEle.cloneNode(false);
6663
+ runEle.className = "entity-label-leader-run";
6664
+ runEle.style.display = "none";
6665
+ this.leaderRun = runEle;
6666
+ layer.element.appendChild(runEle);
6667
+ }
6606
6668
  let MAX_DISTANCE = Math.max(15000, sizeInM * 35);
6607
6669
  if (visual && visual instanceof Entity) {
6608
6670
  // Consistent sized things.
@@ -6622,6 +6684,25 @@ var EntityLabel;
6622
6684
  y: upwards ? endY - height : endY, endY
6623
6685
  };
6624
6686
  };
6687
+ /*
6688
+ * An angled leader parks the label beside the anchor instead of over it,
6689
+ * so the stem rises to the label's middle and then runs out to whichever side has the room.
6690
+ */
6691
+ const fitAngledLeader = (anchorX, anchorY, desired, width, height, radius = 0) => {
6692
+ const canvas = this.viewer.canvas;
6693
+ const above = Math.max(0, anchorY - height / 2);
6694
+ const below = Math.max(0, canvas.clientHeight - anchorY - height / 2);
6695
+ const upwards = above >= Math.max(radius ? radius + 4 : 0, Math.min(8, desired)) || above >= below;
6696
+ const length = Math.min(desired, upwards ? above : below);
6697
+ const endY = anchorY + (upwards ? -length : length);
6698
+ const run = Math.max(ANGLED_LEADER_RUN, radius + 4);
6699
+ const rightwards = anchorX + run + width <= canvas.clientWidth || anchorX - run - width < 0;
6700
+ return {
6701
+ x: Math.max(0, Math.min(rightwards ? anchorX + run : anchorX - run - width, canvas.clientWidth - width)),
6702
+ y: Math.max(0, Math.min(endY - height / 2, canvas.clientHeight - height)), endY,
6703
+ run, side: (rightwards ? "right" : "left")
6704
+ };
6705
+ };
6625
6706
  let cursorMotion = this.cursorMotion;
6626
6707
  const project = () => {
6627
6708
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
@@ -6759,7 +6840,14 @@ var EntityLabel;
6759
6840
  y -= height / 2;
6760
6841
  }
6761
6842
  // Membership uses the unclamped source footprint, never the summary's changing dimensions.
6762
- if (leader)
6843
+ let leaderRun;
6844
+ let leaderSide;
6845
+ if (leader && angled) {
6846
+ const fit = fitAngledLeader(anchorX, anchorY, leaderLength, width, height);
6847
+ ({ x, y } = fit);
6848
+ ({ run: leaderRun, side: leaderSide } = fit);
6849
+ }
6850
+ else if (leader)
6763
6851
  ({ x, y } = fitLeader(anchorX, anchorY, leaderLength, width, height));
6764
6852
  const bounds = clusterBounds || { x, y, width, height };
6765
6853
  if (!leader && (bounds.x + bounds.width < 0 || bounds.y + bounds.height < 0 || bounds.x > this.viewer.canvas.clientWidth || bounds.y > this.viewer.canvas.clientHeight)) {
@@ -6774,20 +6862,21 @@ var EntityLabel;
6774
6862
  extendedGeometry: visual instanceof Entity && Boolean(visual.polygon || visual.polyline || visual.corridor),
6775
6863
  viewportWidth: this.viewer.canvas.clientWidth,
6776
6864
  id: this.id, entityKey: ((_s = this.rego) === null || _s === void 0 ? void 0 : _s.entityId) ? JSON.stringify([this.rego.entityId, this.rego.menuItemId || "", this.Transient && !this.Persistent ? "hover" : ""]) : undefined,
6777
- entityId: (_t = this.rego) === null || _t === void 0 ? void 0 : _t.entityId, group: this.groupId, x, y, width, height, anchorX, anchorY, depth, leaderLength, distance,
6865
+ entityId: (_t = this.rego) === null || _t === void 0 ? void 0 : _t.entityId, group: this.groupId, x, y, width, height, anchorX, anchorY, depth, leaderLength, leaderRun, leaderSide, distance,
6778
6866
  // Priority shows the hovered member individually without dragging its cluster along.
6779
6867
  opacity, priority: hover || cursorAmount > 0 || (state === null || state === void 0 ? void 0 : state.highlighted) ? 1 : 0, options: this.options, text: originalText, clusterBounds
6780
6868
  };
6781
6869
  };
6782
6870
  let displacement = { x: 0, y: 0 };
6783
- let displayedSummaryMore = false;
6784
6871
  let nudge = { x: 0, y: 0, fromX: 0, fromY: 0, targetX: 0, targetY: 0, start: 0 };
6785
6872
  const apply = (placement) => {
6786
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
6873
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0;
6787
6874
  const show = (placement === null || placement === void 0 ? void 0 : placement.visible) && placement.opacity > 0;
6788
6875
  clickEntityIds = show ? placement.entityIds : [];
6789
6876
  ele.style.visibility = show ? "visible" : "hidden";
6790
6877
  lineEle.style.display = show && leader ? "block" : "none";
6878
+ if (runEle)
6879
+ runEle.style.display = show && leader ? "block" : "none";
6791
6880
  this.anchor.style.display = show && leader ? "block" : "none";
6792
6881
  if (!show) {
6793
6882
  displacement = { x: 0, y: 0 };
@@ -6798,17 +6887,8 @@ var EntityLabel;
6798
6887
  return;
6799
6888
  }
6800
6889
  const displayText = (_a = placement.text) !== null && _a !== void 0 ? _a : originalText;
6801
- const summaryMore = Boolean(placement.summaryMore);
6802
- if (textElement && (textElement.textContent !== displayText + (summaryMore ? "\n…" : "") || displayedSummaryMore !== summaryMore)) {
6890
+ if (textElement && textElement.textContent !== displayText) {
6803
6891
  textElement.textContent = displayText;
6804
- if (summaryMore) {
6805
- const more = document.createElement("span");
6806
- more.className = "entity-label-more";
6807
- more.style.opacity = "0.65";
6808
- more.textContent = "\n…";
6809
- textElement.appendChild(more);
6810
- }
6811
- displayedSummaryMore = summaryMore;
6812
6892
  // Measure the summary's footprint next frame without replacing each member's source text.
6813
6893
  this.viewer.scene.requestRender();
6814
6894
  }
@@ -6826,6 +6906,7 @@ var EntityLabel;
6826
6906
  // The badge participates in layout, so its new width needs another measurement.
6827
6907
  this.viewer.scene.requestRender();
6828
6908
  }
6909
+ fitIconBackground === null || fitIconBackground === void 0 ? void 0 : fitIconBackground();
6829
6910
  if (iconContainer) {
6830
6911
  const badgeWidth = countElement.offsetWidth;
6831
6912
  const iconWidth = iconElement.offsetWidth;
@@ -6833,7 +6914,9 @@ var EntityLabel;
6833
6914
  const clearance = badgeWidth ? (narrow ? badgeWidth / 2 + 2 : Math.max(0, badgeWidth / 2 - iconWidth / 4)) : 0;
6834
6915
  countElement.style.left = `calc(100% + ${clearance}px)`;
6835
6916
  // Reserve the badge overhang so adjacent text and viewport edges remain clear.
6836
- iconContainer.style.marginTop = `${countElement.offsetHeight / 2}px`;
6917
+ const overhang = countElement.offsetHeight / 2;
6918
+ iconContainer.style.marginTop = badgeBelow ? "0px" : `${overhang}px`;
6919
+ iconContainer.style.marginBottom = badgeBelow ? `${overhang}px` : "0px";
6837
6920
  iconContainer.style.marginRight = `${badgeWidth / 2 + clearance}px`;
6838
6921
  }
6839
6922
  // Summary and badge changes must fit on their first visible frame, including after resize.
@@ -6876,19 +6959,26 @@ var EntityLabel;
6876
6959
  sharedRing.animation = ringAnimation;
6877
6960
  }
6878
6961
  const radius = ringAnimation.inner > 0 ? ringAnimation.outer : 0;
6879
- const stem = (leader ? fitLeader(frame.anchorX, frame.anchorY, Math.max(frame.leaderLength, radius ? radius + 4 : 0), ele.offsetWidth / coordinates.scaleX, ele.offsetHeight / coordinates.scaleY, radius) : undefined);
6962
+ const stem = (leader ? (angled ? fitAngledLeader : fitLeader)(frame.anchorX, frame.anchorY, Math.max(frame.leaderLength, radius ? radius + 4 : 0), ele.offsetWidth / coordinates.scaleX, ele.offsetHeight / coordinates.scaleY, radius) : undefined);
6880
6963
  const position = { x: (stem || frame).x, y: (stem || frame).y };
6881
6964
  const width = ele.offsetWidth / coordinates.scaleX;
6882
6965
  const height = ele.offsetHeight / coordinates.scaleY;
6883
- if (leader && placement.textSide === "left") {
6966
+ if (leader && angled) {
6967
+ // The declutter owns the side; the fit only says which one had the room to begin with.
6968
+ const run = Math.max(ANGLED_LEADER_RUN, radius + 4);
6969
+ const side = (_l = (_k = placement.textSide) !== null && _k !== void 0 ? _k : frame.leaderSide) !== null && _l !== void 0 ? _l : "right";
6970
+ const beside = side === "left" ? frame.anchorX - run - width : frame.anchorX + run;
6971
+ position.x = Math.max(0, Math.min(beside, this.viewer.canvas.clientWidth - width));
6972
+ }
6973
+ else if (leader && placement.textSide === "left") {
6884
6974
  position.x = Math.max(0, Math.min(frame.anchorX - width, this.viewer.canvas.clientWidth - width));
6885
6975
  }
6886
- const gap = (_l = (_k = this.options.pointerAvoidance) === null || _k === void 0 ? void 0 : _k.gap) !== null && _l !== void 0 ? _l : 12;
6976
+ const gap = (_o = (_m = this.options.pointerAvoidance) === null || _m === void 0 ? void 0 : _m.gap) !== null && _o !== void 0 ? _o : 12;
6887
6977
  const containsPointer = (x, y, margin) => {
6888
6978
  return pointer && pointer.x >= x - margin && pointer.x <= x + width + margin && pointer.y >= y - margin && pointer.y <= y + height + margin;
6889
6979
  };
6890
6980
  const passive = !selectOnClick || !this.visualRegister;
6891
- if (passive && ((_m = this.options.pointerAvoidance) === null || _m === void 0 ? void 0 : _m.enabled) !== false && pointer &&
6981
+ if (passive && ((_p = this.options.pointerAvoidance) === null || _p === void 0 ? void 0 : _p.enabled) !== false && pointer &&
6892
6982
  (containsPointer(position.x, position.y, gap) || containsPointer(position.x + displacement.x, position.y + displacement.y, gap))) {
6893
6983
  if (containsPointer(position.x + displacement.x, position.y + displacement.y, 0)) {
6894
6984
  // Choose against the original box and retain the offset until the pointer leaves both boxes.
@@ -6906,7 +6996,7 @@ var EntityLabel;
6906
6996
  else {
6907
6997
  displacement = { x: 0, y: 0 };
6908
6998
  }
6909
- const nudgeDuration = (_p = (_o = this.options.pointerAvoidance) === null || _o === void 0 ? void 0 : _o.transitionDuration) !== null && _p !== void 0 ? _p : 200;
6999
+ const nudgeDuration = (_r = (_q = this.options.pointerAvoidance) === null || _q === void 0 ? void 0 : _q.transitionDuration) !== null && _r !== void 0 ? _r : 200;
6910
7000
  const animateNudge = () => {
6911
7001
  const progress = now < nudge.start ? 0 : nudgeDuration > 0 ? Math.min(1, (now - nudge.start) / nudgeDuration) : 1;
6912
7002
  const eased = progress * progress * (3 - 2 * progress);
@@ -6922,7 +7012,7 @@ var EntityLabel;
6922
7012
  fromY: nudge.y,
6923
7013
  targetX: displacement.x,
6924
7014
  targetY: displacement.y,
6925
- start: now + ((_r = (_q = this.options.pointerAvoidance) === null || _q === void 0 ? void 0 : _q.delay) !== null && _r !== void 0 ? _r : 180)
7015
+ start: now + ((_t = (_s = this.options.pointerAvoidance) === null || _s === void 0 ? void 0 : _s.delay) !== null && _t !== void 0 ? _t : 180)
6926
7016
  };
6927
7017
  }
6928
7018
  if (animateNudge() < 1 && (nudge.fromX !== nudge.targetX || nudge.fromY !== nudge.targetY)) {
@@ -6934,7 +7024,7 @@ var EntityLabel;
6934
7024
  ele.style.top = `${coordinates.y + position.y * coordinates.scaleY}px`;
6935
7025
  const clickTarget = content.querySelector('[role="button"]');
6936
7026
  if (clickTarget) {
6937
- clickTarget.setAttribute("aria-label", placement.members.length ? `Select ${placement.entityIds.length} entities` : `Select ${(_t = (_s = this.options.text) !== null && _s !== void 0 ? _s : text) !== null && _t !== void 0 ? _t : (_u = this.rego) === null || _u === void 0 ? void 0 : _u.entityId}`);
7027
+ clickTarget.setAttribute("aria-label", placement.members.length ? `Select ${placement.entityIds.length} entities` : `Select ${(_v = (_u = this.options.text) !== null && _u !== void 0 ? _u : text) !== null && _v !== void 0 ? _v : (_w = this.rego) === null || _w === void 0 ? void 0 : _w.entityId}`);
6938
7028
  }
6939
7029
  if (leader) {
6940
7030
  const nodeRadius = ringAnimation.outer;
@@ -6957,8 +7047,13 @@ var EntityLabel;
6957
7047
  anchor.style.opacity = String(opacity * (normalLeader ? 1 : cursorMotion.amount));
6958
7048
  anchor.style.zIndex = String(placement.zIndex);
6959
7049
  // The stem starts at the rim, never across the transparent centre, even after passive displacement.
6960
- const endX = Math.max(position.x, Math.min(frame.anchorX, position.x + width));
6961
- const endY = frame.anchorY < position.y ? position.y : frame.anchorY > position.y + height ? position.y + height : frame.anchorY;
7050
+ const straightX = Math.max(position.x, Math.min(frame.anchorX, position.x + width));
7051
+ const straightY = frame.anchorY < position.y ? position.y : frame.anchorY > position.y + height ? position.y + height : frame.anchorY;
7052
+ const beside = angled && (frame.anchorX < position.x || frame.anchorX > position.x + width);
7053
+ const elbowY = position.y + height / 2;
7054
+ const endX = beside ? frame.anchorX : straightX;
7055
+ const endY = beside ? elbowY : straightY;
7056
+ const runToX = frame.anchorX < position.x ? position.x : position.x + width;
6962
7057
  const dx = endX - frame.anchorX, dy = endY - frame.anchorY;
6963
7058
  const length = Math.hypot(dx, dy);
6964
7059
  const leaderAngle = Math.atan2(dy, dx);
@@ -6969,13 +7064,13 @@ var EntityLabel;
6969
7064
  // Keep the stem inside the 126-degree arc, away from antialiased endpoints.
6970
7065
  const arcFraction = 0.35;
6971
7066
  const rotationLimit = Math.PI * arcFraction - Math.PI / 30;
6972
- const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((_v = ringAnimation.arcOffset) !== null && _v !== void 0 ? _v : 0);
7067
+ const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((_x = ringAnimation.arcOffset) !== null && _x !== void 0 ? _x : 0);
6973
7068
  const targetOpen = cursorMotion.target || (hovered && (ring === null || ring === void 0 ? void 0 : ring.enabled) !== false && targetRadius > 0) ? 1 : 0;
6974
- const elapsed = Math.min(32, Math.max(0, now - ((_w = ringAnimation.arcTime) !== null && _w !== void 0 ? _w : now)));
7069
+ const elapsed = Math.min(32, Math.max(0, now - ((_y = ringAnimation.arcTime) !== null && _y !== void 0 ? _y : now)));
6975
7070
  const blend = duration > 0 ? 1 - Math.exp(-elapsed / (duration / 3)) : 1;
6976
7071
  const approach = (value, target) => Math.abs(value - target) < 0.001 ? target : value + (target - value) * blend;
6977
- ringAnimation.arcOffset = approach((_x = ringAnimation.arcOffset) !== null && _x !== void 0 ? _x : 0, targetOffset);
6978
- ringAnimation.arcOpen = approach((_y = ringAnimation.arcOpen) !== null && _y !== void 0 ? _y : 0, targetOpen);
7072
+ ringAnimation.arcOffset = approach((_z = ringAnimation.arcOffset) !== null && _z !== void 0 ? _z : 0, targetOffset);
7073
+ ringAnimation.arcOpen = approach((_0 = ringAnimation.arcOpen) !== null && _0 !== void 0 ? _0 : 0, targetOpen);
6979
7074
  ringAnimation.arcTime = now;
6980
7075
  if (ringAnimation.arcOffset !== targetOffset || ringAnimation.arcOpen !== targetOpen) {
6981
7076
  this.viewer.scene.requestRender();
@@ -6987,17 +7082,27 @@ var EntityLabel;
6987
7082
  const cut = Math.min(radius, length);
6988
7083
  const startX = frame.anchorX + (length ? dx / length * cut : 0);
6989
7084
  const startY = frame.anchorY + (length ? dy / length * cut : 0);
6990
- const screenDx = (endX - startX) * coordinates.scaleX;
6991
- const screenDy = (endY - startY) * coordinates.scaleY;
6992
- lineEle.style.width = `${2 * coordinates.scaleX}px`;
6993
- lineEle.style.left = `${coordinates.x + (startX - 1) * coordinates.scaleX}px`;
6994
- lineEle.style.top = `${coordinates.y + startY * coordinates.scaleY}px`;
6995
- lineEle.style.height = `${Math.hypot(screenDx, screenDy)}px`;
6996
- lineEle.style.transform = `rotate(${Math.atan2(screenDy, screenDx) - Math.PI / 2}rad)`;
6997
- lineEle.style.boxShadow = radius ? "none" : "0 0 5px rgba(0, 0, 0, 0.3)";
6998
- lineEle.style.transformOrigin = "top center";
6999
- lineEle.style.opacity = String(opacity * (normalLeader ? 1 : cursorMotion.amount));
7000
- lineEle.style.zIndex = String(placement.zIndex);
7085
+ const draw = (element, fromX, fromY, toX, toY) => {
7086
+ const screenDx = (toX - fromX) * coordinates.scaleX;
7087
+ const screenDy = (toY - fromY) * coordinates.scaleY;
7088
+ element.style.width = `${2 * coordinates.scaleX}px`;
7089
+ element.style.left = `${coordinates.x + (fromX - 1) * coordinates.scaleX}px`;
7090
+ element.style.top = `${coordinates.y + fromY * coordinates.scaleY}px`;
7091
+ element.style.height = `${Math.hypot(screenDx, screenDy)}px`;
7092
+ element.style.transform = `rotate(${Math.atan2(screenDy, screenDx) - Math.PI / 2}rad)`;
7093
+ element.style.boxShadow = radius ? "none" : "0 0 5px rgba(0, 0, 0, 0.3)";
7094
+ element.style.transformOrigin = "top center";
7095
+ element.style.opacity = String(opacity * (normalLeader ? 1 : cursorMotion.amount));
7096
+ element.style.zIndex = String(placement.zIndex);
7097
+ };
7098
+ draw(lineEle, startX, startY, endX, endY);
7099
+ if (runEle) {
7100
+ // Overlap the corner by the stem's own width so the elbow has no notch in it.
7101
+ const overlap = frame.anchorX < runToX ? -1 : 1;
7102
+ runEle.style.display = beside ? "block" : "none";
7103
+ if (beside)
7104
+ draw(runEle, endX + overlap, endY, runToX, endY);
7105
+ }
7001
7106
  }
7002
7107
  };
7003
7108
  ele.style.display = "flex";
@@ -7022,7 +7127,7 @@ var EntityLabel;
7022
7127
  * Removes label from the viewer and unsubscribes from events.
7023
7128
  */
7024
7129
  removeLabel() {
7025
- var _a;
7130
+ var _a, _b;
7026
7131
  (_a = this.anchor) === null || _a === void 0 ? void 0 : _a.remove();
7027
7132
  this.anchor = null;
7028
7133
  if (this.label &&
@@ -7034,6 +7139,8 @@ var EntityLabel;
7034
7139
  this.line.parentElement.removeChild(this.line);
7035
7140
  this.line = null;
7036
7141
  }
7142
+ (_b = this.leaderRun) === null || _b === void 0 ? void 0 : _b.remove();
7143
+ this.leaderRun = null;
7037
7144
  if (this.renderRemoval) {
7038
7145
  this.renderRemoval();
7039
7146
  this.renderRemoval = null;
@@ -16689,9 +16796,6 @@ class EntityGatherer {
16689
16796
  // Last date-time range taken from the clock.
16690
16797
  this.lastDateTimeMin = null;
16691
16798
  this.lastDateTimeMax = null;
16692
- // Entity ID -> last resolved date time.
16693
- // Helps determine if a slow resolved request can still be applied while we wait for the next one.
16694
- this.lastFoundDateTimes = new Map();
16695
16799
  // ID of Entity IDs that need to be requested.
16696
16800
  this.eIdQueue = [];
16697
16801
  // Membership mirror of eIdQueue for quick checks.
@@ -16723,8 +16827,12 @@ class EntityGatherer {
16723
16827
  // Unsubscribes the view monitor listener that queues what the camera has moved onto.
16724
16828
  this.monitorRemoval = null;
16725
16829
  // Indicates a tick is active.
16726
- // When true, we won't bother calling onTick again.
16830
+ // When true, a new tick is not started; it is noted in pendingTick and run once this one ends.
16727
16831
  this.runningTick = false;
16832
+ // A tick was asked for while one was running, so another is owed once it finishes.
16833
+ this.pendingTick = false;
16834
+ // The debounce every clock change goes through, kept so a tick owed after a running one does too.
16835
+ this.tickDelay = null;
16728
16836
  // Entity ID -> Entity blob.
16729
16837
  // This is a tool that lets developers set their own JSON blobs instead of requesting data from API.
16730
16838
  this.entityDataOverrides = new Map();
@@ -16788,11 +16896,13 @@ class EntityGatherer {
16788
16896
  let tickDelay = new DelayQueue(() => {
16789
16897
  this.onTick(false);
16790
16898
  }, 250, true);
16899
+ this.tickDelay = tickDelay;
16791
16900
  this.hDisposals.push(() => {
16792
16901
  if (tickDelay) {
16793
16902
  tickDelay.Dispose();
16794
16903
  }
16795
16904
  tickDelay = null;
16905
+ this.tickDelay = null;
16796
16906
  });
16797
16907
  // React to clock changes and request new Entities.
16798
16908
  let prevTime = null;
@@ -16832,7 +16942,6 @@ class EntityGatherer {
16832
16942
  this.priorityCount = 0;
16833
16943
  this.lastOrderFocus = null;
16834
16944
  this.allEIds.clear();
16835
- this.lastFoundDateTimes.clear();
16836
16945
  this.everFound.clear();
16837
16946
  this.clearIndex();
16838
16947
  this.lastDateTime = null;
@@ -17194,7 +17303,6 @@ class EntityGatherer {
17194
17303
  this.eIdQueued.clear();
17195
17304
  this.priorityCount = 0;
17196
17305
  this.allEIds.clear();
17197
- this.lastFoundDateTimes.clear();
17198
17306
  this.everFound.clear();
17199
17307
  this.clearIndex();
17200
17308
  this.entityDataOverrides.clear();
@@ -17210,7 +17318,13 @@ class EntityGatherer {
17210
17318
  }
17211
17319
  }
17212
17320
  onTick(force) {
17213
- if (this.runningTick || this.disposed) {
17321
+ if (this.disposed) {
17322
+ return;
17323
+ }
17324
+ if (this.runningTick) {
17325
+ if (this.historic) {
17326
+ this.pendingTick = true;
17327
+ }
17214
17328
  return;
17215
17329
  }
17216
17330
  const rTime = this.viewer.clock.currentTime.toString();
@@ -17218,10 +17332,16 @@ class EntityGatherer {
17218
17332
  return;
17219
17333
  }
17220
17334
  this.lastDateTime = rTime;
17335
+ // Whether the clock has left the moment this tick is reading.
17336
+ const movedOn = () => this.historic && this.viewer.clock.currentTime.toString() !== rTime;
17221
17337
  (async () => {
17222
17338
  this.runningTick = true;
17339
+ this.pendingTick = false;
17223
17340
  this.placeStragglers();
17224
17341
  let rerun = false;
17342
+ let interrupted = false;
17343
+ // Batch's answer is applied while the next batch is being read.
17344
+ const applying = [];
17225
17345
  try {
17226
17346
  const handleResponse = async (requestedBatch, entities) => {
17227
17347
  var _a, _b, _c, _d;
@@ -17285,45 +17405,7 @@ class EntityGatherer {
17285
17405
  api: this.api
17286
17406
  })).tags;
17287
17407
  }
17288
- // 0 = rTime is still the current time.
17289
- // 1 = rTime is in the future.
17290
- // -1 = rTime is in the past.
17291
- let changeDir = 0;
17292
- if (this.lastDateTime !== this.viewer.clock.currentTime.toString()) {
17293
- changeDir = this.lastDateTime < this.viewer.clock.currentTime.toString() ? 1 : -1;
17294
- }
17295
- let toEmit;
17296
- if (this.historic) {
17297
- // We can emit Entities if the last resolved time (per Entity) isn't newer than rTime.
17298
- // We compare using the changeDir to determine if we should emit or not.
17299
- toEmit = resolvedEntities.filter((e) => {
17300
- var _a;
17301
- const entityId = ((_a = e === null || e === void 0 ? void 0 : e.Bruce) === null || _a === void 0 ? void 0 : _a.ID) || (e === null || e === void 0 ? void 0 : e.id);
17302
- const lastFoundDateTime = this.lastFoundDateTimes.get(entityId);
17303
- if (lastFoundDateTime) {
17304
- if (changeDir === 1) {
17305
- return lastFoundDateTime <= rTime;
17306
- }
17307
- else if (changeDir === -1) {
17308
- return lastFoundDateTime >= rTime;
17309
- }
17310
- }
17311
- return true;
17312
- });
17313
- // Update the last found date times for the filtered Entities.
17314
- toEmit.forEach((e) => {
17315
- var _a;
17316
- const entityId = ((_a = e === null || e === void 0 ? void 0 : e.Bruce) === null || _a === void 0 ? void 0 : _a.ID) || (e === null || e === void 0 ? void 0 : e.id);
17317
- if (entityId) {
17318
- this.lastFoundDateTimes.set(entityId, rTime);
17319
- }
17320
- });
17321
- }
17322
- else {
17323
- // If we're not historic, we can just emit everything.
17324
- toEmit = resolvedEntities;
17325
- }
17326
- this.emitEntities(toEmit, tags);
17408
+ this.emitEntities(resolvedEntities, tags);
17327
17409
  };
17328
17410
  const QUEUE_BATCH_SIZE = 500;
17329
17411
  const requestedIds = new Set();
@@ -17332,7 +17414,8 @@ class EntityGatherer {
17332
17414
  const total = this.eIdQueue.length;
17333
17415
  let done = 0;
17334
17416
  while (this.eIdQueue.length && !this.disposed) {
17335
- if (this.historic && this.lastDateTime !== rTime) {
17417
+ if (movedOn()) {
17418
+ interrupted = true;
17336
17419
  break;
17337
17420
  }
17338
17421
  this.orderQueueByCamera();
@@ -17348,7 +17431,7 @@ class EntityGatherer {
17348
17431
  api: this.api,
17349
17432
  maxSearchTimeSec: 60 * 2
17350
17433
  });
17351
- handleResponse(batch, entities);
17434
+ applying.push(handleResponse(batch, entities));
17352
17435
  for (let i = 0; i < batch.length; i++) {
17353
17436
  requestedIds.add(batch[i]);
17354
17437
  }
@@ -17380,7 +17463,8 @@ class EntityGatherer {
17380
17463
  // The near ones are resolved first out of whatever the view still reaches.
17381
17464
  allEIds = this.bandIds(allEIds);
17382
17465
  while (allEIds.length && !this.disposed && !this.eIdQueue.length) {
17383
- if (this.historic && this.lastDateTime !== rTime) {
17466
+ if (movedOn()) {
17467
+ interrupted = true;
17384
17468
  break;
17385
17469
  }
17386
17470
  const batch = allEIds.splice(0, QUEUE_BATCH_SIZE);
@@ -17391,7 +17475,7 @@ class EntityGatherer {
17391
17475
  api: this.api,
17392
17476
  maxSearchTimeSec: 60 * 2
17393
17477
  });
17394
- handleResponse(batch, entities);
17478
+ applying.push(handleResponse(batch, entities));
17395
17479
  for (let i = 0; i < batch.length; i++) {
17396
17480
  requestedIds.add(batch[i]);
17397
17481
  }
@@ -17406,10 +17490,32 @@ class EntityGatherer {
17406
17490
  console.error(e);
17407
17491
  }
17408
17492
  finally {
17493
+ // Only a historic answer can be overtaken by a newer moment, so nothing else waits.
17494
+ if (this.historic && applying.length) {
17495
+ await Promise.all(applying.map((pending) => pending.catch((e) => console.error(e))));
17496
+ }
17409
17497
  this.runningTick = false;
17410
17498
  }
17411
- if (rerun) {
17499
+ // A moment read only in part is forgotten, so coming back to it reads it again in full.
17500
+ if (interrupted && this.lastDateTime === rTime) {
17501
+ this.lastDateTime = null;
17502
+ }
17503
+ if (this.disposed) {
17504
+ return;
17505
+ }
17506
+ // Leftovers at the same moment carry straight on, as they always have.
17507
+ if (rerun && !interrupted) {
17412
17508
  this.onTick(true);
17509
+ return;
17510
+ }
17511
+ if (interrupted || this.pendingTick || movedOn()) {
17512
+ this.pendingTick = false;
17513
+ if (this.tickDelay) {
17514
+ this.tickDelay.Call();
17515
+ }
17516
+ else {
17517
+ this.onTick(false);
17518
+ }
17413
17519
  }
17414
17520
  })();
17415
17521
  }
@@ -49099,7 +49205,7 @@ var EntityRenderEngine;
49099
49205
  }
49100
49206
  EntityRenderEngine.GetGraphicShapeId = GetGraphicShapeId;
49101
49207
  async function Render(params) {
49102
- var _a, _b, _c;
49208
+ var _a, _b, _c, _d, _e;
49103
49209
  const groupRenderParams = {
49104
49210
  apiGetter: params.apiGetter,
49105
49211
  viewer: params.viewer,
@@ -49276,10 +49382,20 @@ var EntityRenderEngine;
49276
49382
  polygons.push(entity);
49277
49383
  continue;
49278
49384
  }
49385
+ // Present when the gate above chose to reuse this entity's graphic.
49386
+ const existingRoot = cEntities.get(entity.Bruce.ID);
49387
+ const existingParts = new Map();
49388
+ (_d = existingRoot === null || existingRoot === void 0 ? void 0 : existingRoot._siblingGraphics) === null || _d === void 0 ? void 0 : _d.forEach((part) => {
49389
+ if (part._multiPartIndex != null) {
49390
+ existingParts.set(part._multiPartIndex, part);
49391
+ }
49392
+ });
49393
+ const partRendered = new Map();
49394
+ const partIndexes = new Map();
49279
49395
  const pParams = {
49280
49396
  ...groupRenderParams,
49281
49397
  entities: [],
49282
- rendered: cEntities
49398
+ rendered: partRendered
49283
49399
  };
49284
49400
  pParams.entitiesHistoric = params.entitiesHistoric;
49285
49401
  const zoomItem = pParams.zoomItems[entity.Bruce.ID];
@@ -49295,39 +49411,59 @@ var EntityRenderEngine;
49295
49411
  };
49296
49412
  pParams.zoomItems[subEntity.Bruce.ID] = zoomItem;
49297
49413
  pParams.entities.push(subEntity);
49298
- }
49299
- const cPoly = await Polygon.RenderGroup(pParams);
49300
- Array.from(cPoly.keys()).forEach((key) => {
49301
- if (cPoly.get(key)) {
49302
- pParams.entities = pParams.entities.filter((e) => e.Bruce.ID != key);
49303
- }
49304
- });
49305
- let rendered = Array.from(cPoly.values());
49306
- const cLines = await Polyline.RenderGroup(pParams);
49307
- Array.from(cLines.keys()).forEach((key) => {
49308
- if (cLines.get(key)) {
49414
+ partIndexes.set(subEntity.Bruce.ID, j);
49415
+ const existingPart = existingParts.get(j);
49416
+ if (existingPart) {
49417
+ // An engine preparing a reused graphic gathers everything connected to it, so a part still
49418
+ // attached to its root would have every other part evicted from the scene.
49419
+ existingPart._parentEntity = null;
49420
+ partRendered.set(subEntity.Bruce.ID, existingPart);
49421
+ }
49422
+ }
49423
+ const rendered = [];
49424
+ const collect = (results) => {
49425
+ results.forEach((cEntity, key) => {
49426
+ if (!cEntity) {
49427
+ return;
49428
+ }
49429
+ cEntity._multiPartIndex = partIndexes.get(key);
49430
+ rendered.push(cEntity);
49309
49431
  pParams.entities = pParams.entities.filter((e) => e.Bruce.ID != key);
49310
- }
49311
- });
49312
- rendered = rendered.concat(Array.from(cLines.values()));
49432
+ });
49433
+ };
49434
+ collect(await Polygon.RenderGroup(pParams));
49435
+ collect(await Polyline.RenderGroup(pParams));
49313
49436
  // We'll only render points when there's no other geometry.
49314
49437
  // Generally speaking when polygons/polylines are drawn, points are only drawn because of invalid data.
49315
49438
  if (!rendered.length) {
49316
- const cPoints = await Point.RenderGroup(pParams);
49317
- rendered = rendered.concat(Array.from(cPoints.values()));
49439
+ collect(await Point.RenderGroup(pParams));
49318
49440
  }
49319
- rendered = rendered.filter(x => x != null);
49320
49441
  if (rendered.length) {
49321
- const rootEntity = new Entity({
49322
- id: ObjectUtils.UId(15)
49323
- });
49324
- rootEntity._siblingGraphics = [];
49442
+ const sameParts = ((_e = existingRoot === null || existingRoot === void 0 ? void 0 : existingRoot._siblingGraphics) === null || _e === void 0 ? void 0 : _e.length) == rendered.length &&
49443
+ rendered.every((part) => existingRoot._siblingGraphics.includes(part));
49444
+ let rootEntity;
49445
+ if (sameParts) {
49446
+ rootEntity = existingRoot;
49447
+ }
49448
+ else {
49449
+ if (existingRoot === null || existingRoot === void 0 ? void 0 : existingRoot._siblingGraphics) {
49450
+ // Retiring the old root removes its parts, and the ones carried over belong to the new root now.
49451
+ existingRoot._siblingGraphics = existingRoot._siblingGraphics.filter((part) => !rendered.includes(part));
49452
+ }
49453
+ rootEntity = new Entity({
49454
+ id: ObjectUtils.UId(15)
49455
+ });
49456
+ }
49457
+ rootEntity._siblingGraphics = rendered;
49325
49458
  rootEntity._renderGroup = GetRenderGroupId(zoomItem);
49326
- rootEntity._siblingGraphics = rootEntity._siblingGraphics.concat(rendered);
49459
+ // The skip gate compares the rego's style against the zoom item's, and the rego takes it from here.
49460
+ rootEntity.styleId = (zoomItem === null || zoomItem === void 0 ? void 0 : zoomItem.StyleID) == -1 ? -1 : (+(zoomItem === null || zoomItem === void 0 ? void 0 : zoomItem.StyleID) || null);
49461
+ rootEntity.styleEffective = rendered[0].styleEffective;
49327
49462
  cEntities.set(entity.Bruce.ID, rootEntity);
49328
- const firstEntity = rendered[0];
49329
- if (firstEntity) {
49330
- rootEntity.position = GetCValue(params.viewer, firstEntity.position);
49463
+ const newPosition = GetCValue(params.viewer, rendered[0].position);
49464
+ const oldPosition = GetCValue(params.viewer, rootEntity.position);
49465
+ if (newPosition && (!oldPosition || !Cartes.IsEqualCartes3(oldPosition, newPosition))) {
49466
+ rootEntity.position = new ConstantPositionProperty(newPosition);
49331
49467
  }
49332
49468
  rendered.forEach((child) => {
49333
49469
  child._parentEntity = rootEntity;
@@ -50264,7 +50400,7 @@ var StyleUtils;
50264
50400
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
50265
50401
  })(StyleUtils || (StyleUtils = {}));
50266
50402
 
50267
- const VERSION = "7.4.1";
50403
+ const VERSION = "7.4.3";
50268
50404
  /**
50269
50405
  * Updates the environment instance used by bruce-cesium to one specified.
50270
50406
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.