bruce-cesium 7.4.2 → 7.4.4

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, ColorBlendMode, HeadingPitchRoll, Math as Math$1, Transforms, ShadowMode, Cartesian3, ClassificationType, Model, ShaderSource, Ellipsoid, 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, DistanceDisplayCondition, ArcType, CornerType, ConstantPositionProperty, HorizontalOrigin, VerticalOrigin, ImageMaterialProperty, SceneTransforms, PolygonHierarchy, PolylineGraphics, Rectangle, FeatureDetection, KmlDataSource, NearFarScalar, GeoJsonDataSource, Matrix3, SceneMode, Ion, IonResource, Cesium3DTileStyle, HeadingPitchRange, Cesium3DTileColorBlendMode, Cesium3DTileset, Intersect, OrthographicFrustum, EasingFunction, ModelGraphics, PolygonGraphics, CorridorGraphics, PointGraphics, BillboardGraphics, EllipseGraphics, PolylineDashMaterialProperty, EllipsoidGeodesic, EllipsoidTerrainProvider, sampleTerrainMostDetailed, defined, PolygonPipeline, IonImageryProvider, createWorldImagery, createWorldImageryAsync, BingMapsImageryProvider, BingMapsStyle, MapboxImageryProvider, MapboxStyleImageryProvider, ArcGisMapServerImageryProvider, OpenStreetMapImageryProvider, UrlTemplateImageryProvider, GridImageryProvider, GeographicTilingScheme, ImageryLayer, TileMapServiceImageryProvider, CesiumTerrainProvider, GeometryInstance, ScreenSpaceEventHandler, ScreenSpaceEventType, CesiumInspector, ClockRange, DynamicEnvironmentMapManager, CustomDataSource, 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, ClientFileValueMap, Color as Color$1, BruceEvent, EntityCoords, MenuItem, Tileset, ProjectView, ProjectViewBookmark, ZoomControl, Bounds, DataLab, DelayQueue, AccountConcept, BruceApi, EntityRelation, RecordChangeFeed, EntitySource, Camera, ProgramKey, ProjectViewLegacyTile, Account, AccountLimits, Session, UrlUtils, EntityAttachment, EntityAttachmentType, EntityAttribute, MathUtils, ProjectViewBookmarkGroup, EntityRelationType, EntityHistoricData } from 'bruce-models';
2
+ import { Cartographic, ColorMaterialProperty, Entity, Color, ConstantProperty, CallbackProperty, Primitive, Cesium3DTileFeature, Math as Math$1, Cartesian3, JulianDate, Quaternion, Transforms, HeadingPitchRoll, HeightReference, Matrix4, ColorBlendMode, ShadowMode, ClassificationType, Model, HorizontalOrigin, VerticalOrigin, ConstantPositionProperty, ArcType, CornerType, DistanceDisplayCondition, ShaderSource, Ellipsoid, Geometry, GeometryAttribute, ComponentDatatype, PrimitiveType, BoundingSphere, GeometryPipeline, Cartesian2, Cartesian4, sampleTerrain, Texture, PixelFormat, Sampler, TextureWrap, TextureMinificationFilter, TextureMagnificationFilter, VertexArray, BufferUsage, ShaderProgram, RenderState, BlendingState, DrawCommand, Pass, ImageMaterialProperty, PolygonHierarchy, PolylineGraphics, Rectangle, SceneTransforms, FeatureDetection, NearFarScalar, Matrix3, KmlDataSource, GeoJsonDataSource, SceneMode, Cesium3DTileStyle, Cesium3DTileColorBlendMode, HeadingPitchRange, Ion, IonResource, Cesium3DTileset, EllipsoidTerrainProvider, IonImageryProvider, createWorldImagery, createWorldImageryAsync, BingMapsImageryProvider, BingMapsStyle, MapboxImageryProvider, MapboxStyleImageryProvider, ArcGisMapServerImageryProvider, OpenStreetMapImageryProvider, UrlTemplateImageryProvider, GridImageryProvider, GeographicTilingScheme, ImageryLayer, TileMapServiceImageryProvider, CesiumTerrainProvider, OrthographicFrustum, EasingFunction, CustomDataSource, PolygonPipeline, EllipsoidGeodesic, sampleTerrainMostDetailed, defined, ModelGraphics, PolygonGraphics, CorridorGraphics, PointGraphics, BillboardGraphics, EllipseGraphics, PolylineDashMaterialProperty, Intersect, 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, Bounds, MenuItem, ProjectView, ProjectViewBookmark, Tileset, ZoomControl, BruceEvent, EntityCoords, DataLab, DelayQueue, AccountConcept, RecordChangeFeed, BruceApi, EntityRelation, EntitySource, ProgramKey, ProjectViewLegacyTile, Camera, Account, AccountLimits, Session, UrlUtils, EntityAttachment, EntityAttachmentType, EntityAttribute, MathUtils, ProjectViewBookmarkGroup, EntityRelationType, EntityHistoricData } from 'bruce-models';
4
4
 
5
5
  /**
6
6
  * Ensures a number is returned from a given value.
@@ -5403,8 +5403,16 @@ var EntityLabel;
5403
5403
  // so a row of labels reads as one size rather than a set of differently shaped pills.
5404
5404
  const ICON_BACKGROUND_UNIFORM_TOLERANCE = 0.3;
5405
5405
  const TEXT_MAX_WIDTH = 200;
5406
+ // Distinct icons a cluster shows, the representative's included.
5407
+ const ICON_STACK_LIMIT = 3;
5408
+ // Size of every icon in a cluster's arrangement, relative to the representative's own.
5409
+ const ICON_STACK_SCALE = 0.6;
5406
5410
  // How far an angled leader runs sideways before the label starts.
5407
5411
  const ANGLED_LEADER_RUN = 28;
5412
+ // Clearance between a point or billboard glyph and the ring drawn around it.
5413
+ const ICON_RING_PADDING = 4;
5414
+ // Small points still get a ring as large as the default near ring.
5415
+ const ICON_RING_MIN = 10;
5408
5416
  // One per tinted icon, since an SVG mask is addressed by id across the whole document.
5409
5417
  let tintCounter = 0;
5410
5418
  // Key where the label instance is stored within the cesium entity.
@@ -5586,6 +5594,28 @@ var EntityLabel;
5586
5594
  * Prefer declared semantics; only infers from recognisable glyphs and filename tokens, never image pixels.
5587
5595
  */
5588
5596
  EntityLabel.IconPriority = GetClusterIconPriority;
5597
+ function iconLook(options) {
5598
+ return {
5599
+ icon: options.icon || undefined,
5600
+ iconUrl: options.iconUrl || undefined,
5601
+ iconClass: typeof options.iconClass === "string" && options.iconClass ? options.iconClass : undefined,
5602
+ iconColor: typeof options.iconColor === "string" && options.iconColor ? options.iconColor : undefined,
5603
+ iconBackground: options.iconBackground || undefined
5604
+ };
5605
+ }
5606
+ /*
5607
+ * Everything that makes two labels look alike, so labels from different groups only merge when they would read the same.
5608
+ */
5609
+ function clusterLook(options) {
5610
+ var _a;
5611
+ const presentation = options.presentation || "text";
5612
+ return JSON.stringify([
5613
+ presentation,
5614
+ (_a = options.leader) !== null && _a !== void 0 ? _a : null,
5615
+ options.leaderStyle || "default", options.leaderColor || "",
5616
+ presentation === "text" ? null : iconLook(options)
5617
+ ]);
5618
+ }
5589
5619
  /*
5590
5620
  * Resolves clustering and stable text placement before depth fading; highlighted labels receive priority.
5591
5621
  */
@@ -5607,7 +5637,8 @@ var EntityLabel;
5607
5637
  if (!((_b = (_a = frame.options) === null || _a === void 0 ? void 0 : _a.cluster) === null || _b === void 0 ? void 0 : _b.enabled) || frame.priority > 0) {
5608
5638
  continue;
5609
5639
  }
5610
- const key = JSON.stringify([frame.group || "", frame.options.presentation || "text"]);
5640
+ // Groups meet inside a presentation; which of them may merge is decided pairwise below.
5641
+ const key = frame.options.presentation || "text";
5611
5642
  if (!groups.has(key)) {
5612
5643
  groups.set(key, []);
5613
5644
  }
@@ -5619,32 +5650,54 @@ var EntityLabel;
5619
5650
  frame.options.cluster.pixelRange == null ? { ...frame, x: frame.anchorX, y: frame.anchorY, width: 1, height: 1 } :
5620
5651
  frame.clusterBounds ? { ...frame, ...frame.clusterBounds } : frame);
5621
5652
  const index = new ScreenIndex();
5622
- for (const frame of candidates) {
5653
+ const rank = new Map();
5654
+ const looks = new Map();
5655
+ candidates.forEach((frame, i) => {
5623
5656
  index.Add(frame);
5624
- }
5657
+ rank.set(frame.id, i);
5658
+ looks.set(frame.id, clusterLook(frame.options));
5659
+ });
5625
5660
  const clusters = ClusterItems({
5626
5661
  items: candidates,
5627
5662
  id: frame => frame.id,
5628
5663
  minSize: group[0].options.cluster.minSize,
5629
5664
  neighbours: frame => {
5630
5665
  var _a;
5631
- return (index.Query(frame, (_a = frame.options.cluster.pixelRange) !== null && _a !== void 0 ? _a : (frame.options.presentation === "icon" ? 24 : 8)));
5666
+ const nearby = index.Query(frame, (_a = frame.options.cluster.pixelRange) !== null && _a !== void 0 ? _a : (frame.options.presentation === "icon" ? 24 : 8)).sort((a, b) => rank.get(a.id) - rank.get(b.id));
5667
+ return compatibleMembers(frame, nearby, other => other.group || "", other => looks.get(other.id));
5632
5668
  }
5633
5669
  });
5634
5670
  for (const cluster of clusters) {
5635
5671
  let chosen = cluster.representative;
5636
- if ((chosen.options.presentation || "text") !== "text" && chosen.options.cluster.representative !== "nearest") {
5637
- let priority = EntityLabel.IconPriority(chosen.options);
5638
- for (const member of cluster.members) {
5639
- const candidate = EntityLabel.IconPriority(member.options);
5640
- if (candidate > priority) {
5641
- chosen = member;
5642
- priority = candidate;
5643
- }
5644
- }
5672
+ const iconic = (chosen.options.presentation || "text") !== "text";
5673
+ // Members in the order they would win representation; the seed is the nearest.
5674
+ let ordered = [chosen, ...cluster.members.filter(member => member !== chosen)];
5675
+ if (iconic && chosen.options.cluster.representative !== "nearest") {
5676
+ ordered = ordered.map((member, i) => ({ member, i, priority: EntityLabel.IconPriority(member.options) }))
5677
+ .sort((a, b) => b.priority - a.priority || a.i - b.i).map(entry => entry.member);
5678
+ chosen = ordered[0];
5645
5679
  }
5646
5680
  const representative = result.get(chosen.id);
5647
5681
  representative.members = cluster.members.map(frame => frame.id);
5682
+ if (iconic && chosen.options.iconScale !== 0) {
5683
+ const seen = new Set();
5684
+ const icons = [];
5685
+ for (const member of ordered) {
5686
+ const look = iconLook(member.options);
5687
+ const key = JSON.stringify(look);
5688
+ if (seen.has(key)) {
5689
+ continue;
5690
+ }
5691
+ seen.add(key);
5692
+ if (member !== ordered[0])
5693
+ icons.push(look);
5694
+ if (seen.size >= ICON_STACK_LIMIT) {
5695
+ break;
5696
+ }
5697
+ }
5698
+ if (icons.length)
5699
+ representative.icons = icons;
5700
+ }
5648
5701
  representative.entityIds = Array.from(new Set(cluster.members.map(frame => frame.entityId).filter(Boolean)));
5649
5702
  if (chosen.options.presentation !== "icon") {
5650
5703
  const values = new Set();
@@ -5697,6 +5750,34 @@ var EntityLabel;
5697
5750
  return Array.from(result.values());
5698
5751
  }
5699
5752
  EntityLabel.Layout = Layout;
5753
+ /*
5754
+ * The seed plus every neighbour, in order, that shares a group or a look with each member accepted so far.
5755
+ *
5756
+ * Checking against the whole accepted set stops a group with mixed icons bridging two groups that look different.
5757
+ */
5758
+ function compatibleMembers(seed, nearby, group, look) {
5759
+ const accepted = [seed];
5760
+ const byGroup = new Map([[group(seed), 1]]);
5761
+ const byLook = new Map([[look(seed), 1]]);
5762
+ const byBoth = new Map([[JSON.stringify([group(seed), look(seed)]), 1]]);
5763
+ for (const item of nearby) {
5764
+ if (item === seed) {
5765
+ continue;
5766
+ }
5767
+ const itemGroup = group(item), itemLook = look(item), both = JSON.stringify([itemGroup, itemLook]);
5768
+ // Accepted members in another group with another look are the ones this item may not sit beside.
5769
+ const conflicting = accepted.length - (byGroup.get(itemGroup) || 0) - (byLook.get(itemLook) || 0) + (byBoth.get(both) || 0);
5770
+ if (conflicting > 0) {
5771
+ continue;
5772
+ }
5773
+ accepted.push(item);
5774
+ byGroup.set(itemGroup, (byGroup.get(itemGroup) || 0) + 1);
5775
+ byLook.set(itemLook, (byLook.get(itemLook) || 0) + 1);
5776
+ byBoth.set(both, (byBoth.get(both) || 0) + 1);
5777
+ }
5778
+ return accepted;
5779
+ }
5780
+ EntityLabel.compatibleMembers = compatibleMembers;
5700
5781
  const layers = new WeakMap();
5701
5782
  const containers = new WeakMap();
5702
5783
  /*
@@ -6094,7 +6175,7 @@ var EntityLabel;
6094
6175
  * @param starterPos3d optional position to create label at. Ignored for billboard labels.
6095
6176
  */
6096
6177
  async createLabel(starterPos3d) {
6097
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
6178
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
6098
6179
  this.removeLabel();
6099
6180
  let pos3d = starterPos3d ? starterPos3d.clone() : null;
6100
6181
  const counter = ++this.counter;
@@ -6416,125 +6497,28 @@ var EntityLabel;
6416
6497
  let iconElement;
6417
6498
  let iconContainer;
6418
6499
  let fitIconBackground;
6500
+ let iconStack;
6501
+ let stackedFits = [];
6502
+ let displayedStack = "[]";
6419
6503
  content.className = "entity-label-content";
6420
6504
  content.style.cssText = "display:flex;align-items:center;gap:6px;";
6421
6505
  if (presentation === "icon-text-above") {
6422
6506
  content.style.flexDirection = "column-reverse";
6423
6507
  }
6424
6508
  if (presentation !== "text") {
6425
- const icon = document.createElement("span");
6426
- iconElement = icon;
6427
- icon.className = "entity-label-icon";
6428
- icon.style.cssText = `font-size:${(this.options.iconSize || 24) * ((_m = this.options.iconScale) !== null && _m !== void 0 ? _m : 1)}px;line-height:1;`;
6429
- icon.style.textShadow = "0 1px 2px rgba(0, 0, 0, 0.25)";
6430
- const iconColor = typeof this.options.iconColor === "string" ? this.options.iconColor : undefined;
6431
- const iconBackground = this.options.iconBackground && this.options.iconScale !== 0;
6432
- if (iconColor) {
6433
- icon.style.color = iconColor;
6434
- }
6435
- if (this.options.iconUrl) {
6436
- const image = document.createElement("img");
6437
- image.draggable = false;
6438
- image.alt = this.options.icon || "";
6439
- const scale = (_o = this.options.iconScale) !== null && _o !== void 0 ? _o : 1;
6440
- const size = this.options.iconSize;
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);
6459
- }
6460
- const resize = () => {
6461
- if (this.disposed || this.counter !== counter) {
6462
- return;
6463
- }
6464
- const intrinsic = this.options.iconScale != null && size == null;
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
- }
6478
- this.viewer.scene.requestRender();
6479
- };
6480
- image.onload = resize;
6481
- resize();
6482
- image.src = this.options.iconUrl;
6483
- if (!tint) {
6484
- icon.appendChild(image);
6485
- }
6486
- }
6487
- else if (typeof this.options.iconClass === "string" && this.options.iconClass) {
6488
- icon.className = `entity-label-icon ${this.options.iconClass}`;
6489
- }
6490
- else {
6491
- icon.textContent = this.options.icon || "●";
6492
- }
6493
- if (iconBackground) {
6494
- const color = Color.fromCssColorString(iconColor || "white") || Color.WHITE;
6495
- const luminance = 0.2126 * color.red + 0.7152 * color.green + 0.0722 * color.blue;
6496
- const background = (luminance > 0.5 ? color.darken(ICON_BACKGROUND_BLEND, new Color()) :
6497
- color.brighten(ICON_BACKGROUND_BLEND, new Color()));
6498
- const size = (this.options.iconSize || 24) * ((_p = this.options.iconScale) !== null && _p !== void 0 ? _p : 1);
6499
- icon.style.backgroundColor = background.withAlpha(ICON_BACKGROUND_ALPHA).toCssColorString();
6500
- icon.style.borderRadius = "50%";
6501
- icon.style.padding = `${size / 3}px`;
6502
- icon.style.display = "inline-flex";
6503
- icon.style.alignItems = "center";
6504
- icon.style.justifyContent = "center";
6505
- icon.style.minWidth = `${size}px`;
6506
- icon.style.minHeight = `${size}px`;
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
- };
6529
- }
6530
- if (this.options.iconScale === 0) {
6531
- icon.style.display = "none";
6532
- }
6509
+ const built = this.buildIcon(this.options, counter);
6510
+ const icon = iconElement = built.icon;
6511
+ fitIconBackground = built.fit;
6533
6512
  makeClickable(icon);
6534
6513
  // Keep badges outside the glyph so font classes and image masks cannot affect them.
6535
6514
  iconContainer = document.createElement("span");
6536
- iconContainer.style.cssText = "position:relative;display:inline-flex;flex:none;";
6537
- iconContainer.appendChild(icon);
6515
+ iconContainer.style.cssText = "position:relative;display:inline-flex;flex:none;align-items:center;";
6516
+ icon.style.position = "relative";
6517
+ iconStack = document.createElement("span");
6518
+ iconStack.className = "entity-label-icon-stack";
6519
+ iconStack.style.cssText = "display:none;gap:2px;align-items:center;justify-items:center;flex:none;";
6520
+ makeClickable(iconStack);
6521
+ iconContainer.append(iconStack, icon);
6538
6522
  content.appendChild(iconContainer);
6539
6523
  }
6540
6524
  if (presentation !== "icon") {
@@ -6546,7 +6530,7 @@ var EntityLabel;
6546
6530
  if (presentation === "text")
6547
6531
  makeClickable(textElement);
6548
6532
  content.appendChild(textElement);
6549
- if ((_q = this.options.cluster) === null || _q === void 0 ? void 0 : _q.enabled) {
6533
+ if ((_m = this.options.cluster) === null || _m === void 0 ? void 0 : _m.enabled) {
6550
6534
  sourceText = document.createElement("span");
6551
6535
  sourceText.className = "entity-label-source-text";
6552
6536
  sourceText.setAttribute("aria-hidden", "true");
@@ -6587,7 +6571,7 @@ var EntityLabel;
6587
6571
  ele.appendChild(countElement);
6588
6572
  }
6589
6573
  ele.className = "entity-label";
6590
- ele.dataset.entityId = ((_r = this.rego) === null || _r === void 0 ? void 0 : _r.entityId) || "";
6574
+ ele.dataset.entityId = ((_o = this.rego) === null || _o === void 0 ? void 0 : _o.entityId) || "";
6591
6575
  ele.dataset.presentation = presentation;
6592
6576
  if (this.groupId) {
6593
6577
  ele.dataset.groupId = this.groupId;
@@ -6703,6 +6687,41 @@ var EntityLabel;
6703
6687
  run, side: (rightwards ? "right" : "left")
6704
6688
  };
6705
6689
  };
6690
+ /*
6691
+ * The on-screen centre and half-size of a point or billboard glyph, relative to its projected position.
6692
+ * Read every frame, since styling and selection can rescale a billboard in place.
6693
+ */
6694
+ const measureIcon = () => {
6695
+ var _a;
6696
+ if (!normalLeader || !(visual instanceof Entity)) {
6697
+ return null;
6698
+ }
6699
+ if (visual.point) {
6700
+ const size = EnsureNumber(getValue(this.viewer, visual.point.pixelSize), 1) +
6701
+ EnsureNumber(getValue(this.viewer, visual.point.outlineWidth), 0) * 2;
6702
+ return size > 0 ? { x: 0, y: 0, radius: size / 2 } : null;
6703
+ }
6704
+ if (!visual.billboard) {
6705
+ return null;
6706
+ }
6707
+ const billboard = visual.billboard;
6708
+ const scale = EnsureNumber(getValue(this.viewer, billboard.scale), 1);
6709
+ // Icon billboards only record their image height; generated circles set both sides.
6710
+ const height = EnsureNumber((_a = getValue(this.viewer, billboard.height)) !== null && _a !== void 0 ? _a : billboard._billboardSize, 0) * scale;
6711
+ const width = EnsureNumber(getValue(this.viewer, billboard.width), height / scale) * scale;
6712
+ if (!(height > 0) || !(width > 0)) {
6713
+ return null;
6714
+ }
6715
+ const vertical = getValue(this.viewer, billboard.verticalOrigin);
6716
+ const horizontal = getValue(this.viewer, billboard.horizontalOrigin);
6717
+ const offset = getValue(this.viewer, billboard.pixelOffset);
6718
+ return {
6719
+ x: ((offset === null || offset === void 0 ? void 0 : offset.x) || 0) + (horizontal === HorizontalOrigin.LEFT ? width / 2 : horizontal === HorizontalOrigin.RIGHT ? -width / 2 : 0),
6720
+ y: ((offset === null || offset === void 0 ? void 0 : offset.y) || 0) + (vertical === VerticalOrigin.BOTTOM || vertical === VerticalOrigin.BASELINE ? -height / 2 :
6721
+ vertical === VerticalOrigin.TOP ? height / 2 : 0),
6722
+ radius: Math.max(width, height) / 2
6723
+ };
6724
+ };
6706
6725
  let cursorMotion = this.cursorMotion;
6707
6726
  const project = () => {
6708
6727
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
@@ -6769,9 +6788,13 @@ var EntityLabel;
6769
6788
  this.cursorMotion = cursorMotion;
6770
6789
  const cursorAmount = cursorMotion.amount;
6771
6790
  leader = normalLeader || cursorAmount > 0;
6772
- let x = ((_l = transientPivot === null || transientPivot === void 0 ? void 0 : transientPivot.x) !== null && _l !== void 0 ? _l : anchor.x) + pixelOffset.x;
6773
- let y = ((_m = transientPivot === null || transientPivot === void 0 ? void 0 : transientPivot.y) !== null && _m !== void 0 ? _m : anchor.y) + pixelOffset.y;
6774
- if (cursorAmount > 0) {
6791
+ // An icon keeps its callout centred on the glyph rather than following the cursor across it.
6792
+ const icon = measureIcon();
6793
+ const iconRing = icon ? Math.max(ICON_RING_MIN, icon.radius + ICON_RING_PADDING) : 0;
6794
+ const pivot = icon ? undefined : transientPivot;
6795
+ let x = ((_l = pivot === null || pivot === void 0 ? void 0 : pivot.x) !== null && _l !== void 0 ? _l : anchor.x) + (icon || pixelOffset).x;
6796
+ let y = ((_m = pivot === null || pivot === void 0 ? void 0 : pivot.y) !== null && _m !== void 0 ? _m : anchor.y) + (icon || pixelOffset).y;
6797
+ if (cursorAmount > 0 && !icon) {
6775
6798
  // A leader-less label starts centred on its anchor; preserve that box as it lifts away.
6776
6799
  if (!normalLeader) {
6777
6800
  x -= ele.offsetWidth / layer.coordinates.scaleX / 2;
@@ -6809,9 +6832,11 @@ var EntityLabel;
6809
6832
  if (cursorAmount > 0) {
6810
6833
  leaderLength += (96 - leaderLength) * cursorAmount;
6811
6834
  }
6835
+ // Measured from the ring, so the visible stem is the same length whatever the glyph's size.
6836
+ leaderLength += iconRing;
6812
6837
  y = anchorY - leaderLength;
6813
6838
  }
6814
- else if (hover && !reused) {
6839
+ else if (hover && !reused && !icon) {
6815
6840
  x = hover.x + pixelOffset.x;
6816
6841
  y = hover.y + pixelOffset.y - 24;
6817
6842
  }
@@ -6863,6 +6888,7 @@ var EntityLabel;
6863
6888
  viewportWidth: this.viewer.canvas.clientWidth,
6864
6889
  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,
6865
6890
  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,
6891
+ iconRadius: icon === null || icon === void 0 ? void 0 : icon.radius,
6866
6892
  // Priority shows the hovered member individually without dragging its cluster along.
6867
6893
  opacity, priority: hover || cursorAmount > 0 || (state === null || state === void 0 ? void 0 : state.highlighted) ? 1 : 0, options: this.options, text: originalText, clusterBounds
6868
6894
  };
@@ -6870,7 +6896,7 @@ var EntityLabel;
6870
6896
  let displacement = { x: 0, y: 0 };
6871
6897
  let nudge = { x: 0, y: 0, fromX: 0, fromY: 0, targetX: 0, targetY: 0, start: 0 };
6872
6898
  const apply = (placement) => {
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;
6899
+ 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, _1, _2;
6874
6900
  const show = (placement === null || placement === void 0 ? void 0 : placement.visible) && placement.opacity > 0;
6875
6901
  clickEntityIds = show ? placement.entityIds : [];
6876
6902
  ele.style.visibility = show ? "visible" : "hidden";
@@ -6907,6 +6933,29 @@ var EntityLabel;
6907
6933
  this.viewer.scene.requestRender();
6908
6934
  }
6909
6935
  fitIconBackground === null || fitIconBackground === void 0 ? void 0 : fitIconBackground();
6936
+ const stack = JSON.stringify(placement.icons || []);
6937
+ if (iconStack && stack !== displayedStack) {
6938
+ displayedStack = stack;
6939
+ const looks = ((_b = placement.icons) === null || _b === void 0 ? void 0 : _b.length) ? [iconLook(this.options), ...placement.icons] : [];
6940
+ // One small size for all, so no icon in the arrangement reads as more important than another.
6941
+ const size = (this.options.iconSize || 24) * ((_c = this.options.iconScale) !== null && _c !== void 0 ? _c : 1) * ICON_STACK_SCALE;
6942
+ const built = looks.map(look => this.buildIcon({ ...look, iconSize: size }, counter));
6943
+ // Two sit side by side; three make a triangle with the representative on top.
6944
+ iconStack.style.gridTemplateColumns = built.length === 3 ? "auto auto" : `repeat(${built.length}, auto)`;
6945
+ if (built.length === 3) {
6946
+ built[0].icon.style.gridColumn = "1 / span 2";
6947
+ }
6948
+ iconStack.replaceChildren(...built.map(({ icon }) => icon));
6949
+ iconStack.style.display = built.length ? "inline-grid" : "none";
6950
+ // The glyph stays measurable, since cluster membership is taken from its footprint.
6951
+ iconElement.style.visibility = built.length ? "hidden" : "";
6952
+ iconElement.style.position = built.length ? "absolute" : "relative";
6953
+ stackedFits = built.map(({ fit }) => fit).filter(Boolean);
6954
+ // The stack widens the label, which the next frame has to measure.
6955
+ this.viewer.scene.requestRender();
6956
+ }
6957
+ for (const fit of stackedFits)
6958
+ fit();
6910
6959
  if (iconContainer) {
6911
6960
  const badgeWidth = countElement.offsetWidth;
6912
6961
  const iconWidth = iconElement.offsetWidth;
@@ -6922,36 +6971,43 @@ var EntityLabel;
6922
6971
  // Summary and badge changes must fit on their first visible frame, including after resize.
6923
6972
  const ring = this.options.anchorRing;
6924
6973
  const pointer = (layer.pointer || (cursorMotion.target ? cursorMotion.position : undefined) || (this.Transient && !this.Persistent ?
6925
- (this.hoverFade.to === 0 ? this.retainedHoverPosition : (_b = this.visualRegister) === null || _b === void 0 ? void 0 : _b.GetLabelHoverPosition((_c = this.rego) === null || _c === void 0 ? void 0 : _c.entityId, (_d = this.rego) === null || _d === void 0 ? void 0 : _d.menuItemId)) : undefined));
6926
- const hovered = pointer && Math.hypot(pointer.x - frame.anchorX, pointer.y - frame.anchorY) <= ((_e = ring === null || ring === void 0 ? void 0 : ring.hoverDistance) !== null && _e !== void 0 ? _e : 24);
6927
- const normalRadius = (!leader || (ring === null || ring === void 0 ? void 0 : ring.enabled) === false ? 0 : Math.max(frame.distance <= ((_f = ring === null || ring === void 0 ? void 0 : ring.nearDistance) !== null && _f !== void 0 ? _f : 100) ? ((_g = ring === null || ring === void 0 ? void 0 : ring.nearRadius) !== null && _g !== void 0 ? _g : 10) : 0, hovered ? ((_h = ring === null || ring === void 0 ? void 0 : ring.hoverRadius) !== null && _h !== void 0 ? _h : (this.Transient && !this.Persistent ? 15.4 : 22)) : 0));
6974
+ (this.hoverFade.to === 0 ? this.retainedHoverPosition : (_d = this.visualRegister) === null || _d === void 0 ? void 0 : _d.GetLabelHoverPosition((_e = this.rego) === null || _e === void 0 ? void 0 : _e.entityId, (_f = this.rego) === null || _f === void 0 ? void 0 : _f.menuItemId)) : undefined));
6975
+ const iconRing = frame.iconRadius ? Math.max(ICON_RING_MIN, frame.iconRadius + ICON_RING_PADDING) : 0;
6976
+ const hovered = pointer && Math.hypot(pointer.x - frame.anchorX, pointer.y - frame.anchorY) <= Math.max((_g = ring === null || ring === void 0 ? void 0 : ring.hoverDistance) !== null && _g !== void 0 ? _g : 24, iconRing);
6977
+ const normalRadius = (!leader || (ring === null || ring === void 0 ? void 0 : ring.enabled) === false ? 0 : Math.max(frame.distance <= ((_h = ring === null || ring === void 0 ? void 0 : ring.nearDistance) !== null && _h !== void 0 ? _h : 100) ? ((_j = ring === null || ring === void 0 ? void 0 : ring.nearRadius) !== null && _j !== void 0 ? _j : 10) : 0, hovered ? ((_k = ring === null || ring === void 0 ? void 0 : ring.hoverRadius) !== null && _k !== void 0 ? _k : (this.Transient && !this.Persistent ? 15.4 : 22)) : 0));
6928
6978
  const targetRadius = normalRadius + (15.4 - normalRadius) * cursorMotion.amount;
6979
+ // Around a glyph the ring keeps the glyph's size and only fades its stroke in, so it never covers the icon.
6980
+ const targetOuter = iconRing || targetRadius || 2.5;
6981
+ const targetInner = iconRing ? (targetRadius > 0 ? iconRing - 2 : iconRing) : Math.max(0, targetRadius - 2);
6929
6982
  const now = Date.now();
6930
- const duration = (_j = ring === null || ring === void 0 ? void 0 : ring.transitionDuration) !== null && _j !== void 0 ? _j : 200;
6983
+ const duration = (_l = ring === null || ring === void 0 ? void 0 : ring.transitionDuration) !== null && _l !== void 0 ? _l : 200;
6931
6984
  const sharedRing = this.Transient && !this.Persistent && this.hoverFade.to === 1 ? this.hoverRingState : undefined;
6932
6985
  let ringAnimation = sharedRing ? sharedRing.animation : this.ringAnimation;
6933
6986
  if (!ringAnimation) {
6934
- ringAnimation = { target: 0, start: now, outer: 2.5, inner: 0, fromOuter: 2.5, fromInner: 0 };
6987
+ const outer = iconRing || 2.5;
6988
+ const inner = iconRing;
6989
+ ringAnimation = { target: outer, targetInner: inner, start: now, outer, inner, fromOuter: outer, fromInner: inner };
6935
6990
  }
6936
6991
  const animateRing = () => {
6937
6992
  const progress = duration > 0 ? Math.min(1, Math.max(0, (now - ringAnimation.start) / duration)) : 1;
6938
6993
  const eased = progress * progress * (3 - 2 * progress);
6939
- ringAnimation.outer = ringAnimation.fromOuter + ((ringAnimation.target || 2.5) - ringAnimation.fromOuter) * eased;
6940
- ringAnimation.inner = ringAnimation.fromInner + (Math.max(0, ringAnimation.target - 2) - ringAnimation.fromInner) * eased;
6994
+ ringAnimation.outer = ringAnimation.fromOuter + (ringAnimation.target - ringAnimation.fromOuter) * eased;
6995
+ ringAnimation.inner = ringAnimation.fromInner + (ringAnimation.targetInner - ringAnimation.fromInner) * eased;
6941
6996
  return progress;
6942
6997
  };
6943
6998
  animateRing();
6944
- if (ringAnimation.target !== targetRadius) {
6999
+ if (ringAnimation.target !== targetOuter || ringAnimation.targetInner !== targetInner) {
6945
7000
  ringAnimation = {
6946
7001
  ...ringAnimation,
6947
- target: targetRadius,
7002
+ target: targetOuter,
7003
+ targetInner,
6948
7004
  start: now,
6949
7005
  fromOuter: ringAnimation.outer,
6950
7006
  fromInner: ringAnimation.inner
6951
7007
  };
6952
7008
  }
6953
7009
  // Animate the hole and stem together; no CSS transition may lag behind a moving world anchor.
6954
- if (animateRing() < 1 && (ringAnimation.fromOuter !== (targetRadius || 2.5) || ringAnimation.fromInner !== Math.max(0, targetRadius - 2))) {
7010
+ if (animateRing() < 1 && (ringAnimation.fromOuter !== targetOuter || ringAnimation.fromInner !== targetInner)) {
6955
7011
  this.viewer.scene.requestRender();
6956
7012
  }
6957
7013
  this.ringAnimation = ringAnimation;
@@ -6966,19 +7022,19 @@ var EntityLabel;
6966
7022
  if (leader && angled) {
6967
7023
  // The declutter owns the side; the fit only says which one had the room to begin with.
6968
7024
  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";
7025
+ const side = (_o = (_m = placement.textSide) !== null && _m !== void 0 ? _m : frame.leaderSide) !== null && _o !== void 0 ? _o : "right";
6970
7026
  const beside = side === "left" ? frame.anchorX - run - width : frame.anchorX + run;
6971
7027
  position.x = Math.max(0, Math.min(beside, this.viewer.canvas.clientWidth - width));
6972
7028
  }
6973
7029
  else if (leader && placement.textSide === "left") {
6974
7030
  position.x = Math.max(0, Math.min(frame.anchorX - width, this.viewer.canvas.clientWidth - width));
6975
7031
  }
6976
- const gap = (_o = (_m = this.options.pointerAvoidance) === null || _m === void 0 ? void 0 : _m.gap) !== null && _o !== void 0 ? _o : 12;
7032
+ const gap = (_q = (_p = this.options.pointerAvoidance) === null || _p === void 0 ? void 0 : _p.gap) !== null && _q !== void 0 ? _q : 12;
6977
7033
  const containsPointer = (x, y, margin) => {
6978
7034
  return pointer && pointer.x >= x - margin && pointer.x <= x + width + margin && pointer.y >= y - margin && pointer.y <= y + height + margin;
6979
7035
  };
6980
7036
  const passive = !selectOnClick || !this.visualRegister;
6981
- if (passive && ((_p = this.options.pointerAvoidance) === null || _p === void 0 ? void 0 : _p.enabled) !== false && pointer &&
7037
+ if (passive && ((_r = this.options.pointerAvoidance) === null || _r === void 0 ? void 0 : _r.enabled) !== false && pointer &&
6982
7038
  (containsPointer(position.x, position.y, gap) || containsPointer(position.x + displacement.x, position.y + displacement.y, gap))) {
6983
7039
  if (containsPointer(position.x + displacement.x, position.y + displacement.y, 0)) {
6984
7040
  // Choose against the original box and retain the offset until the pointer leaves both boxes.
@@ -6996,7 +7052,7 @@ var EntityLabel;
6996
7052
  else {
6997
7053
  displacement = { x: 0, y: 0 };
6998
7054
  }
6999
- const nudgeDuration = (_r = (_q = this.options.pointerAvoidance) === null || _q === void 0 ? void 0 : _q.transitionDuration) !== null && _r !== void 0 ? _r : 200;
7055
+ const nudgeDuration = (_t = (_s = this.options.pointerAvoidance) === null || _s === void 0 ? void 0 : _s.transitionDuration) !== null && _t !== void 0 ? _t : 200;
7000
7056
  const animateNudge = () => {
7001
7057
  const progress = now < nudge.start ? 0 : nudgeDuration > 0 ? Math.min(1, (now - nudge.start) / nudgeDuration) : 1;
7002
7058
  const eased = progress * progress * (3 - 2 * progress);
@@ -7012,7 +7068,7 @@ var EntityLabel;
7012
7068
  fromY: nudge.y,
7013
7069
  targetX: displacement.x,
7014
7070
  targetY: displacement.y,
7015
- start: now + ((_t = (_s = this.options.pointerAvoidance) === null || _s === void 0 ? void 0 : _s.delay) !== null && _t !== void 0 ? _t : 180)
7071
+ start: now + ((_v = (_u = this.options.pointerAvoidance) === null || _u === void 0 ? void 0 : _u.delay) !== null && _v !== void 0 ? _v : 180)
7016
7072
  };
7017
7073
  }
7018
7074
  if (animateNudge() < 1 && (nudge.fromX !== nudge.targetX || nudge.fromY !== nudge.targetY)) {
@@ -7022,9 +7078,8 @@ var EntityLabel;
7022
7078
  position.y += nudge.y;
7023
7079
  ele.style.left = `${coordinates.x + position.x * coordinates.scaleX}px`;
7024
7080
  ele.style.top = `${coordinates.y + position.y * coordinates.scaleY}px`;
7025
- const clickTarget = content.querySelector('[role="button"]');
7026
- if (clickTarget) {
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}`);
7081
+ for (const clickTarget of Array.from(content.querySelectorAll('[role="button"]'))) {
7082
+ clickTarget.setAttribute("aria-label", placement.members.length ? `Select ${placement.entityIds.length} entities` : `Select ${(_x = (_w = this.options.text) !== null && _w !== void 0 ? _w : text) !== null && _x !== void 0 ? _x : (_y = this.rego) === null || _y === void 0 ? void 0 : _y.entityId}`);
7028
7083
  }
7029
7084
  if (leader) {
7030
7085
  const nodeRadius = ringAnimation.outer;
@@ -7064,13 +7119,13 @@ var EntityLabel;
7064
7119
  // Keep the stem inside the 126-degree arc, away from antialiased endpoints.
7065
7120
  const arcFraction = 0.35;
7066
7121
  const rotationLimit = Math.PI * arcFraction - Math.PI / 30;
7067
- const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((_x = ringAnimation.arcOffset) !== null && _x !== void 0 ? _x : 0);
7122
+ const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((_z = ringAnimation.arcOffset) !== null && _z !== void 0 ? _z : 0);
7068
7123
  const targetOpen = cursorMotion.target || (hovered && (ring === null || ring === void 0 ? void 0 : ring.enabled) !== false && targetRadius > 0) ? 1 : 0;
7069
- const elapsed = Math.min(32, Math.max(0, now - ((_y = ringAnimation.arcTime) !== null && _y !== void 0 ? _y : now)));
7124
+ const elapsed = Math.min(32, Math.max(0, now - ((_0 = ringAnimation.arcTime) !== null && _0 !== void 0 ? _0 : now)));
7070
7125
  const blend = duration > 0 ? 1 - Math.exp(-elapsed / (duration / 3)) : 1;
7071
7126
  const approach = (value, target) => Math.abs(value - target) < 0.001 ? target : value + (target - value) * blend;
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);
7127
+ ringAnimation.arcOffset = approach((_1 = ringAnimation.arcOffset) !== null && _1 !== void 0 ? _1 : 0, targetOffset);
7128
+ ringAnimation.arcOpen = approach((_2 = ringAnimation.arcOpen) !== null && _2 !== void 0 ? _2 : 0, targetOpen);
7074
7129
  ringAnimation.arcTime = now;
7075
7130
  if (ringAnimation.arcOffset !== targetOffset || ringAnimation.arcOpen !== targetOpen) {
7076
7131
  this.viewer.scene.requestRender();
@@ -7123,6 +7178,123 @@ var EntityLabel;
7123
7178
  }, 2500);
7124
7179
  });
7125
7180
  }
7181
+ /*
7182
+ * Builds one icon glyph from label options, with its background chip when asked for.
7183
+ *
7184
+ * The returned fit squares the chip off once the glyph has a measurable size.
7185
+ */
7186
+ buildIcon(options, counter) {
7187
+ var _a, _b, _c;
7188
+ let fit;
7189
+ const icon = document.createElement("span");
7190
+ icon.className = "entity-label-icon";
7191
+ icon.style.cssText = `font-size:${(options.iconSize || 24) * ((_a = options.iconScale) !== null && _a !== void 0 ? _a : 1)}px;line-height:1;`;
7192
+ icon.style.textShadow = "0 1px 2px rgba(0, 0, 0, 0.25)";
7193
+ const iconColor = typeof options.iconColor === "string" ? options.iconColor : undefined;
7194
+ const iconBackground = options.iconBackground && options.iconScale !== 0;
7195
+ if (iconColor) {
7196
+ icon.style.color = iconColor;
7197
+ }
7198
+ if (options.iconUrl) {
7199
+ const image = document.createElement("img");
7200
+ image.draggable = false;
7201
+ image.alt = options.icon || "";
7202
+ const scale = (_b = options.iconScale) !== null && _b !== void 0 ? _b : 1;
7203
+ const size = options.iconSize;
7204
+ const svgNs = "http://www.w3.org/2000/svg";
7205
+ const tint = iconColor ? document.createElementNS(svgNs, "svg") : null;
7206
+ const painted = tint ? document.createElementNS(svgNs, "image") : null;
7207
+ const inked = tint ? document.createElementNS(svgNs, "rect") : null;
7208
+ if (tint) {
7209
+ const maskId = `entity-label-tint-${++tintCounter}`;
7210
+ const mask = document.createElementNS(svgNs, "mask");
7211
+ mask.setAttribute("id", maskId);
7212
+ mask.setAttribute("maskUnits", "userSpaceOnUse");
7213
+ painted.setAttribute("href", options.iconUrl);
7214
+ painted.setAttribute("preserveAspectRatio", "xMidYMid meet");
7215
+ mask.appendChild(painted);
7216
+ inked.setAttribute("fill", iconColor);
7217
+ inked.setAttribute("mask", `url(#${maskId})`);
7218
+ tint.append(mask, inked);
7219
+ tint.style.display = "block";
7220
+ icon.style.display = "inline-flex";
7221
+ icon.appendChild(tint);
7222
+ }
7223
+ const resize = () => {
7224
+ if (this.disposed || this.counter !== counter) {
7225
+ return;
7226
+ }
7227
+ const intrinsic = options.iconScale != null && size == null;
7228
+ const width = (intrinsic ? image.naturalWidth || 24 : size || 24) * scale;
7229
+ const height = (intrinsic ? image.naturalHeight || 24 : size || 24) * scale;
7230
+ if (tint) {
7231
+ tint.setAttribute("viewBox", `0 0 ${width} ${height}`);
7232
+ for (const node of [tint, painted, inked]) {
7233
+ node.setAttribute("width", String(width));
7234
+ node.setAttribute("height", String(height));
7235
+ }
7236
+ }
7237
+ else {
7238
+ image.style.width = `${width}px`;
7239
+ image.style.height = `${height}px`;
7240
+ }
7241
+ this.viewer.scene.requestRender();
7242
+ };
7243
+ image.onload = resize;
7244
+ resize();
7245
+ image.src = options.iconUrl;
7246
+ if (!tint) {
7247
+ icon.appendChild(image);
7248
+ }
7249
+ }
7250
+ else if (typeof options.iconClass === "string" && options.iconClass) {
7251
+ icon.className = `entity-label-icon ${options.iconClass}`;
7252
+ }
7253
+ else {
7254
+ icon.textContent = options.icon || "●";
7255
+ }
7256
+ if (iconBackground) {
7257
+ const color = Color.fromCssColorString(iconColor || "white") || Color.WHITE;
7258
+ const luminance = 0.2126 * color.red + 0.7152 * color.green + 0.0722 * color.blue;
7259
+ const background = (luminance > 0.5 ? color.darken(ICON_BACKGROUND_BLEND, new Color()) :
7260
+ color.brighten(ICON_BACKGROUND_BLEND, new Color()));
7261
+ const size = (options.iconSize || 24) * ((_c = options.iconScale) !== null && _c !== void 0 ? _c : 1);
7262
+ icon.style.backgroundColor = background.withAlpha(ICON_BACKGROUND_ALPHA).toCssColorString();
7263
+ icon.style.borderRadius = "50%";
7264
+ icon.style.padding = `${size / 3}px`;
7265
+ icon.style.display = "inline-flex";
7266
+ icon.style.alignItems = "center";
7267
+ icon.style.justifyContent = "center";
7268
+ icon.style.minWidth = `${size}px`;
7269
+ icon.style.minHeight = `${size}px`;
7270
+ icon.style.boxSizing = "content-box";
7271
+ let fitted = false;
7272
+ fit = () => {
7273
+ if (fitted) {
7274
+ return;
7275
+ }
7276
+ const padding = size / 3;
7277
+ const width = icon.offsetWidth - padding * 2;
7278
+ const height = icon.offsetHeight - padding * 2;
7279
+ if (width <= 0 || height <= 0) {
7280
+ return;
7281
+ }
7282
+ fitted = true;
7283
+ const longest = Math.max(width, height);
7284
+ const difference = (longest - Math.min(width, height)) / longest;
7285
+ if (difference > ICON_BACKGROUND_UNIFORM_TOLERANCE) {
7286
+ return;
7287
+ }
7288
+ icon.style.minWidth = `${longest}px`;
7289
+ icon.style.minHeight = `${longest}px`;
7290
+ this.viewer.scene.requestRender();
7291
+ };
7292
+ }
7293
+ if (options.iconScale === 0) {
7294
+ icon.style.display = "none";
7295
+ }
7296
+ return { icon, fit };
7297
+ }
7126
7298
  /**
7127
7299
  * Removes label from the viewer and unsubscribes from events.
7128
7300
  */
@@ -16796,9 +16968,6 @@ class EntityGatherer {
16796
16968
  // Last date-time range taken from the clock.
16797
16969
  this.lastDateTimeMin = null;
16798
16970
  this.lastDateTimeMax = null;
16799
- // Entity ID -> last resolved date time.
16800
- // Helps determine if a slow resolved request can still be applied while we wait for the next one.
16801
- this.lastFoundDateTimes = new Map();
16802
16971
  // ID of Entity IDs that need to be requested.
16803
16972
  this.eIdQueue = [];
16804
16973
  // Membership mirror of eIdQueue for quick checks.
@@ -16830,8 +16999,12 @@ class EntityGatherer {
16830
16999
  // Unsubscribes the view monitor listener that queues what the camera has moved onto.
16831
17000
  this.monitorRemoval = null;
16832
17001
  // Indicates a tick is active.
16833
- // When true, we won't bother calling onTick again.
17002
+ // When true, a new tick is not started; it is noted in pendingTick and run once this one ends.
16834
17003
  this.runningTick = false;
17004
+ // A tick was asked for while one was running, so another is owed once it finishes.
17005
+ this.pendingTick = false;
17006
+ // The debounce every clock change goes through, kept so a tick owed after a running one does too.
17007
+ this.tickDelay = null;
16835
17008
  // Entity ID -> Entity blob.
16836
17009
  // This is a tool that lets developers set their own JSON blobs instead of requesting data from API.
16837
17010
  this.entityDataOverrides = new Map();
@@ -16895,11 +17068,13 @@ class EntityGatherer {
16895
17068
  let tickDelay = new DelayQueue(() => {
16896
17069
  this.onTick(false);
16897
17070
  }, 250, true);
17071
+ this.tickDelay = tickDelay;
16898
17072
  this.hDisposals.push(() => {
16899
17073
  if (tickDelay) {
16900
17074
  tickDelay.Dispose();
16901
17075
  }
16902
17076
  tickDelay = null;
17077
+ this.tickDelay = null;
16903
17078
  });
16904
17079
  // React to clock changes and request new Entities.
16905
17080
  let prevTime = null;
@@ -16939,7 +17114,6 @@ class EntityGatherer {
16939
17114
  this.priorityCount = 0;
16940
17115
  this.lastOrderFocus = null;
16941
17116
  this.allEIds.clear();
16942
- this.lastFoundDateTimes.clear();
16943
17117
  this.everFound.clear();
16944
17118
  this.clearIndex();
16945
17119
  this.lastDateTime = null;
@@ -17301,7 +17475,6 @@ class EntityGatherer {
17301
17475
  this.eIdQueued.clear();
17302
17476
  this.priorityCount = 0;
17303
17477
  this.allEIds.clear();
17304
- this.lastFoundDateTimes.clear();
17305
17478
  this.everFound.clear();
17306
17479
  this.clearIndex();
17307
17480
  this.entityDataOverrides.clear();
@@ -17317,7 +17490,13 @@ class EntityGatherer {
17317
17490
  }
17318
17491
  }
17319
17492
  onTick(force) {
17320
- if (this.runningTick || this.disposed) {
17493
+ if (this.disposed) {
17494
+ return;
17495
+ }
17496
+ if (this.runningTick) {
17497
+ if (this.historic) {
17498
+ this.pendingTick = true;
17499
+ }
17321
17500
  return;
17322
17501
  }
17323
17502
  const rTime = this.viewer.clock.currentTime.toString();
@@ -17325,10 +17504,16 @@ class EntityGatherer {
17325
17504
  return;
17326
17505
  }
17327
17506
  this.lastDateTime = rTime;
17507
+ // Whether the clock has left the moment this tick is reading.
17508
+ const movedOn = () => this.historic && this.viewer.clock.currentTime.toString() !== rTime;
17328
17509
  (async () => {
17329
17510
  this.runningTick = true;
17511
+ this.pendingTick = false;
17330
17512
  this.placeStragglers();
17331
17513
  let rerun = false;
17514
+ let interrupted = false;
17515
+ // Batch's answer is applied while the next batch is being read.
17516
+ const applying = [];
17332
17517
  try {
17333
17518
  const handleResponse = async (requestedBatch, entities) => {
17334
17519
  var _a, _b, _c, _d;
@@ -17392,45 +17577,7 @@ class EntityGatherer {
17392
17577
  api: this.api
17393
17578
  })).tags;
17394
17579
  }
17395
- // 0 = rTime is still the current time.
17396
- // 1 = rTime is in the future.
17397
- // -1 = rTime is in the past.
17398
- let changeDir = 0;
17399
- if (this.lastDateTime !== this.viewer.clock.currentTime.toString()) {
17400
- changeDir = this.lastDateTime < this.viewer.clock.currentTime.toString() ? 1 : -1;
17401
- }
17402
- let toEmit;
17403
- if (this.historic) {
17404
- // We can emit Entities if the last resolved time (per Entity) isn't newer than rTime.
17405
- // We compare using the changeDir to determine if we should emit or not.
17406
- toEmit = resolvedEntities.filter((e) => {
17407
- var _a;
17408
- 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);
17409
- const lastFoundDateTime = this.lastFoundDateTimes.get(entityId);
17410
- if (lastFoundDateTime) {
17411
- if (changeDir === 1) {
17412
- return lastFoundDateTime <= rTime;
17413
- }
17414
- else if (changeDir === -1) {
17415
- return lastFoundDateTime >= rTime;
17416
- }
17417
- }
17418
- return true;
17419
- });
17420
- // Update the last found date times for the filtered Entities.
17421
- toEmit.forEach((e) => {
17422
- var _a;
17423
- 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);
17424
- if (entityId) {
17425
- this.lastFoundDateTimes.set(entityId, rTime);
17426
- }
17427
- });
17428
- }
17429
- else {
17430
- // If we're not historic, we can just emit everything.
17431
- toEmit = resolvedEntities;
17432
- }
17433
- this.emitEntities(toEmit, tags);
17580
+ this.emitEntities(resolvedEntities, tags);
17434
17581
  };
17435
17582
  const QUEUE_BATCH_SIZE = 500;
17436
17583
  const requestedIds = new Set();
@@ -17439,7 +17586,8 @@ class EntityGatherer {
17439
17586
  const total = this.eIdQueue.length;
17440
17587
  let done = 0;
17441
17588
  while (this.eIdQueue.length && !this.disposed) {
17442
- if (this.historic && this.lastDateTime !== rTime) {
17589
+ if (movedOn()) {
17590
+ interrupted = true;
17443
17591
  break;
17444
17592
  }
17445
17593
  this.orderQueueByCamera();
@@ -17455,7 +17603,7 @@ class EntityGatherer {
17455
17603
  api: this.api,
17456
17604
  maxSearchTimeSec: 60 * 2
17457
17605
  });
17458
- handleResponse(batch, entities);
17606
+ applying.push(handleResponse(batch, entities));
17459
17607
  for (let i = 0; i < batch.length; i++) {
17460
17608
  requestedIds.add(batch[i]);
17461
17609
  }
@@ -17487,7 +17635,8 @@ class EntityGatherer {
17487
17635
  // The near ones are resolved first out of whatever the view still reaches.
17488
17636
  allEIds = this.bandIds(allEIds);
17489
17637
  while (allEIds.length && !this.disposed && !this.eIdQueue.length) {
17490
- if (this.historic && this.lastDateTime !== rTime) {
17638
+ if (movedOn()) {
17639
+ interrupted = true;
17491
17640
  break;
17492
17641
  }
17493
17642
  const batch = allEIds.splice(0, QUEUE_BATCH_SIZE);
@@ -17498,7 +17647,7 @@ class EntityGatherer {
17498
17647
  api: this.api,
17499
17648
  maxSearchTimeSec: 60 * 2
17500
17649
  });
17501
- handleResponse(batch, entities);
17650
+ applying.push(handleResponse(batch, entities));
17502
17651
  for (let i = 0; i < batch.length; i++) {
17503
17652
  requestedIds.add(batch[i]);
17504
17653
  }
@@ -17513,10 +17662,32 @@ class EntityGatherer {
17513
17662
  console.error(e);
17514
17663
  }
17515
17664
  finally {
17665
+ // Only a historic answer can be overtaken by a newer moment, so nothing else waits.
17666
+ if (this.historic && applying.length) {
17667
+ await Promise.all(applying.map((pending) => pending.catch((e) => console.error(e))));
17668
+ }
17516
17669
  this.runningTick = false;
17517
17670
  }
17518
- if (rerun) {
17671
+ // A moment read only in part is forgotten, so coming back to it reads it again in full.
17672
+ if (interrupted && this.lastDateTime === rTime) {
17673
+ this.lastDateTime = null;
17674
+ }
17675
+ if (this.disposed) {
17676
+ return;
17677
+ }
17678
+ // Leftovers at the same moment carry straight on, as they always have.
17679
+ if (rerun && !interrupted) {
17519
17680
  this.onTick(true);
17681
+ return;
17682
+ }
17683
+ if (interrupted || this.pendingTick || movedOn()) {
17684
+ this.pendingTick = false;
17685
+ if (this.tickDelay) {
17686
+ this.tickDelay.Call();
17687
+ }
17688
+ else {
17689
+ this.onTick(false);
17690
+ }
17520
17691
  }
17521
17692
  })();
17522
17693
  }
@@ -49206,7 +49377,7 @@ var EntityRenderEngine;
49206
49377
  }
49207
49378
  EntityRenderEngine.GetGraphicShapeId = GetGraphicShapeId;
49208
49379
  async function Render(params) {
49209
- var _a, _b, _c;
49380
+ var _a, _b, _c, _d, _e;
49210
49381
  const groupRenderParams = {
49211
49382
  apiGetter: params.apiGetter,
49212
49383
  viewer: params.viewer,
@@ -49383,10 +49554,20 @@ var EntityRenderEngine;
49383
49554
  polygons.push(entity);
49384
49555
  continue;
49385
49556
  }
49557
+ // Present when the gate above chose to reuse this entity's graphic.
49558
+ const existingRoot = cEntities.get(entity.Bruce.ID);
49559
+ const existingParts = new Map();
49560
+ (_d = existingRoot === null || existingRoot === void 0 ? void 0 : existingRoot._siblingGraphics) === null || _d === void 0 ? void 0 : _d.forEach((part) => {
49561
+ if (part._multiPartIndex != null) {
49562
+ existingParts.set(part._multiPartIndex, part);
49563
+ }
49564
+ });
49565
+ const partRendered = new Map();
49566
+ const partIndexes = new Map();
49386
49567
  const pParams = {
49387
49568
  ...groupRenderParams,
49388
49569
  entities: [],
49389
- rendered: cEntities
49570
+ rendered: partRendered
49390
49571
  };
49391
49572
  pParams.entitiesHistoric = params.entitiesHistoric;
49392
49573
  const zoomItem = pParams.zoomItems[entity.Bruce.ID];
@@ -49402,39 +49583,59 @@ var EntityRenderEngine;
49402
49583
  };
49403
49584
  pParams.zoomItems[subEntity.Bruce.ID] = zoomItem;
49404
49585
  pParams.entities.push(subEntity);
49405
- }
49406
- const cPoly = await Polygon.RenderGroup(pParams);
49407
- Array.from(cPoly.keys()).forEach((key) => {
49408
- if (cPoly.get(key)) {
49409
- pParams.entities = pParams.entities.filter((e) => e.Bruce.ID != key);
49410
- }
49411
- });
49412
- let rendered = Array.from(cPoly.values());
49413
- const cLines = await Polyline.RenderGroup(pParams);
49414
- Array.from(cLines.keys()).forEach((key) => {
49415
- if (cLines.get(key)) {
49586
+ partIndexes.set(subEntity.Bruce.ID, j);
49587
+ const existingPart = existingParts.get(j);
49588
+ if (existingPart) {
49589
+ // An engine preparing a reused graphic gathers everything connected to it, so a part still
49590
+ // attached to its root would have every other part evicted from the scene.
49591
+ existingPart._parentEntity = null;
49592
+ partRendered.set(subEntity.Bruce.ID, existingPart);
49593
+ }
49594
+ }
49595
+ const rendered = [];
49596
+ const collect = (results) => {
49597
+ results.forEach((cEntity, key) => {
49598
+ if (!cEntity) {
49599
+ return;
49600
+ }
49601
+ cEntity._multiPartIndex = partIndexes.get(key);
49602
+ rendered.push(cEntity);
49416
49603
  pParams.entities = pParams.entities.filter((e) => e.Bruce.ID != key);
49417
- }
49418
- });
49419
- rendered = rendered.concat(Array.from(cLines.values()));
49604
+ });
49605
+ };
49606
+ collect(await Polygon.RenderGroup(pParams));
49607
+ collect(await Polyline.RenderGroup(pParams));
49420
49608
  // We'll only render points when there's no other geometry.
49421
49609
  // Generally speaking when polygons/polylines are drawn, points are only drawn because of invalid data.
49422
49610
  if (!rendered.length) {
49423
- const cPoints = await Point.RenderGroup(pParams);
49424
- rendered = rendered.concat(Array.from(cPoints.values()));
49611
+ collect(await Point.RenderGroup(pParams));
49425
49612
  }
49426
- rendered = rendered.filter(x => x != null);
49427
49613
  if (rendered.length) {
49428
- const rootEntity = new Entity({
49429
- id: ObjectUtils.UId(15)
49430
- });
49431
- rootEntity._siblingGraphics = [];
49614
+ const sameParts = ((_e = existingRoot === null || existingRoot === void 0 ? void 0 : existingRoot._siblingGraphics) === null || _e === void 0 ? void 0 : _e.length) == rendered.length &&
49615
+ rendered.every((part) => existingRoot._siblingGraphics.includes(part));
49616
+ let rootEntity;
49617
+ if (sameParts) {
49618
+ rootEntity = existingRoot;
49619
+ }
49620
+ else {
49621
+ if (existingRoot === null || existingRoot === void 0 ? void 0 : existingRoot._siblingGraphics) {
49622
+ // Retiring the old root removes its parts, and the ones carried over belong to the new root now.
49623
+ existingRoot._siblingGraphics = existingRoot._siblingGraphics.filter((part) => !rendered.includes(part));
49624
+ }
49625
+ rootEntity = new Entity({
49626
+ id: ObjectUtils.UId(15)
49627
+ });
49628
+ }
49629
+ rootEntity._siblingGraphics = rendered;
49432
49630
  rootEntity._renderGroup = GetRenderGroupId(zoomItem);
49433
- rootEntity._siblingGraphics = rootEntity._siblingGraphics.concat(rendered);
49631
+ // The skip gate compares the rego's style against the zoom item's, and the rego takes it from here.
49632
+ rootEntity.styleId = (zoomItem === null || zoomItem === void 0 ? void 0 : zoomItem.StyleID) == -1 ? -1 : (+(zoomItem === null || zoomItem === void 0 ? void 0 : zoomItem.StyleID) || null);
49633
+ rootEntity.styleEffective = rendered[0].styleEffective;
49434
49634
  cEntities.set(entity.Bruce.ID, rootEntity);
49435
- const firstEntity = rendered[0];
49436
- if (firstEntity) {
49437
- rootEntity.position = GetCValue(params.viewer, firstEntity.position);
49635
+ const newPosition = GetCValue(params.viewer, rendered[0].position);
49636
+ const oldPosition = GetCValue(params.viewer, rootEntity.position);
49637
+ if (newPosition && (!oldPosition || !Cartes.IsEqualCartes3(oldPosition, newPosition))) {
49638
+ rootEntity.position = new ConstantPositionProperty(newPosition);
49438
49639
  }
49439
49640
  rendered.forEach((child) => {
49440
49641
  child._parentEntity = rootEntity;
@@ -50371,7 +50572,7 @@ var StyleUtils;
50371
50572
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
50372
50573
  })(StyleUtils || (StyleUtils = {}));
50373
50574
 
50374
- const VERSION = "7.4.2";
50575
+ const VERSION = "7.4.4";
50375
50576
  /**
50376
50577
  * Updates the environment instance used by bruce-cesium to one specified.
50377
50578
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.