bruce-cesium 7.3.9 โ†’ 7.4.1

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.
Files changed (27) hide show
  1. package/dist/bruce-cesium.es5.js +2655 -603
  2. package/dist/bruce-cesium.es5.js.map +1 -1
  3. package/dist/bruce-cesium.umd.js +2654 -600
  4. package/dist/bruce-cesium.umd.js.map +1 -1
  5. package/dist/lib/bruce-cesium.js +8 -2
  6. package/dist/lib/bruce-cesium.js.map +1 -1
  7. package/dist/lib/rendering/render-managers/common/entity-label-group.js +538 -0
  8. package/dist/lib/rendering/render-managers/common/entity-label-group.js.map +1 -0
  9. package/dist/lib/rendering/render-managers/common/entity-label.js +1213 -155
  10. package/dist/lib/rendering/render-managers/common/entity-label.js.map +1 -1
  11. package/dist/lib/rendering/render-managers/common/point-clustering-core.js +85 -0
  12. package/dist/lib/rendering/render-managers/common/point-clustering-core.js.map +1 -0
  13. package/dist/lib/rendering/render-managers/common/point-clustering.js +566 -419
  14. package/dist/lib/rendering/render-managers/common/point-clustering.js.map +1 -1
  15. package/dist/lib/rendering/render-managers/entities/entities-ids-render-manager.js +1 -1
  16. package/dist/lib/rendering/render-managers/entities/entities-ids-render-manager.js.map +1 -1
  17. package/dist/lib/rendering/render-managers/entities/entities-render-manager.js +2 -2
  18. package/dist/lib/rendering/render-managers/entities/entities-render-manager.js.map +1 -1
  19. package/dist/lib/rendering/visuals-register.js +273 -24
  20. package/dist/lib/rendering/visuals-register.js.map +1 -1
  21. package/dist/types/bruce-cesium.d.ts +4 -1
  22. package/dist/types/rendering/render-managers/common/entity-label-group.d.ts +109 -0
  23. package/dist/types/rendering/render-managers/common/entity-label.d.ts +138 -1
  24. package/dist/types/rendering/render-managers/common/point-clustering-core.d.ts +17 -0
  25. package/dist/types/rendering/render-managers/common/point-clustering.d.ts +37 -72
  26. package/dist/types/rendering/visuals-register.d.ts +43 -2
  27. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import * as Cesium from 'cesium';
2
- import { Cartographic, ColorMaterialProperty, Entity, Color, ConstantProperty, CallbackProperty, Primitive, Cesium3DTileFeature, 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, HeightReference, ColorBlendMode, ShadowMode, ClassificationType, Model, HorizontalOrigin, VerticalOrigin, ConstantPositionProperty, ArcType, CornerType, DistanceDisplayCondition, SceneTransforms, ImageMaterialProperty, PolygonHierarchy, PolylineGraphics, Rectangle, NearFarScalar, Matrix3, KmlDataSource, GeoJsonDataSource, SceneMode, 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, CustomDataSource, EllipsoidGeodesic, sampleTerrainMostDetailed, defined, CesiumInspector, ClockRange, DynamicEnvironmentMapManager, ScreenSpaceEventHandler, ScreenSpaceEventType, GeometryInstance, 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, Bounds, ClientFileValueMap, Color as Color$1, MenuItem, ProjectView, ProjectViewBookmark, Tileset, ZoomControl, BruceEvent, EntityCoords, DataLab, DelayQueue, AccountConcept, EntityRelation, BruceApi, RecordChangeFeed, EntitySource, ProgramKey, ProjectViewLegacyTile, Camera, Account, AccountLimits, Session, UrlUtils, ProjectViewBookmarkGroup, EntityAttachment, EntityAttachmentType, EntityAttribute, MathUtils, EntityRelationType, EntityHistoricData } from 'bruce-models';
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';
4
4
 
5
5
  /**
6
6
  * Ensures a number is returned from a given value.
@@ -5295,6 +5295,86 @@ function InstallShowGate(target) {
5295
5295
  return gate;
5296
5296
  }
5297
5297
 
5298
+ /*
5299
+ * Status semantics are shared by label and scene-entity clusters; opaque image pixels are not inferred.
5300
+ * A bit jank but helps keep current 'important' labels higher than others.
5301
+ */
5302
+ function GetClusterIconPriority(options) {
5303
+ if (Number.isFinite(options === null || options === void 0 ? void 0 : options.iconPriority)) {
5304
+ return options.iconPriority;
5305
+ }
5306
+ const statusPriority = (value) => {
5307
+ const tokens = value.toLowerCase().split(/[^a-z0-9]+/);
5308
+ if (tokens.some(token => ["critical", "fatal", "error", "fault", "failed", "alarm", "danger", "red"].includes(token))) {
5309
+ return 3;
5310
+ }
5311
+ if (tokens.some(token => ["warning", "warn", "degraded", "amber", "orange", "yellow"].includes(token))) {
5312
+ return 2;
5313
+ }
5314
+ if (tokens.some(token => ["unknown", "offline"].includes(token))) {
5315
+ return 1;
5316
+ }
5317
+ return 0;
5318
+ };
5319
+ if ((options === null || options === void 0 ? void 0 : options.iconStatus) != null) {
5320
+ return statusPriority(options.iconStatus);
5321
+ }
5322
+ const glyph = (options === null || options === void 0 ? void 0 : options.icon) || "";
5323
+ if (/[๐Ÿ”ด๐ŸŸฅโŒ๐Ÿšจ]/u.test(glyph)) {
5324
+ return 3;
5325
+ }
5326
+ if (/[๐ŸŸ ๐ŸŸกโš ]/u.test(glyph)) {
5327
+ return 2;
5328
+ }
5329
+ let filename = "";
5330
+ if ((options === null || options === void 0 ? void 0 : options.iconUrl) && !/^(data|blob):/i.test(options.iconUrl)) {
5331
+ try {
5332
+ filename = decodeURIComponent(new URL(options.iconUrl, "https://label.invalid").pathname.split("/").pop());
5333
+ }
5334
+ catch {
5335
+ // Ignore malformed URLs when inferring status.
5336
+ }
5337
+ }
5338
+ return Math.max(statusPriority(glyph), statusPriority(filename));
5339
+ }
5340
+ /*
5341
+ * Groups eligible neighbours around stable representatives without coupling membership to its renderer.
5342
+ */
5343
+ function ClusterItems(params) {
5344
+ const eligible = new Map();
5345
+ for (const item of params.items) {
5346
+ if (!params.eligible || params.eligible(item)) {
5347
+ eligible.set(params.id(item), item);
5348
+ }
5349
+ }
5350
+ const assigned = new Set();
5351
+ const result = [];
5352
+ const minimum = Math.max(2, params.minSize || 2);
5353
+ for (const [id, item] of eligible) {
5354
+ if (assigned.has(id)) {
5355
+ continue;
5356
+ }
5357
+ const members = new Map([[id, item]]);
5358
+ for (const neighbour of params.neighbours(item)) {
5359
+ const key = params.id(neighbour);
5360
+ if (eligible.has(key) && !assigned.has(key)) {
5361
+ members.set(key, eligible.get(key));
5362
+ }
5363
+ }
5364
+ if (members.size < minimum) {
5365
+ continue;
5366
+ }
5367
+ for (const key of members.keys()) {
5368
+ assigned.add(key);
5369
+ }
5370
+ result.push({
5371
+ representative: item,
5372
+ members: Array.from(members.values())
5373
+ });
5374
+ }
5375
+ return result;
5376
+ }
5377
+
5298
5378
  /**
5299
5379
  * Returns cesium property's value.
5300
5380
  * This will check if it's one that changes over time, or just a fixed value.
@@ -5317,10 +5397,564 @@ function getValue(viewer, obj) {
5317
5397
  */
5318
5398
  var EntityLabel;
5319
5399
  (function (EntityLabel) {
5400
+ const ICON_BACKGROUND_BLEND = 0.8;
5401
+ const ICON_BACKGROUND_ALPHA = 0.45;
5402
+ const TEXT_MAX_WIDTH = 200;
5320
5403
  // Key where the label instance is stored within the cesium entity.
5321
5404
  // Eg: rego[ATTACH_KEY] = new EntityLabel(...);
5322
5405
  EntityLabel.ATTACH_KEY = "nextspace-navigator-entity-label";
5406
+ /*
5407
+ * Indexes screen rectangles so sparse scenes do not compare every label with every other label.
5408
+ */
5409
+ class ScreenIndex {
5410
+ constructor() {
5411
+ this.cells = new Map();
5412
+ this.large = [];
5413
+ this.all = [];
5414
+ }
5415
+ keys(frame, padding = 0) {
5416
+ const x0 = Math.floor((frame.x - padding) / 64);
5417
+ const y0 = Math.floor((frame.y - padding) / 64);
5418
+ const x1 = Math.floor((frame.x + frame.width + padding) / 64);
5419
+ const y1 = Math.floor((frame.y + frame.height + padding) / 64);
5420
+ if ((x1 - x0 + 1) * (y1 - y0 + 1) > 4096) {
5421
+ return null;
5422
+ }
5423
+ const keys = [];
5424
+ for (let x = x0; x <= x1; x++) {
5425
+ for (let y = y0; y <= y1; y++) {
5426
+ keys.push(`${x},${y}`);
5427
+ }
5428
+ }
5429
+ return keys;
5430
+ }
5431
+ Add(frame) {
5432
+ this.all.push(frame);
5433
+ const keys = this.keys(frame);
5434
+ if (!keys) {
5435
+ this.large.push(frame);
5436
+ return;
5437
+ }
5438
+ for (const key of keys) {
5439
+ if (!this.cells.has(key)) {
5440
+ this.cells.set(key, []);
5441
+ }
5442
+ this.cells.get(key).push(frame);
5443
+ }
5444
+ }
5445
+ Query(frame, padding = 0) {
5446
+ const keys = this.keys(frame, padding);
5447
+ const found = new Set(keys ? this.large : this.all);
5448
+ for (const key of keys || []) {
5449
+ for (const item of this.cells.get(key) || []) {
5450
+ found.add(item);
5451
+ }
5452
+ }
5453
+ return (Array.from(found).filter(other => frame.x <= other.x + other.width + padding && frame.x + frame.width + padding >= other.x &&
5454
+ frame.y <= other.y + other.height + padding && frame.y + frame.height + padding >= other.y));
5455
+ }
5456
+ HasOverlap(frame) {
5457
+ var _a;
5458
+ const intersects = (other) => (frame.x <= other.x + other.width && frame.x + frame.width >= other.x &&
5459
+ frame.y <= other.y + other.height && frame.y + frame.height >= other.y);
5460
+ const keys = this.keys(frame);
5461
+ if ((keys ? this.large : this.all).some(intersects)) {
5462
+ return true;
5463
+ }
5464
+ for (const key of keys || []) {
5465
+ if ((_a = this.cells.get(key)) === null || _a === void 0 ? void 0 : _a.some(intersects)) {
5466
+ return true;
5467
+ }
5468
+ }
5469
+ return false;
5470
+ }
5471
+ }
5472
+ /*
5473
+ * Includes the stem in collision checks without treating two crossing stems as text occlusion.
5474
+ */
5475
+ function leaderBounds(frame) {
5476
+ if (!frame.hasLeader || !Number.isFinite(frame.anchorX) || !Number.isFinite(frame.anchorY)) {
5477
+ return undefined;
5478
+ }
5479
+ const endX = Math.max(frame.x, Math.min(frame.anchorX, frame.x + frame.width));
5480
+ const endY = Math.max(frame.y, Math.min(frame.anchorY, frame.y + frame.height));
5481
+ if (Math.hypot(frame.anchorX - endX, frame.anchorY - endY) < .01) {
5482
+ return undefined;
5483
+ }
5484
+ return {
5485
+ ...frame,
5486
+ x: Math.min(frame.anchorX, endX) - 1,
5487
+ y: Math.min(frame.anchorY, endY) + .01,
5488
+ width: Math.abs(frame.anchorX - endX) + 2,
5489
+ height: Math.max(0, Math.abs(frame.anchorY - endY) - .02)
5490
+ };
5491
+ }
5492
+ /*
5493
+ * Remembers sides, never rendered positions: its own output cannot trigger another layout pass.
5494
+ */
5495
+ class PlacementMemory {
5496
+ constructor() {
5497
+ this.previous = new Map();
5498
+ this.nextOrder = 0;
5499
+ }
5500
+ Resolve(frames) {
5501
+ var _a, _b;
5502
+ const live = new Set(frames.map(frame => frame.id));
5503
+ const moved = (frame) => {
5504
+ var _a;
5505
+ const old = (_a = this.previous.get(frame.id)) === null || _a === void 0 ? void 0 : _a.frame;
5506
+ return (!old ||
5507
+ old.transient !== frame.transient || old.canFlip !== frame.canFlip ||
5508
+ Math.hypot(frame.x - old.x, frame.y - old.y) >= 24 ||
5509
+ Math.abs(frame.width - old.width) >= 8 || Math.abs(frame.height - old.height) >= 8);
5510
+ };
5511
+ const persistent = frames.filter(frame => !frame.transient);
5512
+ const changes = (persistent.filter(moved).length +
5513
+ Array.from(this.previous.values()).filter(old => !old.frame.transient && !live.has(old.frame.id)).length);
5514
+ // A hover or one arrival cannot rearrange incumbents; a substantial batch/camera change can.
5515
+ const restructure = this.previous.size > 0 && changes >= Math.max(2, Math.ceil(persistent.length * 0.3));
5516
+ const fixed = (frame) => !restructure && !frame.transient && this.previous.has(frame.id) && !moved(frame);
5517
+ const ordered = ([...frames].sort((a, b) => {
5518
+ var _a, _b, _c, _d;
5519
+ return Number(fixed(b)) - Number(fixed(a)) ||
5520
+ Number(Boolean(a.transient)) - Number(Boolean(b.transient)) ||
5521
+ ((_b = (_a = this.previous.get(a.id)) === null || _a === void 0 ? void 0 : _a.order) !== null && _b !== void 0 ? _b : Infinity) - ((_d = (_c = this.previous.get(b.id)) === null || _c === void 0 ? void 0 : _c.order) !== null && _d !== void 0 ? _d : Infinity) ||
5522
+ a.depth - b.depth || a.id.localeCompare(b.id);
5523
+ }));
5524
+ const text = new ScreenIndex(), leaders = new ScreenIndex();
5525
+ const placed = new Map();
5526
+ const next = new Map();
5527
+ 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);
5532
+ };
5533
+ const score = (frame, padding = 0) => {
5534
+ const box = {
5535
+ ...frame,
5536
+ x: frame.x - padding + .01,
5537
+ y: frame.y - padding + .01,
5538
+ width: Math.max(0, frame.width + padding * 2 - .02),
5539
+ height: Math.max(0, frame.height + padding * 2 - .02)
5540
+ };
5541
+ const stem = leaderBounds(frame);
5542
+ return (Number(text.HasOverlap(box)) * 4 + Number(leaders.HasOverlap(box)) +
5543
+ Number(Boolean(stem && text.HasOverlap(stem))));
5544
+ };
5545
+ for (const frame of ordered) {
5546
+ 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";
5548
+ let chosen = candidate(frame, side);
5549
+ if (!fixed(frame) && frame.canFlip) {
5550
+ const otherSide = side === "left" ? "right" : "left";
5551
+ const other = candidate(frame, otherSide);
5552
+ // An established side only changes when the alternative has a little breathing room.
5553
+ if (score(other, old ? 4 : 0) < score(chosen)) {
5554
+ side = otherSide;
5555
+ chosen = other;
5556
+ }
5557
+ }
5558
+ 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++ });
5560
+ text.Add(chosen);
5561
+ const stem = leaderBounds(chosen);
5562
+ if (stem) {
5563
+ leaders.Add(stem);
5564
+ }
5565
+ }
5566
+ this.previous = next;
5567
+ return placed;
5568
+ }
5569
+ }
5570
+ EntityLabel.PlacementMemory = PlacementMemory;
5571
+ /*
5572
+ * Prefer declared semantics; only infers from recognisable glyphs and filename tokens, never image pixels.
5573
+ */
5574
+ EntityLabel.IconPriority = GetClusterIconPriority;
5575
+ /*
5576
+ * Resolves clustering and stable text placement before depth fading; highlighted labels receive priority.
5577
+ */
5578
+ function Layout(input, memory = new PlacementMemory()) {
5579
+ var _a, _b, _c, _d, _e, _f, _g, _h;
5580
+ const frames = input.filter(frame => [frame.x, frame.y, frame.width, frame.height, frame.depth].every(Number.isFinite))
5581
+ .sort((a, b) => (b.priority || 0) - (a.priority || 0) || a.depth - b.depth || a.id.localeCompare(b.id));
5582
+ const result = new Map();
5583
+ const entityKeys = new Set();
5584
+ const groups = new Map();
5585
+ for (const frame of frames) {
5586
+ const duplicate = frame.entityKey && entityKeys.has(frame.entityKey);
5587
+ result.set(frame.id, { frame, visible: !duplicate, opacity: frame.opacity, zIndex: 0, members: [], entityIds: frame.entityId ? [frame.entityId] : [] });
5588
+ if (duplicate) {
5589
+ continue;
5590
+ }
5591
+ if (frame.entityKey)
5592
+ entityKeys.add(frame.entityKey);
5593
+ if (!((_b = (_a = frame.options) === null || _a === void 0 ? void 0 : _a.cluster) === null || _b === void 0 ? void 0 : _b.enabled) || frame.priority > 0) {
5594
+ continue;
5595
+ }
5596
+ const key = JSON.stringify([frame.group || "", frame.options.presentation || "text"]);
5597
+ if (!groups.has(key)) {
5598
+ groups.set(key, []);
5599
+ }
5600
+ groups.get(key).push(frame);
5601
+ }
5602
+ for (const group of groups.values()) {
5603
+ // Summary and badge dimensions are output, never the next frame's membership input.
5604
+ const candidates = group.map(frame => frame.extendedGeometry && frame.canFlip &&
5605
+ frame.options.cluster.pixelRange == null ? { ...frame, x: frame.anchorX, y: frame.anchorY, width: 1, height: 1 } :
5606
+ frame.clusterBounds ? { ...frame, ...frame.clusterBounds } : frame);
5607
+ const index = new ScreenIndex();
5608
+ for (const frame of candidates) {
5609
+ index.Add(frame);
5610
+ }
5611
+ const clusters = ClusterItems({
5612
+ items: candidates,
5613
+ id: frame => frame.id,
5614
+ minSize: group[0].options.cluster.minSize,
5615
+ neighbours: frame => {
5616
+ var _a;
5617
+ return (index.Query(frame, (_a = frame.options.cluster.pixelRange) !== null && _a !== void 0 ? _a : (frame.options.presentation === "icon" ? 24 : 8)));
5618
+ }
5619
+ });
5620
+ for (const cluster of clusters) {
5621
+ let chosen = cluster.representative;
5622
+ if ((chosen.options.presentation || "text") !== "text" && chosen.options.cluster.representative !== "nearest") {
5623
+ let priority = EntityLabel.IconPriority(chosen.options);
5624
+ for (const member of cluster.members) {
5625
+ const candidate = EntityLabel.IconPriority(member.options);
5626
+ if (candidate > priority) {
5627
+ chosen = member;
5628
+ priority = candidate;
5629
+ }
5630
+ }
5631
+ }
5632
+ const representative = result.get(chosen.id);
5633
+ representative.members = cluster.members.map(frame => frame.id);
5634
+ representative.entityIds = Array.from(new Set(cluster.members.map(frame => frame.entityId).filter(Boolean)));
5635
+ if (chosen.options.presentation !== "icon") {
5636
+ const values = new Set();
5637
+ const stableMembers = [...cluster.members].sort((a, b) => (a.entityKey || a.entityId || a.id).localeCompare(b.entityKey || b.entityId || b.id));
5638
+ for (const member of stableMembers) {
5639
+ const value = ((_e = (_c = member.text) !== null && _c !== void 0 ? _c : (_d = member.options) === null || _d === void 0 ? void 0 : _d.text) !== null && _e !== void 0 ? _e : "").replace(/\s+/g, " ").trim();
5640
+ if (value) {
5641
+ values.add(value);
5642
+ }
5643
+ }
5644
+ 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
+ }
5656
+ }
5657
+ }
5658
+ for (const frame of cluster.members) {
5659
+ if (frame !== chosen)
5660
+ result.get(frame.id).visible = false;
5661
+ }
5662
+ }
5663
+ }
5664
+ const placed = memory.Resolve(frames.filter(frame => result.get(frame.id).visible));
5665
+ const foreground = new ScreenIndex(), foregroundLeaders = new ScreenIndex();
5666
+ let zIndex = frames.length + 1;
5667
+ for (const original of frames) {
5668
+ const placement = result.get(original.id);
5669
+ if (!placement.visible) {
5670
+ continue;
5671
+ }
5672
+ const chosen = placed.get(original.id);
5673
+ const frame = placement.frame = chosen.frame;
5674
+ placement.textSide = chosen.side;
5675
+ const stem = leaderBounds(frame);
5676
+ 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
+ const overlap = foreground.HasOverlap(box) || foregroundLeaders.HasOverlap(box) || Boolean(stem && foreground.HasOverlap(stem));
5678
+ 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));
5680
+ }
5681
+ placement.zIndex = zIndex--;
5682
+ foreground.Add(frame);
5683
+ if (stem) {
5684
+ foregroundLeaders.Add(stem);
5685
+ }
5686
+ }
5687
+ return Array.from(result.values());
5688
+ }
5689
+ EntityLabel.Layout = Layout;
5690
+ const layers = new WeakMap();
5691
+ const containers = new WeakMap();
5692
+ /*
5693
+ * Moves the shared overlay without recreating labels or resolving their sources.
5694
+ */
5695
+ function SetContainer(viewer, container) {
5696
+ const layer = layers.get(viewer);
5697
+ if (container && (container === (layer === null || layer === void 0 ? void 0 : layer.element) || (layer === null || layer === void 0 ? void 0 : layer.element.contains(container)))) {
5698
+ throw new Error("Label container cannot be inside the label overlay");
5699
+ }
5700
+ if (container) {
5701
+ containers.set(viewer, container);
5702
+ }
5703
+ else
5704
+ containers.delete(viewer);
5705
+ layer === null || layer === void 0 ? void 0 : layer.Move();
5706
+ }
5707
+ EntityLabel.SetContainer = SetContainer;
5708
+ /*
5709
+ * Owns one scene callback per viewer and separates projection/measurement from DOM writes.
5710
+ */
5711
+ class Layer {
5712
+ constructor(viewer) {
5713
+ this.viewer = viewer;
5714
+ this.element = document.createElement("div");
5715
+ this.coordinates = { x: 0, y: 0, scaleX: 1, scaleY: 1 };
5716
+ this.participants = new Set();
5717
+ this.placementMemory = new PlacementMemory();
5718
+ this.forwardingWheel = false;
5719
+ this.pendingDrag = null;
5720
+ this.TrackPointer = (event) => {
5721
+ var _a, _b;
5722
+ if (event.pointerType && event.pointerType !== "mouse" && event.pointerType !== "pen")
5723
+ return;
5724
+ const rect = (_b = (_a = this.viewer.canvas).getBoundingClientRect) === null || _b === void 0 ? void 0 : _b.call(_a);
5725
+ if (!rect || event.clientX < rect.left || event.clientX >= rect.right || event.clientY < rect.top || event.clientY >= rect.bottom) {
5726
+ if (this.pointerClient)
5727
+ this.ClearPointer();
5728
+ return;
5729
+ }
5730
+ this.pointerClient = { x: event.clientX, y: event.clientY };
5731
+ this.viewer.scene.requestRender();
5732
+ };
5733
+ this.PointerOut = (event) => { if (!event.relatedTarget)
5734
+ this.ClearPointer(); };
5735
+ this.ClearPointer = () => {
5736
+ this.pointerClient = undefined;
5737
+ this.pointer = undefined;
5738
+ if (!this.viewer.isDestroyed()) {
5739
+ this.viewer.scene.requestRender();
5740
+ }
5741
+ };
5742
+ this.PreventNativeDrag = (event) => {
5743
+ event.preventDefault();
5744
+ };
5745
+ this.BeginDrag = (event) => {
5746
+ var _a, _b, _c;
5747
+ this.EndPendingDrag();
5748
+ const detection = FeatureDetection;
5749
+ const target = (_b = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest) === null || _b === void 0 ? void 0 : _b.call(_a, '.entity-label [role="button"]');
5750
+ const canvas = this.viewer.canvas;
5751
+ if (event.defaultPrevented || event.button !== 0 || event.pointerType !== "mouse" ||
5752
+ ((_c = detection.supportsPointerEvents) === null || _c === void 0 ? void 0 : _c.call(detection)) === false || !canvas.setPointerCapture || this.viewer.isDestroyed() ||
5753
+ !target || !this.element.contains(target) ||
5754
+ this.element.ownerDocument.defaultView.getComputedStyle(target).visibility !== "visible") {
5755
+ return;
5756
+ }
5757
+ const bounds = canvas.getBoundingClientRect();
5758
+ if (event.clientX < bounds.left || event.clientX >= bounds.right || event.clientY < bounds.top || event.clientY >= bounds.bottom) {
5759
+ return;
5760
+ }
5761
+ this.pendingDrag = event;
5762
+ const doc = this.element.ownerDocument;
5763
+ doc.addEventListener("pointermove", this.ContinueDrag, true);
5764
+ doc.addEventListener("pointerup", this.FinishPendingDrag, true);
5765
+ doc.addEventListener("pointercancel", this.FinishPendingDrag, true);
5766
+ doc.defaultView.addEventListener("blur", this.EndPendingDrag);
5767
+ };
5768
+ this.FinishPendingDrag = (event) => {
5769
+ var _a;
5770
+ if (event.pointerId === ((_a = this.pendingDrag) === null || _a === void 0 ? void 0 : _a.pointerId)) {
5771
+ this.EndPendingDrag();
5772
+ }
5773
+ };
5774
+ this.EndPendingDrag = () => {
5775
+ this.pendingDrag = null;
5776
+ const doc = this.element.ownerDocument;
5777
+ doc.removeEventListener("pointermove", this.ContinueDrag, true);
5778
+ doc.removeEventListener("pointerup", this.FinishPendingDrag, true);
5779
+ doc.removeEventListener("pointercancel", this.FinishPendingDrag, true);
5780
+ doc.defaultView.removeEventListener("blur", this.EndPendingDrag);
5781
+ };
5782
+ this.ContinueDrag = (event) => {
5783
+ const start = this.pendingDrag;
5784
+ if (!start || event.pointerId !== start.pointerId) {
5785
+ return;
5786
+ }
5787
+ if (!(event.buttons & 1) || this.viewer.isDestroyed() || !this.element.contains(start.target)) {
5788
+ this.EndPendingDrag();
5789
+ return;
5790
+ }
5791
+ // Keep ordinary clicks local; only a deliberate drag starts Cesium's camera gesture.
5792
+ if (Math.hypot(event.clientX - start.clientX, event.clientY - start.clientY) < 6) {
5793
+ return;
5794
+ }
5795
+ this.EndPendingDrag();
5796
+ const canvas = this.viewer.canvas;
5797
+ try {
5798
+ canvas.setPointerCapture(start.pointerId);
5799
+ }
5800
+ catch {
5801
+ // A cancelled pointer or unsupported capture still cannot start native image dragging.
5802
+ return;
5803
+ }
5804
+ const win = this.element.ownerDocument.defaultView;
5805
+ const copy = (source) => ({
5806
+ bubbles: true, cancelable: true, pointerId: source.pointerId, pointerType: source.pointerType,
5807
+ isPrimary: source.isPrimary, button: source.button, buttons: source.buttons,
5808
+ clientX: source.clientX, clientY: source.clientY, screenX: source.screenX, screenY: source.screenY,
5809
+ ctrlKey: source.ctrlKey, shiftKey: source.shiftKey, altKey: source.altKey, metaKey: source.metaKey
5810
+ });
5811
+ // Capture routes subsequent real move/up events to Cesium, including releases outside the label.
5812
+ canvas.dispatchEvent(new win.PointerEvent("pointerdown", copy(start)));
5813
+ canvas.dispatchEvent(new win.PointerEvent("pointermove", copy(event)));
5814
+ event.preventDefault();
5815
+ event.stopPropagation();
5816
+ this.viewer.scene.requestRender();
5817
+ };
5818
+ this.ForwardWheel = (event) => {
5819
+ var _a, _b, _c;
5820
+ if (this.forwardingWheel || event.defaultPrevented || this.viewer.isDestroyed()) {
5821
+ return;
5822
+ }
5823
+ const target = (_b = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest) === null || _b === void 0 ? void 0 : _b.call(_a, '.entity-label [role="button"]');
5824
+ const canvas = this.viewer.canvas;
5825
+ const win = this.element.ownerDocument.defaultView;
5826
+ if (!target || !this.element.contains(target) || win.getComputedStyle(target).visibility !== "visible") {
5827
+ return;
5828
+ }
5829
+ const bounds = canvas.getBoundingClientRect();
5830
+ if (event.clientX < bounds.left || event.clientX >= bounds.right || event.clientY < bounds.top || event.clientY >= bounds.bottom) {
5831
+ return;
5832
+ }
5833
+ const position = {
5834
+ bubbles: true, cancelable: true, clientX: event.clientX, clientY: event.clientY,
5835
+ screenX: event.screenX, screenY: event.screenY, buttons: event.buttons,
5836
+ ctrlKey: event.ctrlKey, shiftKey: event.shiftKey, altKey: event.altKey, metaKey: event.metaKey,
5837
+ pointerId: 1, pointerType: "mouse", isPrimary: true
5838
+ };
5839
+ this.forwardingWheel = true;
5840
+ try {
5841
+ // Cesium's wheel zoom uses its last mouse position, not the wheel event's coordinates.
5842
+ const detection = FeatureDetection;
5843
+ // Older Cesium versions opt Firefox out even when the browser exposes PointerEvent.
5844
+ canvas.dispatchEvent(((_c = detection.supportsPointerEvents) === null || _c === void 0 ? void 0 : _c.call(detection)) !== false && typeof win.PointerEvent === "function" ?
5845
+ new win.PointerEvent("pointermove", position) : new win.MouseEvent("mousemove", position));
5846
+ canvas.dispatchEvent(new win.WheelEvent("wheel", {
5847
+ ...position, deltaX: event.deltaX, deltaY: event.deltaY, deltaZ: event.deltaZ, deltaMode: event.deltaMode
5848
+ }));
5849
+ this.viewer.scene.requestRender();
5850
+ event.preventDefault();
5851
+ event.stopPropagation();
5852
+ }
5853
+ finally {
5854
+ this.forwardingWheel = false;
5855
+ }
5856
+ };
5857
+ this.element.className = "entity-label-layer";
5858
+ // Depth ordering belongs inside the map overlay, never in the application's UI stack.
5859
+ this.element.style.cssText = "position:absolute;inset:0;z-index:0;isolation:isolate;pointer-events:none;";
5860
+ this.Move();
5861
+ this.element.addEventListener("wheel", this.ForwardWheel, { passive: false });
5862
+ this.element.addEventListener("pointerdown", this.BeginDrag, true);
5863
+ this.element.addEventListener("dragstart", this.PreventNativeDrag);
5864
+ this.element.ownerDocument.addEventListener("pointermove", this.TrackPointer, true);
5865
+ this.element.ownerDocument.addEventListener("pointerout", this.PointerOut, true);
5866
+ this.element.ownerDocument.defaultView.addEventListener("blur", this.ClearPointer);
5867
+ this.remove = viewer.scene.postUpdate.addEventListener(() => this.Update());
5868
+ }
5869
+ Move() {
5870
+ (containers.get(this.viewer) || this.viewer.container).appendChild(this.element);
5871
+ this.Update();
5872
+ this.viewer.scene.requestRender();
5873
+ }
5874
+ MeasureCoordinates() {
5875
+ var _a;
5876
+ const canvas = this.viewer.canvas;
5877
+ const canvasRect = (_a = canvas.getBoundingClientRect) === null || _a === void 0 ? void 0 : _a.call(canvas);
5878
+ const rect = this.element.getBoundingClientRect();
5879
+ const scaleX = rect.width / this.element.offsetWidth || 1;
5880
+ const scaleY = rect.height / this.element.offsetHeight || 1;
5881
+ return {
5882
+ x: (((canvasRect === null || canvasRect === void 0 ? void 0 : canvasRect.left) || 0) - rect.left) / scaleX,
5883
+ y: (((canvasRect === null || canvasRect === void 0 ? void 0 : canvasRect.top) || 0) - rect.top) / scaleY,
5884
+ scaleX: ((canvasRect === null || canvasRect === void 0 ? void 0 : canvasRect.width) / canvas.clientWidth || 1) / scaleX,
5885
+ scaleY: ((canvasRect === null || canvasRect === void 0 ? void 0 : canvasRect.height) / canvas.clientHeight || 1) / scaleY
5886
+ };
5887
+ }
5888
+ Add(participant) {
5889
+ this.participants.add(participant);
5890
+ this.viewer.scene.requestRender();
5891
+ return () => {
5892
+ this.participants.delete(participant);
5893
+ if (!this.participants.size) {
5894
+ this.remove();
5895
+ this.element.removeEventListener("wheel", this.ForwardWheel);
5896
+ this.element.removeEventListener("pointerdown", this.BeginDrag, true);
5897
+ this.element.removeEventListener("dragstart", this.PreventNativeDrag);
5898
+ this.element.ownerDocument.removeEventListener("pointermove", this.TrackPointer, true);
5899
+ this.element.ownerDocument.removeEventListener("pointerout", this.PointerOut, true);
5900
+ this.element.ownerDocument.defaultView.removeEventListener("blur", this.ClearPointer);
5901
+ this.EndPendingDrag();
5902
+ this.element.remove();
5903
+ layers.delete(this.viewer);
5904
+ }
5905
+ };
5906
+ }
5907
+ Update() {
5908
+ var _a, _b;
5909
+ this.coordinates = this.MeasureCoordinates();
5910
+ const rect = (_b = (_a = this.viewer.canvas).getBoundingClientRect) === null || _b === void 0 ? void 0 : _b.call(_a);
5911
+ const pointer = this.pointerClient;
5912
+ this.pointer = pointer && (rect === null || rect === void 0 ? void 0 : rect.width) && (rect === null || rect === void 0 ? void 0 : rect.height) && pointer.x >= rect.left && pointer.x < rect.right && pointer.y >= rect.top && pointer.y < rect.bottom ?
5913
+ {
5914
+ x: (pointer.x - rect.left) * this.viewer.canvas.clientWidth / rect.width,
5915
+ y: (pointer.y - rect.top) * this.viewer.canvas.clientHeight / rect.height
5916
+ } : undefined;
5917
+ const projected = new Map();
5918
+ for (const participant of this.participants) {
5919
+ projected.set(participant, participant.project());
5920
+ }
5921
+ const placements = new Map(Layout(Array.from(projected.values()).filter(Boolean), this.placementMemory).map(item => [item.frame.id, item]));
5922
+ for (const [participant, frame] of projected) {
5923
+ participant.apply(frame ? placements.get(frame.id) : undefined);
5924
+ }
5925
+ }
5926
+ }
5927
+ let nextLabelId = 0;
5323
5928
  class Label {
5929
+ get HasText() { return this.options.presentation !== "icon"; }
5930
+ SetHoverVisible(visible) {
5931
+ var _a, _b, _c;
5932
+ if (!this.Transient || this.Persistent) {
5933
+ return;
5934
+ }
5935
+ if (!visible) {
5936
+ this.retainedHoverPosition = (_a = this.visualRegister) === null || _a === void 0 ? void 0 : _a.GetLabelHoverPosition((_b = this.rego) === null || _b === void 0 ? void 0 : _b.entityId, (_c = this.rego) === null || _c === void 0 ? void 0 : _c.menuItemId);
5937
+ // Retiring labels must not change the ring now owned by the next hover target.
5938
+ if (this.ringAnimation) {
5939
+ this.ringAnimation = { ...this.ringAnimation };
5940
+ }
5941
+ }
5942
+ if (this.hoverFade.to === Number(visible)) {
5943
+ return;
5944
+ }
5945
+ this.hoverFade = { from: this.HoverOpacity(), to: Number(visible), start: Date.now() };
5946
+ this.viewer.scene.requestRender();
5947
+ }
5948
+ HoverOpacity() {
5949
+ if (!this.Transient || this.Persistent) {
5950
+ return 1;
5951
+ }
5952
+ const progress = Math.min(1, Math.max(0, (Date.now() - this.hoverFade.start) / 120));
5953
+ if (progress < 1) {
5954
+ this.viewer.scene.requestRender();
5955
+ }
5956
+ return this.hoverFade.from + (this.hoverFade.to - this.hoverFade.from) * progress * progress * (3 - 2 * progress);
5957
+ }
5324
5958
  get Rego() {
5325
5959
  return this.rego;
5326
5960
  }
@@ -5333,10 +5967,18 @@ var EntityLabel;
5333
5967
  set pos3d(val) {
5334
5968
  this._pos3d = val;
5335
5969
  this.prePositioned = !isNaN(val === null || val === void 0 ? void 0 : val.x);
5336
- this.createLabel(val);
5970
+ if (this.label && this.prePositioned)
5971
+ this.viewer.scene.requestRender();
5972
+ else
5973
+ this.createLabel(val);
5337
5974
  }
5338
5975
  constructor(params) {
5339
- var _a;
5976
+ var _a, _b, _c, _d;
5977
+ this.id = `label-${++nextLabelId}`;
5978
+ this.Persistent = false;
5979
+ this.Transient = false;
5980
+ this.hoverFade = { from: 0, to: 1, start: Date.now() };
5981
+ this.cursorMotion = { amount: 0, from: 0, target: 0, start: 0, position: undefined };
5340
5982
  // Counter to help differentiate between createLabel calls.
5341
5983
  this.counter = 0;
5342
5984
  // The label element itself.
@@ -5356,15 +5998,24 @@ var EntityLabel;
5356
5998
  this.loadTimeout = null;
5357
5999
  this.loadedName = false;
5358
6000
  this.line = null;
6001
+ this.anchor = null;
5359
6002
  // Null = not attempted to load.
5360
6003
  this.additionalTexts = null;
5361
6004
  this.rego = params.rego;
5362
6005
  this.viewer = params.viewer;
5363
6006
  this.api = params.api;
5364
6007
  this.visualRegister = params.visualRegister;
5365
- this.prePositioned = !isNaN((_a = params.pos3d) === null || _a === void 0 ? void 0 : _a.x);
6008
+ this.options = params.options || {};
6009
+ this.groupId = params.groupId;
6010
+ this.Persistent = (_a = params.persistent) !== null && _a !== void 0 ? _a : Boolean(params.groupId);
6011
+ this.Transient = (_b = params.transient) !== null && _b !== void 0 ? _b : false;
6012
+ this.hoverRingState = params.hoverRingState;
6013
+ this.visible = params.visible;
6014
+ if (((_c = params.options) === null || _c === void 0 ? void 0 : _c.text) != null)
6015
+ this.additionalTexts = [];
6016
+ this.prePositioned = !isNaN((_d = params.pos3d) === null || _d === void 0 ? void 0 : _d.x);
5366
6017
  this.createLabel(params.pos3d);
5367
- if (!params.api && !ENVIRONMENT.IS_SELF_MANAGED) {
6018
+ if (!params.api && !ENVIRONMENT.IS_SELF_MANAGED && this.options.text == null) {
5368
6019
  this.api = ENVIRONMENT.Api().GetBruceApi();
5369
6020
  }
5370
6021
  }
@@ -5414,12 +6065,24 @@ var EntityLabel;
5414
6065
  clearTimeout(this.loadTimeout);
5415
6066
  this.loadTimeout = null;
5416
6067
  }
6068
+ /*
6069
+ * Rebuilds only when configuration changes; state refreshes must not recreate DOM every frame.
6070
+ */
6071
+ Configure(options, groupId) {
6072
+ if (this.groupId === groupId && JSON.stringify(this.options) === JSON.stringify(options || {})) {
6073
+ return;
6074
+ }
6075
+ this.options = options || {};
6076
+ this.groupId = groupId;
6077
+ this.additionalTexts = this.options.text != null ? [] : null;
6078
+ this.createLabel();
6079
+ }
5417
6080
  /**
5418
6081
  * Creates cesium entity for the label if one does not exist.
5419
6082
  * @param starterPos3d optional position to create label at. Ignored for billboard labels.
5420
6083
  */
5421
6084
  async createLabel(starterPos3d) {
5422
- var _a, _b, _c, _d, _e, _f, _g, _h;
6085
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
5423
6086
  this.removeLabel();
5424
6087
  let pos3d = starterPos3d ? starterPos3d.clone() : null;
5425
6088
  const counter = ++this.counter;
@@ -5628,7 +6291,7 @@ var EntityLabel;
5628
6291
  }
5629
6292
  clearTimeout(this.loadTimeout);
5630
6293
  this.loadTimeout = setTimeout(async () => {
5631
- var _a, _b, _c, _d;
6294
+ var _a, _b, _c, _d, _e;
5632
6295
  if (this.disposed || this.counter != counter) {
5633
6296
  return;
5634
6297
  }
@@ -5636,10 +6299,10 @@ var EntityLabel;
5636
6299
  if (this.additionalTexts == null) {
5637
6300
  const additionalTexts = [];
5638
6301
  // Rego might not have a known or default Entity Type. Eg: OSM building.
5639
- if (this.rego.entityTypeId) {
6302
+ if (this.rego.entityTypeId || this.options.attributes) {
5640
6303
  let entityType = null;
5641
6304
  try {
5642
- const data = await EntityType.Get({
6305
+ const data = this.options.attributes ? null : await EntityType.Get({
5643
6306
  api: this.api,
5644
6307
  entityTypeId: this.rego.entityTypeId
5645
6308
  });
@@ -5654,14 +6317,14 @@ var EntityLabel;
5654
6317
  }
5655
6318
  // Type could be deleted, however we'll just catch that to show a label instead of nothing.
5656
6319
  // This is not unusual for Tilesets where the Type that is baked might be outdated.
5657
- if (entityType) {
5658
- const attributes = (_b = (_a = entityType.VisualSettings) === null || _a === void 0 ? void 0 : _a.label) === null || _b === void 0 ? void 0 : _b.attributes;
6320
+ if (entityType || this.options.attributes) {
6321
+ const attributes = (_a = this.options.attributes) !== null && _a !== void 0 ? _a : (_c = (_b = entityType.VisualSettings) === null || _b === void 0 ? void 0 : _b.label) === null || _c === void 0 ? void 0 : _c.attributes;
5659
6322
  if (attributes) {
5660
6323
  try {
5661
6324
  const { entity } = await Entity$1.Get({
5662
6325
  api: this.api,
5663
6326
  entityId: this.rego.entityId,
5664
- scenario: ((_c = this.rego.outline) === null || _c === void 0 ? void 0 : _c.length) ? (_d = this.rego.outline.find(x => !!x.Scenario)) === null || _d === void 0 ? void 0 : _d.Scenario : null,
6327
+ scenario: ((_d = this.rego.outline) === null || _d === void 0 ? void 0 : _d.length) ? (_e = this.rego.outline.find(x => !!x.Scenario)) === null || _e === void 0 ? void 0 : _e.Scenario : null,
5665
6328
  schemaId: this.rego.schema,
5666
6329
  migrated: true
5667
6330
  });
@@ -5670,7 +6333,7 @@ var EntityLabel;
5670
6333
  entity: entity,
5671
6334
  path: attributes[i]
5672
6335
  });
5673
- if (value) {
6336
+ if (value != null) {
5674
6337
  additionalTexts.push(value);
5675
6338
  }
5676
6339
  }
@@ -5699,12 +6362,166 @@ var EntityLabel;
5699
6362
  if (this.disposed || this.counter != counter) {
5700
6363
  return;
5701
6364
  }
5702
- const labelContent = `
5703
- <div>
5704
- <div style="margin-bottom:4px">${text}</div>
5705
- ${additionalTexts.map(attr => `<div>${attr}</div>`).join('')}
5706
- </div>
5707
- `;
6365
+ const presentation = this.options.presentation || "text";
6366
+ let clickEntityIds = [];
6367
+ const selectOnClick = (_j = this.options.selectOnClick) !== null && _j !== void 0 ? _j : Boolean(this.groupId);
6368
+ const select = (event) => {
6369
+ event.preventDefault();
6370
+ event.stopPropagation();
6371
+ if (!selectOnClick || this.disposed || this.counter !== counter || !clickEntityIds.length || !this.visualRegister) {
6372
+ return;
6373
+ }
6374
+ this.visualRegister.ClearSelected({ requestRender: false });
6375
+ this.visualRegister.SetSelected({ entityIds: clickEntityIds, selected: true });
6376
+ };
6377
+ const makeClickable = (target) => {
6378
+ if (!selectOnClick || !this.visualRegister) {
6379
+ return;
6380
+ }
6381
+ target.style.pointerEvents = "auto";
6382
+ target.style.cursor = "pointer";
6383
+ target.style.userSelect = "none";
6384
+ target.setAttribute("role", "button");
6385
+ target.tabIndex = 0;
6386
+ target.addEventListener("click", select);
6387
+ target.addEventListener("keydown", event => {
6388
+ if (event.key === "Enter" || event.key === " ")
6389
+ select(event);
6390
+ });
6391
+ // Prevent viewer picking and camera gestures from interpreting label interactions.
6392
+ for (const type of ["pointerdown", "pointerup", "mousedown", "mouseup", "dblclick"]) {
6393
+ target.addEventListener(type, event => event.stopPropagation());
6394
+ }
6395
+ };
6396
+ const normalLeader = (_k = this.options.leader) !== null && _k !== void 0 ? _k : presentation === "text";
6397
+ let leader = normalLeader;
6398
+ const content = document.createElement("span");
6399
+ const originalText = (_l = this.options.text) !== null && _l !== void 0 ? _l : [text, ...additionalTexts].join("\n");
6400
+ let textElement;
6401
+ let sourceText;
6402
+ let sourceNaturalText;
6403
+ let iconElement;
6404
+ let iconContainer;
6405
+ content.className = "entity-label-content";
6406
+ content.style.cssText = "display:flex;align-items:center;gap:6px;";
6407
+ if (presentation === "icon-text-above") {
6408
+ content.style.flexDirection = "column-reverse";
6409
+ }
6410
+ if (presentation !== "text") {
6411
+ const icon = document.createElement("span");
6412
+ iconElement = icon;
6413
+ icon.className = "entity-label-icon";
6414
+ icon.style.cssText = `font-size:${(this.options.iconSize || 24) * ((_m = this.options.iconScale) !== null && _m !== void 0 ? _m : 1)}px;line-height:1;`;
6415
+ icon.style.textShadow = "0 1px 2px rgba(0, 0, 0, 0.25)";
6416
+ const iconColor = typeof this.options.iconColor === "string" ? this.options.iconColor : undefined;
6417
+ const iconBackground = this.options.iconBackground && this.options.iconScale !== 0;
6418
+ if (iconColor) {
6419
+ icon.style.color = iconColor;
6420
+ }
6421
+ if (this.options.iconUrl) {
6422
+ const image = document.createElement("img");
6423
+ image.draggable = false;
6424
+ image.alt = this.options.icon || "";
6425
+ const scale = (_o = this.options.iconScale) !== null && _o !== void 0 ? _o : 1;
6426
+ 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
+ }
6442
+ const resize = () => {
6443
+ if (this.disposed || this.counter !== counter) {
6444
+ return;
6445
+ }
6446
+ 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`;
6449
+ this.viewer.scene.requestRender();
6450
+ };
6451
+ image.onload = resize;
6452
+ resize();
6453
+ image.src = this.options.iconUrl;
6454
+ if (!iconColor) {
6455
+ icon.appendChild(image);
6456
+ }
6457
+ }
6458
+ else if (typeof this.options.iconClass === "string" && this.options.iconClass) {
6459
+ icon.className = `entity-label-icon ${this.options.iconClass}`;
6460
+ }
6461
+ else {
6462
+ icon.textContent = this.options.icon || "โ—";
6463
+ }
6464
+ if (iconBackground) {
6465
+ const color = Color.fromCssColorString(iconColor || "white") || Color.WHITE;
6466
+ const luminance = 0.2126 * color.red + 0.7152 * color.green + 0.0722 * color.blue;
6467
+ const background = (luminance > 0.5 ? color.darken(ICON_BACKGROUND_BLEND, new Color()) :
6468
+ color.brighten(ICON_BACKGROUND_BLEND, new Color()));
6469
+ const size = (this.options.iconSize || 24) * ((_p = this.options.iconScale) !== null && _p !== void 0 ? _p : 1);
6470
+ icon.style.backgroundColor = background.withAlpha(ICON_BACKGROUND_ALPHA).toCssColorString();
6471
+ icon.style.borderRadius = "50%";
6472
+ icon.style.padding = `${size / 3}px`;
6473
+ icon.style.display = "inline-flex";
6474
+ icon.style.alignItems = "center";
6475
+ icon.style.justifyContent = "center";
6476
+ icon.style.minWidth = `${size}px`;
6477
+ icon.style.minHeight = `${size}px`;
6478
+ icon.style.boxSizing = "content-box";
6479
+ }
6480
+ if (this.options.iconScale === 0) {
6481
+ icon.style.display = "none";
6482
+ }
6483
+ makeClickable(icon);
6484
+ // Keep badges outside the glyph so font classes and image masks cannot affect them.
6485
+ iconContainer = document.createElement("span");
6486
+ iconContainer.style.cssText = "position:relative;display:inline-flex;flex:none;";
6487
+ iconContainer.appendChild(icon);
6488
+ content.appendChild(iconContainer);
6489
+ }
6490
+ if (presentation !== "icon") {
6491
+ textElement = document.createElement("span");
6492
+ textElement.className = "entity-label-text";
6493
+ const textStyle = `white-space:pre-wrap;overflow-wrap:anywhere;max-width:${TEXT_MAX_WIDTH}px;width:max-content;flex-shrink:0;`;
6494
+ textElement.style.cssText = textStyle;
6495
+ textElement.textContent = originalText;
6496
+ if (presentation === "text")
6497
+ makeClickable(textElement);
6498
+ content.appendChild(textElement);
6499
+ if ((_q = this.options.cluster) === null || _q === void 0 ? void 0 : _q.enabled) {
6500
+ sourceText = document.createElement("span");
6501
+ sourceText.className = "entity-label-source-text";
6502
+ sourceText.setAttribute("aria-hidden", "true");
6503
+ sourceText.style.cssText = textStyle + "position:absolute;visibility:hidden;pointer-events:none;";
6504
+ sourceText.textContent = originalText;
6505
+ content.appendChild(sourceText);
6506
+ // Measure the original unwrapped text, never a rendered cluster summary.
6507
+ sourceNaturalText = document.createElement("span");
6508
+ sourceNaturalText.setAttribute("aria-hidden", "true");
6509
+ sourceNaturalText.style.cssText = "position:absolute;visibility:hidden;pointer-events:none;white-space:pre;width:max-content;";
6510
+ sourceNaturalText.textContent = originalText;
6511
+ content.appendChild(sourceNaturalText);
6512
+ }
6513
+ }
6514
+ const countElement = document.createElement("span");
6515
+ countElement.className = "entity-label-count";
6516
+ 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;";
6517
+ if (iconContainer) {
6518
+ countElement.style.cssText += "position:absolute;left:100%;top:0;transform:translate(-50%,-50%);";
6519
+ iconContainer.appendChild(countElement);
6520
+ }
6521
+ if (!layers.has(this.viewer)) {
6522
+ layers.set(this.viewer, new Layer(this.viewer));
6523
+ }
6524
+ const layer = layers.get(this.viewer);
5708
6525
  let ele = null;
5709
6526
  let lineEle = null;
5710
6527
  if (this.label) {
@@ -5712,11 +6529,22 @@ var EntityLabel;
5712
6529
  }
5713
6530
  else {
5714
6531
  ele = document.createElement("div");
5715
- ele.innerHTML = labelContent;
6532
+ ele.appendChild(content);
6533
+ if (!iconContainer) {
6534
+ ele.appendChild(countElement);
6535
+ }
6536
+ ele.className = "entity-label";
6537
+ ele.dataset.entityId = ((_r = this.rego) === null || _r === void 0 ? void 0 : _r.entityId) || "";
6538
+ ele.dataset.presentation = presentation;
6539
+ if (this.groupId) {
6540
+ ele.dataset.groupId = this.groupId;
6541
+ }
5716
6542
  ele.setAttribute("style", `
5717
6543
  position: absolute;
5718
6544
  z-index: 0;
5719
6545
  display: none;
6546
+ align-items: center;
6547
+ gap: 6px;
5720
6548
  pointer-events: none;
5721
6549
  padding: 6px 8px;
5722
6550
  border-radius: 6px;
@@ -5725,9 +6553,10 @@ var EntityLabel;
5725
6553
  border-radius: 9px;
5726
6554
  color: #ffffff;
5727
6555
  text-shadow: 0 0 5px rgba(0, 0, 0, 0.8);
5728
- }`);
6556
+ `);
5729
6557
  this.label = ele;
5730
- this.viewer.container.appendChild(ele);
6558
+ ele.style.width = "max-content";
6559
+ layer.element.appendChild(ele);
5731
6560
  }
5732
6561
  if (this.line) {
5733
6562
  lineEle = this.line;
@@ -5744,7 +6573,10 @@ var EntityLabel;
5744
6573
  box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
5745
6574
  `);
5746
6575
  this.line = lineEle;
5747
- // We'll draw a ball at the bottom of the line to make it look like it's connecting to the entity.
6576
+ lineEle.className = "entity-label-leader";
6577
+ if (this.options.leaderColor)
6578
+ lineEle.style.background = this.options.leaderColor;
6579
+ // A separate anchor keeps the stem out of the hollow centre when proximity opens the ring.
5748
6580
  const lineEleNode = document.createElement("div");
5749
6581
  lineEleNode.setAttribute("style", `
5750
6582
  position: absolute;
@@ -5757,35 +6589,20 @@ var EntityLabel;
5757
6589
  top: -2px;
5758
6590
  left: -2px;
5759
6591
  `);
5760
- lineEle.appendChild(lineEleNode);
5761
- this.viewer.container.appendChild(lineEle);
6592
+ lineEleNode.className = "entity-label-anchor";
6593
+ // SVG preserves fractional strokes in scaled containers where CSS borders round to pixels.
6594
+ const ringSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
6595
+ ringSvg.style.cssText = "position:absolute;inset:0;width:100%;height:100%;overflow:visible;";
6596
+ ringSvg.setAttribute("aria-hidden", "true");
6597
+ ringSvg.setAttribute("preserveAspectRatio", "none");
6598
+ const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
6599
+ circle.setAttribute("fill", "none");
6600
+ ringSvg.appendChild(circle);
6601
+ lineEleNode.appendChild(ringSvg);
6602
+ this.anchor = lineEleNode;
6603
+ layer.element.appendChild(lineEleNode);
6604
+ layer.element.appendChild(lineEle);
5762
6605
  }
5763
- let _lastDistance = null;
5764
- let _lastCameraPos = null;
5765
- const getDistance = () => {
5766
- var _a;
5767
- if (this.disposed) {
5768
- return null;
5769
- }
5770
- const cameraPos = (_a = this.viewer.camera) === null || _a === void 0 ? void 0 : _a.position;
5771
- if (isNaN(cameraPos === null || cameraPos === void 0 ? void 0 : cameraPos.x)) {
5772
- return null;
5773
- }
5774
- // If camera hasn't moved much then we can use the last distance.
5775
- const TOLERANCE = 0.1;
5776
- if (_lastCameraPos) {
5777
- const totalMovement = Math.abs(_lastCameraPos.x - cameraPos.x) +
5778
- Math.abs(_lastCameraPos.y - cameraPos.y) +
5779
- Math.abs(_lastCameraPos.z - cameraPos.z);
5780
- if (totalMovement < TOLERANCE) {
5781
- return _lastDistance;
5782
- }
5783
- }
5784
- const distance = Cartesian3.distance(cameraPos, this._pos3d);
5785
- _lastDistance = distance;
5786
- _lastCameraPos = cameraPos.clone();
5787
- return distance;
5788
- };
5789
6606
  let MAX_DISTANCE = Math.max(15000, sizeInM * 35);
5790
6607
  if (visual && visual instanceof Entity) {
5791
6608
  // Consistent sized things.
@@ -5793,123 +6610,399 @@ var EntityLabel;
5793
6610
  MAX_DISTANCE = Infinity;
5794
6611
  }
5795
6612
  }
5796
- const updateLabel = () => {
5797
- var _a;
5798
- if (this.disposed || isNaN((_a = this._pos3d) === null || _a === void 0 ? void 0 : _a.x)) {
5799
- return;
6613
+ const fitLeader = (anchorX, anchorY, desired, width, height, radius = 0) => {
6614
+ const above = Math.max(0, anchorY - height);
6615
+ const below = Math.max(0, this.viewer.canvas.clientHeight - anchorY - height);
6616
+ // Keep a short visible stem before switching sides; never aim it at the cursor or a box corner.
6617
+ const upwards = above >= Math.max(radius ? radius + 4 : 0, Math.min(8, desired)) || above >= below;
6618
+ const length = Math.min(desired, upwards ? above : below);
6619
+ const endY = anchorY + (upwards ? -length : length);
6620
+ return {
6621
+ x: Math.max(0, Math.min(anchorX, this.viewer.canvas.clientWidth - width)),
6622
+ y: upwards ? endY - height : endY, endY
6623
+ };
6624
+ };
6625
+ let cursorMotion = this.cursorMotion;
6626
+ const project = () => {
6627
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
6628
+ if (this.disposed || !this.viewer || this.viewer.isDestroyed() || ((_a = this.visible) === null || _a === void 0 ? void 0 : _a.call(this)) === false ||
6629
+ (presentation === "icon" && this.options.iconScale === 0)) {
6630
+ return null;
5800
6631
  }
5801
- if (!this.viewer || this.viewer.isDestroyed()) {
5802
- this.Dispose();
5803
- return;
6632
+ if (this.Transient && !this.Persistent && this.hoverFade.to === 0 &&
6633
+ (this.hoverFade.from === 0 || Date.now() - this.hoverFade.start >= 120)) {
6634
+ return null;
5804
6635
  }
5805
- const distance = getDistance();
5806
- if (isNaN(distance) || (!this.prePositioned && distance >= MAX_DISTANCE)) {
5807
- ele.style.display = "none";
5808
- lineEle.style.display = "none";
5809
- return;
6636
+ // Moving point/model positions must be sampled at the current clock time.
6637
+ if (!this.prePositioned && visual instanceof Entity && visual.position) {
6638
+ const position = getValue(this.viewer, visual.position);
6639
+ if (position)
6640
+ this._pos3d = DrawingUtils.EnsurePosHeight({
6641
+ desiredHeightRef: HeightReference.NONE, heightRef, pos3d: position, viewer: this.viewer
6642
+ });
5810
6643
  }
5811
- const CESIUM = Cesium;
5812
- const _3dTo2D = SceneTransforms.hasOwnProperty("wgs84ToWindowCoordinates") ? CESIUM.SceneTransforms.wgs84ToWindowCoordinates : CESIUM.SceneTransforms.worldToWindowCoordinates;
5813
- const lineStartPos2d = _3dTo2D(this.viewer.scene, this._pos3d);
5814
- // Invalid point.
5815
- if (isNaN(lineStartPos2d === null || lineStartPos2d === void 0 ? void 0 : lineStartPos2d.x)) {
5816
- ele.style.transform = "";
5817
- ele.style.opacity = "0";
5818
- lineEle.style.display = "none";
5819
- return;
6644
+ if (!Number.isFinite((_b = this._pos3d) === null || _b === void 0 ? void 0 : _b.x)) {
6645
+ return null;
6646
+ }
6647
+ const camera = this.viewer.camera;
6648
+ const cameraPosition = camera.positionWC || camera.position;
6649
+ const relative = Cartesian3.subtract(this._pos3d, cameraPosition, new Cartesian3());
6650
+ const depth = Cartesian3.dot(relative, camera.directionWC || camera.direction);
6651
+ const distance = Cartesian3.magnitude(relative);
6652
+ if (!Number.isFinite(distance) || depth <= 0 || distance < ((_c = this.options.near) !== null && _c !== void 0 ? _c : 0) ||
6653
+ distance > ((_d = this.options.far) !== null && _d !== void 0 ? _d : Infinity) || (!this.prePositioned && distance >= MAX_DISTANCE)) {
6654
+ return null;
6655
+ }
6656
+ const transforms = SceneTransforms;
6657
+ const projectPosition = transforms.wgs84ToWindowCoordinates || transforms.worldToWindowCoordinates;
6658
+ const anchor = projectPosition(this.viewer.scene, this._pos3d);
6659
+ if (!Number.isFinite(anchor === null || anchor === void 0 ? void 0 : anchor.x) || !Number.isFinite(anchor === null || anchor === void 0 ? void 0 : anchor.y)) {
6660
+ return null;
6661
+ }
6662
+ if (leader && (anchor.x < 0 || anchor.y < 0 || anchor.x >= this.viewer.canvas.clientWidth || anchor.y >= this.viewer.canvas.clientHeight))
6663
+ return null;
6664
+ const transientPivot = (this.Transient && !this.Persistent ?
6665
+ (this.hoverFade.to === 0 ? this.retainedHoverPosition : (_e = this.visualRegister) === null || _e === void 0 ? void 0 : _e.GetLabelHoverPosition((_f = this.rego) === null || _f === void 0 ? void 0 : _f.entityId, (_g = this.rego) === null || _g === void 0 ? void 0 : _g.menuItemId)) : undefined);
6666
+ const hover = (presentation !== "icon" ?
6667
+ (this.Transient && !this.Persistent && this.hoverFade.to === 0 ? this.retainedHoverPosition :
6668
+ (_h = this.visualRegister) === null || _h === void 0 ? void 0 : _h.GetLabelHoverPosition((_j = this.rego) === null || _j === void 0 ? void 0 : _j.entityId, (_k = this.rego) === null || _k === void 0 ? void 0 : _k.menuItemId)) : undefined);
6669
+ const reused = !this.Transient || this.Persistent;
6670
+ const now = Date.now();
6671
+ const animateCursor = () => {
6672
+ const progress = Math.min(1, Math.max(0, (now - cursorMotion.start) / 200));
6673
+ const eased = progress * progress * (3 - 2 * progress);
6674
+ cursorMotion.amount = cursorMotion.from + (cursorMotion.target - cursorMotion.from) * eased;
6675
+ return progress;
6676
+ };
6677
+ animateCursor();
6678
+ const cursorTarget = reused && hover ? 1 : 0;
6679
+ if (cursorMotion.target !== cursorTarget) {
6680
+ cursorMotion = { ...cursorMotion, from: cursorMotion.amount, target: cursorTarget, start: now };
6681
+ }
6682
+ if (hover) {
6683
+ cursorMotion.position = { x: hover.x, y: hover.y };
6684
+ }
6685
+ if (animateCursor() < 1 && cursorMotion.from !== cursorMotion.target) {
6686
+ this.viewer.scene.requestRender();
5820
6687
  }
5821
- // Offset start by the pixel offset.
5822
- lineStartPos2d.x += pixelOffset.x;
5823
- lineStartPos2d.y += pixelOffset.y;
5824
- ele.style.display = "block";
5825
- lineEle.style.display = "block";
5826
- // Calculate opacity to apply based on distance between camera and pos3d.
5827
- // We'll make the label less transparent when further away.
5828
- const opacity = this.prePositioned || MAX_DISTANCE == Infinity ? 1 : 1 - (distance / MAX_DISTANCE);
5829
- if (opacity && opacity > 0.15) {
5830
- this.label.style.opacity = `${opacity}`;
5831
- // Calc where the line should end.
5832
- // We get the pos3d and add 'n' to it, then turn it back into 2d.
5833
- let lineEndPos3d = this._pos3d.clone();
5834
- const lineEndPoint = Cartographic.fromCartesian(lineEndPos3d);
5835
- let addHeight = null;
5836
- // If we're pre-positioned that means the user is hovering over the entity.
5837
- // We will keep the height low to avoid looking goofy.
5838
- if (this.prePositioned) {
5839
- addHeight = distance / 24;
6688
+ this.cursorMotion = cursorMotion;
6689
+ const cursorAmount = cursorMotion.amount;
6690
+ leader = normalLeader || cursorAmount > 0;
6691
+ let x = ((_l = transientPivot === null || transientPivot === void 0 ? void 0 : transientPivot.x) !== null && _l !== void 0 ? _l : anchor.x) + pixelOffset.x;
6692
+ let y = ((_m = transientPivot === null || transientPivot === void 0 ? void 0 : transientPivot.y) !== null && _m !== void 0 ? _m : anchor.y) + pixelOffset.y;
6693
+ if (cursorAmount > 0) {
6694
+ // A leader-less label starts centred on its anchor; preserve that box as it lifts away.
6695
+ if (!normalLeader) {
6696
+ x -= ele.offsetWidth / layer.coordinates.scaleX / 2;
6697
+ y += ele.offsetHeight / layer.coordinates.scaleY / 2;
6698
+ }
6699
+ x += (cursorMotion.position.x + pixelOffset.x - x) * cursorAmount;
6700
+ y += (cursorMotion.position.y + pixelOffset.y - y) * cursorAmount;
6701
+ }
6702
+ const anchorX = x;
6703
+ const anchorY = y;
6704
+ let leaderLength;
6705
+ if (leader) {
6706
+ if (!normalLeader) {
6707
+ leaderLength = 0;
6708
+ }
6709
+ else if (this.options.leaderLength != null) {
6710
+ leaderLength = Math.max(0, this.options.leaderLength);
6711
+ }
6712
+ else if (transientPivot) {
6713
+ // Cursor callouts must not jump when the next hover target has a different extent.
6714
+ leaderLength = 96;
5840
6715
  }
5841
6716
  else {
5842
- // If known then calculate based on size.
5843
- // If we're close then we'll favour the distance over the size.
5844
- if (sizeInM && sizeInM > 0) {
5845
- addHeight = Math.min(distance / 5, sizeInM * 1.2);
5846
- }
5847
- // If unknown then calculate based on camera height or distance.
5848
- // Radius is unknown for Tileset things. We could bake the radius in possibly as a feature property.
5849
- // We have to make it work for both labels against tall skyscrapers, and up-close assembly models.
5850
- else {
5851
- addHeight = Math.min(distance, this.viewer.camera.positionCartographic.height) / 7;
5852
- }
5853
- }
5854
- // The ratio is roughly 1m = 11px on the screen.
5855
- // So if we want to add additional labels then we'll need to adjust the height.
5856
- if (additionalTexts === null || additionalTexts === void 0 ? void 0 : additionalTexts.length) {
5857
- // Each additional text is about 16px tall.
5858
- // So we get the total px height and divide by 11 to get the meters.
5859
- addHeight += ((additionalTexts.length * 16) / 11);
5860
- }
5861
- // Safety check.
5862
- if (!addHeight || addHeight <= 0) {
5863
- addHeight = 0.001;
5864
- }
5865
- lineEndPoint.height += addHeight;
5866
- lineEndPos3d = Cartographic.toCartesian(lineEndPoint);
5867
- const CESIUM = Cesium;
5868
- const _3dTo2D = SceneTransforms.hasOwnProperty("wgs84ToWindowCoordinates") ? CESIUM.SceneTransforms.wgs84ToWindowCoordinates : CESIUM.SceneTransforms.worldToWindowCoordinates;
5869
- let lineEndPos2d = _3dTo2D(this.viewer.scene, lineEndPos3d);
5870
- // Invalid point.
5871
- if (isNaN(lineEndPos2d === null || lineEndPos2d === void 0 ? void 0 : lineEndPos2d.x)) {
5872
- ele.style.transform = "";
5873
- ele.style.opacity = "0";
5874
- lineEle.style.display = "none";
5875
- return;
6717
+ const point = Cartographic.fromCartesian(this._pos3d);
6718
+ const height = (this.prePositioned ? distance / 24 : sizeInM > 0 ?
6719
+ Math.min(distance / 5, sizeInM * 1.2) : Math.min(distance, camera.positionCartographic.height) / 7);
6720
+ point.height += Math.max(0.001, height);
6721
+ const end = projectPosition(this.viewer.scene, Cartographic.toCartesian(point));
6722
+ // A tall world-space leader can cross the camera plane while its anchor remains visible.
6723
+ leaderLength = Number.isFinite(end === null || end === void 0 ? void 0 : end.x) && Number.isFinite(end === null || end === void 0 ? void 0 : end.y) ? Math.hypot(end.x - anchor.x, end.y - anchor.y) : 24;
6724
+ // Scale after projection so large areas and routes get shorter leaders at every zoom.
6725
+ if (visual instanceof Entity && (visual.polygon || visual.polyline))
6726
+ leaderLength /= 3;
6727
+ }
6728
+ if (cursorAmount > 0) {
6729
+ leaderLength += (96 - leaderLength) * cursorAmount;
6730
+ }
6731
+ y = anchorY - leaderLength;
6732
+ }
6733
+ else if (hover && !reused) {
6734
+ x = hover.x + pixelOffset.x;
6735
+ y = hover.y + pixelOffset.y - 24;
6736
+ }
6737
+ // Hidden participants remain measurable; visibility never removes their layout boxes.
6738
+ const coordinates = layer.coordinates;
6739
+ const width = ele.offsetWidth / coordinates.scaleX;
6740
+ const height = ele.offsetHeight / coordinates.scaleY;
6741
+ let clusterBounds;
6742
+ if ((_o = this.options.cluster) === null || _o === void 0 ? void 0 : _o.enabled) {
6743
+ const textWidth = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetWidth) || 0;
6744
+ const textHeight = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetHeight) || 0;
6745
+ const iconWidth = (iconElement === null || iconElement === void 0 ? void 0 : iconElement.offsetWidth) || 0;
6746
+ const iconHeight = (iconElement === null || iconElement === void 0 ? void 0 : iconElement.offsetHeight) || 0;
6747
+ const gap = sourceText && iconWidth ? 6 : 0;
6748
+ const above = presentation === "icon-text-above";
6749
+ // Match the fixed content gap and label padding, excluding the count badge.
6750
+ const baseWidth = ((above ? Math.max(textWidth, iconWidth) : textWidth + iconWidth + gap) + 16) / coordinates.scaleX;
6751
+ const baseHeight = ((above ? textHeight + iconHeight + gap : Math.max(textHeight, iconHeight)) + 12) / coordinates.scaleY;
6752
+ clusterBounds = {
6753
+ x: leader ? x : x - baseWidth / 2,
6754
+ y: leader ? y - baseHeight : y - baseHeight / 2, width: baseWidth, height: baseHeight
6755
+ };
6756
+ }
6757
+ if (!leader) {
6758
+ x -= width / 2;
6759
+ y -= height / 2;
6760
+ }
6761
+ // Membership uses the unclamped source footprint, never the summary's changing dimensions.
6762
+ if (leader)
6763
+ ({ x, y } = fitLeader(anchorX, anchorY, leaderLength, width, height));
6764
+ const bounds = clusterBounds || { x, y, width, height };
6765
+ if (!leader && (bounds.x + bounds.width < 0 || bounds.y + bounds.height < 0 || bounds.x > this.viewer.canvas.clientWidth || bounds.y > this.viewer.canvas.clientHeight)) {
6766
+ return null;
6767
+ }
6768
+ const state = (_p = this.visualRegister) === null || _p === void 0 ? void 0 : _p.GetState({ entityId: (_q = this.rego) === null || _q === void 0 ? void 0 : _q.entityId, menuItemId: (_r = this.rego) === null || _r === void 0 ? void 0 : _r.menuItemId });
6769
+ const opacity = this.prePositioned || MAX_DISTANCE === Infinity ? 1 : 1 - distance / MAX_DISTANCE;
6770
+ return {
6771
+ canFlip: leader && presentation !== "icon", hasLeader: leader,
6772
+ textWrapped: ((sourceNaturalText === null || sourceNaturalText === void 0 ? void 0 : sourceNaturalText.offsetWidth) || 0) > TEXT_MAX_WIDTH,
6773
+ transient: this.Transient || Boolean(hover) || cursorAmount > 0,
6774
+ extendedGeometry: visual instanceof Entity && Boolean(visual.polygon || visual.polyline || visual.corridor),
6775
+ viewportWidth: this.viewer.canvas.clientWidth,
6776
+ 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,
6778
+ // Priority shows the hovered member individually without dragging its cluster along.
6779
+ opacity, priority: hover || cursorAmount > 0 || (state === null || state === void 0 ? void 0 : state.highlighted) ? 1 : 0, options: this.options, text: originalText, clusterBounds
6780
+ };
6781
+ };
6782
+ let displacement = { x: 0, y: 0 };
6783
+ let displayedSummaryMore = false;
6784
+ let nudge = { x: 0, y: 0, fromX: 0, fromY: 0, targetX: 0, targetY: 0, start: 0 };
6785
+ 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;
6787
+ const show = (placement === null || placement === void 0 ? void 0 : placement.visible) && placement.opacity > 0;
6788
+ clickEntityIds = show ? placement.entityIds : [];
6789
+ ele.style.visibility = show ? "visible" : "hidden";
6790
+ lineEle.style.display = show && leader ? "block" : "none";
6791
+ this.anchor.style.display = show && leader ? "block" : "none";
6792
+ if (!show) {
6793
+ displacement = { x: 0, y: 0 };
6794
+ nudge = { x: 0, y: 0, fromX: 0, fromY: 0, targetX: 0, targetY: 0, start: 0 };
6795
+ if (!this.Transient) {
6796
+ this.ringAnimation = undefined;
5876
6797
  }
5877
- // Offset end by the pixel offset.
5878
- lineEndPos2d.x += pixelOffset.x;
5879
- lineEndPos2d.y += pixelOffset.y;
5880
- // If out of the screen then we can hide it.
5881
- const startOutOfScreen = (lineStartPos2d.x < 0 || lineStartPos2d.x > this.viewer.canvas.clientWidth ||
5882
- lineStartPos2d.y < 0 || lineStartPos2d.y > this.viewer.canvas.clientHeight);
5883
- const endOutOfScreen = (lineEndPos2d.x < 0 || lineEndPos2d.x > this.viewer.canvas.clientWidth ||
5884
- lineEndPos2d.y < 0 || lineEndPos2d.y > this.viewer.canvas.clientHeight);
5885
- if (startOutOfScreen && endOutOfScreen) {
5886
- ele.style.transform = "";
5887
- ele.style.opacity = "0";
5888
- lineEle.style.display = "none";
5889
- return;
6798
+ return;
6799
+ }
6800
+ 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)) {
6803
+ 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
+ // Measure the summary's footprint next frame without replacing each member's source text.
6813
+ this.viewer.scene.requestRender();
6814
+ }
6815
+ const frame = placement.frame;
6816
+ const coordinates = layer.coordinates;
6817
+ const opacity = placement.opacity * this.HoverOpacity();
6818
+ ele.style.opacity = String(opacity);
6819
+ ele.style.zIndex = String(placement.zIndex);
6820
+ ele.dataset.clusterCount = String(placement.members.length || 1);
6821
+ countElement.style.display = placement.members.length ? "block" : "none";
6822
+ countElement.style.order = placement.textSide === "left" ? "-1" : "0";
6823
+ const count = placement.members.length ? String(placement.members.length) : "";
6824
+ if (countElement.textContent !== count) {
6825
+ countElement.textContent = count;
6826
+ // The badge participates in layout, so its new width needs another measurement.
6827
+ this.viewer.scene.requestRender();
6828
+ }
6829
+ if (iconContainer) {
6830
+ const badgeWidth = countElement.offsetWidth;
6831
+ const iconWidth = iconElement.offsetWidth;
6832
+ const narrow = iconWidth < iconElement.offsetHeight * 0.6;
6833
+ const clearance = badgeWidth ? (narrow ? badgeWidth / 2 + 2 : Math.max(0, badgeWidth / 2 - iconWidth / 4)) : 0;
6834
+ countElement.style.left = `calc(100% + ${clearance}px)`;
6835
+ // Reserve the badge overhang so adjacent text and viewport edges remain clear.
6836
+ iconContainer.style.marginTop = `${countElement.offsetHeight / 2}px`;
6837
+ iconContainer.style.marginRight = `${badgeWidth / 2 + clearance}px`;
6838
+ }
6839
+ // Summary and badge changes must fit on their first visible frame, including after resize.
6840
+ const ring = this.options.anchorRing;
6841
+ const pointer = (layer.pointer || (cursorMotion.target ? cursorMotion.position : undefined) || (this.Transient && !this.Persistent ?
6842
+ (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));
6843
+ 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);
6844
+ 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));
6845
+ const targetRadius = normalRadius + (15.4 - normalRadius) * cursorMotion.amount;
6846
+ const now = Date.now();
6847
+ const duration = (_j = ring === null || ring === void 0 ? void 0 : ring.transitionDuration) !== null && _j !== void 0 ? _j : 200;
6848
+ const sharedRing = this.Transient && !this.Persistent && this.hoverFade.to === 1 ? this.hoverRingState : undefined;
6849
+ let ringAnimation = sharedRing ? sharedRing.animation : this.ringAnimation;
6850
+ if (!ringAnimation) {
6851
+ ringAnimation = { target: 0, start: now, outer: 2.5, inner: 0, fromOuter: 2.5, fromInner: 0 };
6852
+ }
6853
+ const animateRing = () => {
6854
+ const progress = duration > 0 ? Math.min(1, Math.max(0, (now - ringAnimation.start) / duration)) : 1;
6855
+ const eased = progress * progress * (3 - 2 * progress);
6856
+ ringAnimation.outer = ringAnimation.fromOuter + ((ringAnimation.target || 2.5) - ringAnimation.fromOuter) * eased;
6857
+ ringAnimation.inner = ringAnimation.fromInner + (Math.max(0, ringAnimation.target - 2) - ringAnimation.fromInner) * eased;
6858
+ return progress;
6859
+ };
6860
+ animateRing();
6861
+ if (ringAnimation.target !== targetRadius) {
6862
+ ringAnimation = {
6863
+ ...ringAnimation,
6864
+ target: targetRadius,
6865
+ start: now,
6866
+ fromOuter: ringAnimation.outer,
6867
+ fromInner: ringAnimation.inner
6868
+ };
6869
+ }
6870
+ // Animate the hole and stem together; no CSS transition may lag behind a moving world anchor.
6871
+ if (animateRing() < 1 && (ringAnimation.fromOuter !== (targetRadius || 2.5) || ringAnimation.fromInner !== Math.max(0, targetRadius - 2))) {
6872
+ this.viewer.scene.requestRender();
6873
+ }
6874
+ this.ringAnimation = ringAnimation;
6875
+ if (sharedRing) {
6876
+ sharedRing.animation = ringAnimation;
6877
+ }
6878
+ 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);
6880
+ const position = { x: (stem || frame).x, y: (stem || frame).y };
6881
+ const width = ele.offsetWidth / coordinates.scaleX;
6882
+ const height = ele.offsetHeight / coordinates.scaleY;
6883
+ if (leader && placement.textSide === "left") {
6884
+ position.x = Math.max(0, Math.min(frame.anchorX - width, this.viewer.canvas.clientWidth - width));
6885
+ }
6886
+ const gap = (_l = (_k = this.options.pointerAvoidance) === null || _k === void 0 ? void 0 : _k.gap) !== null && _l !== void 0 ? _l : 12;
6887
+ const containsPointer = (x, y, margin) => {
6888
+ return pointer && pointer.x >= x - margin && pointer.x <= x + width + margin && pointer.y >= y - margin && pointer.y <= y + height + margin;
6889
+ };
6890
+ const passive = !selectOnClick || !this.visualRegister;
6891
+ if (passive && ((_m = this.options.pointerAvoidance) === null || _m === void 0 ? void 0 : _m.enabled) !== false && pointer &&
6892
+ (containsPointer(position.x, position.y, gap) || containsPointer(position.x + displacement.x, position.y + displacement.y, gap))) {
6893
+ if (containsPointer(position.x + displacement.x, position.y + displacement.y, 0)) {
6894
+ // Choose against the original box and retain the offset until the pointer leaves both boxes.
6895
+ const candidates = [
6896
+ { x: 0, y: pointer.y - gap - height - position.y },
6897
+ { x: 0, y: pointer.y + gap - position.y },
6898
+ { x: pointer.x - gap - width - position.x, y: 0 },
6899
+ { x: pointer.x + gap - position.x, y: 0 }
6900
+ ].filter(offset => position.x + offset.x >= 0 && position.y + offset.y >= 0 &&
6901
+ position.x + offset.x + width <= this.viewer.canvas.clientWidth && position.y + offset.y + height <= this.viewer.canvas.clientHeight)
6902
+ .sort((a, b) => Math.hypot(a.x, a.y) - Math.hypot(b.x, b.y));
6903
+ displacement = candidates[0] || { x: 0, y: 0 };
5890
6904
  }
5891
- // Position the label above the line end point.
5892
- this.label.style.left = `${lineEndPos2d.x}px`;
5893
- this.label.style.top = `${lineEndPos2d.y}px`;
5894
- // Update line position and size.
5895
- const dx = lineEndPos2d.x - lineStartPos2d.x;
5896
- const dy = lineEndPos2d.y - lineStartPos2d.y;
5897
- const length = Math.sqrt(dx * dx + dy * dy);
5898
- const angle = Math.atan2(dy, dx) - Math.PI / 2;
5899
- this.line.style.left = `${lineStartPos2d.x}px`;
5900
- this.line.style.top = `${lineStartPos2d.y}px`;
5901
- this.line.style.height = `${length}px`;
5902
- this.line.style.transform = `rotate(${angle}rad)`;
5903
- this.line.style.transformOrigin = 'top center';
5904
6905
  }
5905
6906
  else {
5906
- ele.style.transform = "";
5907
- ele.style.opacity = "0";
5908
- lineEle.style.display = "none";
6907
+ displacement = { x: 0, y: 0 };
6908
+ }
6909
+ const nudgeDuration = (_p = (_o = this.options.pointerAvoidance) === null || _o === void 0 ? void 0 : _o.transitionDuration) !== null && _p !== void 0 ? _p : 200;
6910
+ const animateNudge = () => {
6911
+ const progress = now < nudge.start ? 0 : nudgeDuration > 0 ? Math.min(1, (now - nudge.start) / nudgeDuration) : 1;
6912
+ const eased = progress * progress * (3 - 2 * progress);
6913
+ nudge.x = nudge.fromX + (nudge.targetX - nudge.fromX) * eased;
6914
+ nudge.y = nudge.fromY + (nudge.targetY - nudge.fromY) * eased;
6915
+ return progress;
6916
+ };
6917
+ animateNudge();
6918
+ if (nudge.targetX !== displacement.x || nudge.targetY !== displacement.y) {
6919
+ nudge = {
6920
+ ...nudge,
6921
+ fromX: nudge.x,
6922
+ fromY: nudge.y,
6923
+ targetX: displacement.x,
6924
+ 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)
6926
+ };
6927
+ }
6928
+ if (animateNudge() < 1 && (nudge.fromX !== nudge.targetX || nudge.fromY !== nudge.targetY)) {
6929
+ this.viewer.scene.requestRender();
6930
+ }
6931
+ position.x += nudge.x;
6932
+ position.y += nudge.y;
6933
+ ele.style.left = `${coordinates.x + position.x * coordinates.scaleX}px`;
6934
+ ele.style.top = `${coordinates.y + position.y * coordinates.scaleY}px`;
6935
+ const clickTarget = content.querySelector('[role="button"]');
6936
+ 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}`);
6938
+ }
6939
+ if (leader) {
6940
+ const nodeRadius = ringAnimation.outer;
6941
+ const anchor = this.anchor;
6942
+ anchor.style.left = `${coordinates.x + (frame.anchorX - nodeRadius) * coordinates.scaleX}px`;
6943
+ anchor.style.top = `${coordinates.y + (frame.anchorY - nodeRadius) * coordinates.scaleY}px`;
6944
+ anchor.style.width = `${nodeRadius * 2 * coordinates.scaleX}px`;
6945
+ anchor.style.height = `${nodeRadius * 2 * coordinates.scaleY}px`;
6946
+ anchor.style.boxSizing = "border-box";
6947
+ anchor.style.background = "transparent";
6948
+ const svg = anchor.firstElementChild;
6949
+ svg.setAttribute("viewBox", `0 0 ${nodeRadius * 2} ${nodeRadius * 2}`);
6950
+ const circle = svg.firstElementChild;
6951
+ circle.setAttribute("cx", String(nodeRadius));
6952
+ circle.setAttribute("cy", String(nodeRadius));
6953
+ circle.setAttribute("r", String((nodeRadius + ringAnimation.inner) / 2));
6954
+ circle.setAttribute("stroke", this.options.leaderColor || "rgba(255, 255, 255, 0.8)");
6955
+ circle.setAttribute("stroke-width", String(nodeRadius - ringAnimation.inner));
6956
+ anchor.style.boxShadow = radius ? "none" : "0 0 5px rgba(0, 0, 0, 0.3)";
6957
+ anchor.style.opacity = String(opacity * (normalLeader ? 1 : cursorMotion.amount));
6958
+ anchor.style.zIndex = String(placement.zIndex);
6959
+ // 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;
6962
+ const dx = endX - frame.anchorX, dy = endY - frame.anchorY;
6963
+ const length = Math.hypot(dx, dy);
6964
+ const leaderAngle = Math.atan2(dy, dx);
6965
+ const pointerDx = pointer ? pointer.x - frame.anchorX : 0;
6966
+ const pointerDy = pointer ? pointer.y - frame.anchorY : 0;
6967
+ const desiredAngle = Math.hypot(pointerDx, pointerDy) > 0.5 ? Math.atan2(pointerDy, pointerDx) + Math.PI - leaderAngle : 0;
6968
+ const wrappedAngle = Math.atan2(Math.sin(desiredAngle), Math.cos(desiredAngle));
6969
+ // Keep the stem inside the 126-degree arc, away from antialiased endpoints.
6970
+ const arcFraction = 0.35;
6971
+ 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);
6973
+ 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)));
6975
+ const blend = duration > 0 ? 1 - Math.exp(-elapsed / (duration / 3)) : 1;
6976
+ 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);
6979
+ ringAnimation.arcTime = now;
6980
+ if (ringAnimation.arcOffset !== targetOffset || ringAnimation.arcOpen !== targetOpen) {
6981
+ this.viewer.scene.requestRender();
6982
+ }
6983
+ const circumference = Math.PI * (nodeRadius + ringAnimation.inner);
6984
+ const filledFraction = 1 - ringAnimation.arcOpen * (1 - arcFraction);
6985
+ circle.setAttribute("stroke-dasharray", `${circumference * filledFraction} ${circumference * (1 - filledFraction)}`);
6986
+ circle.setAttribute("transform", `rotate(${(leaderAngle + ringAnimation.arcOffset - Math.PI * filledFraction) * 180 / Math.PI} ${nodeRadius} ${nodeRadius})`);
6987
+ const cut = Math.min(radius, length);
6988
+ const startX = frame.anchorX + (length ? dx / length * cut : 0);
6989
+ 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);
5909
7001
  }
5910
7002
  };
5911
- this.renderRemoval = this.viewer.scene.postUpdate.addEventListener(updateLabel);
5912
- updateLabel();
7003
+ ele.style.display = "flex";
7004
+ ele.style.visibility = "hidden";
7005
+ this.renderRemoval = layer.Add({ project, apply });
5913
7006
  // If terrain changes then we'll re-call createLabel as the position may have changed.
5914
7007
  let terrTimeout = null;
5915
7008
  this.terrChangedRemoval = this.viewer.scene.terrainProviderChanged.addEventListener(() => {
@@ -5929,6 +7022,9 @@ var EntityLabel;
5929
7022
  * Removes label from the viewer and unsubscribes from events.
5930
7023
  */
5931
7024
  removeLabel() {
7025
+ var _a;
7026
+ (_a = this.anchor) === null || _a === void 0 ? void 0 : _a.remove();
7027
+ this.anchor = null;
5932
7028
  if (this.label &&
5933
7029
  this.label.parentElement) {
5934
7030
  this.label.parentElement.removeChild(this.label);
@@ -5954,16 +7050,44 @@ var EntityLabel;
5954
7050
  * If one exists then the action is ignored.
5955
7051
  */
5956
7052
  function Attach(params) {
5957
- const { rego, viewer, pos3d, visualRegister, api } = params;
5958
- if (!rego || !viewer || rego[EntityLabel.ATTACH_KEY]) {
7053
+ var _a, _b, _c;
7054
+ const { rego, viewer, pos3d, visualRegister, api, options, groupId } = params;
7055
+ if (!rego || !viewer) {
5959
7056
  return;
5960
7057
  }
7058
+ if (params.persistent == null && visualRegister) {
7059
+ const group = visualRegister.GetLabelGroupFor(rego.entityId, rego.menuItemId);
7060
+ if (group || visualRegister.GetIsLabelled({ entityId: rego.entityId, menuItemId: rego.menuItemId })) {
7061
+ return Attach({
7062
+ ...params,
7063
+ pos3d: undefined,
7064
+ options: (_a = group === null || group === void 0 ? void 0 : group.options) !== null && _a !== void 0 ? _a : options,
7065
+ groupId: (_b = group === null || group === void 0 ? void 0 : group.groupId) !== null && _b !== void 0 ? _b : groupId,
7066
+ persistent: true
7067
+ });
7068
+ }
7069
+ }
7070
+ if (rego[EntityLabel.ATTACH_KEY]) {
7071
+ // Legacy hover callers omit ownership/options and must not reconfigure a register-owned label.
7072
+ if (rego[EntityLabel.ATTACH_KEY].Persistent && params.persistent !== true && !groupId) {
7073
+ return rego[EntityLabel.ATTACH_KEY];
7074
+ }
7075
+ rego[EntityLabel.ATTACH_KEY].Persistent = (_c = params.persistent) !== null && _c !== void 0 ? _c : Boolean(groupId);
7076
+ rego[EntityLabel.ATTACH_KEY].Configure(options, groupId);
7077
+ return rego[EntityLabel.ATTACH_KEY];
7078
+ }
5961
7079
  rego[EntityLabel.ATTACH_KEY] = new Label({
5962
7080
  rego,
5963
7081
  viewer,
5964
7082
  pos3d,
5965
7083
  visualRegister,
5966
- api
7084
+ api,
7085
+ options,
7086
+ groupId,
7087
+ persistent: params.persistent,
7088
+ transient: params.transient,
7089
+ hoverRingState: params.hoverRingState,
7090
+ visible: params.visible
5967
7091
  });
5968
7092
  return rego[EntityLabel.ATTACH_KEY];
5969
7093
  }
@@ -5978,12 +7102,19 @@ var EntityLabel;
5978
7102
  if (!rego && label) {
5979
7103
  rego = label.Rego;
5980
7104
  }
7105
+ if (label && label !== (rego === null || rego === void 0 ? void 0 : rego[EntityLabel.ATTACH_KEY])) {
7106
+ if (!label.Persistent || params.force)
7107
+ label.Dispose();
7108
+ return;
7109
+ }
5981
7110
  if (!rego || !rego[EntityLabel.ATTACH_KEY]) {
5982
7111
  if (label) {
5983
7112
  (_a = label.Dispose) === null || _a === void 0 ? void 0 : _a.call(label);
5984
7113
  }
5985
7114
  return;
5986
7115
  }
7116
+ if (rego[EntityLabel.ATTACH_KEY].Persistent && !params.force)
7117
+ return;
5987
7118
  (_c = (_b = rego[EntityLabel.ATTACH_KEY]).Dispose) === null || _c === void 0 ? void 0 : _c.call(_b);
5988
7119
  rego[EntityLabel.ATTACH_KEY] = null;
5989
7120
  }
@@ -6003,6 +7134,538 @@ var EntityLabel;
6003
7134
  EntityLabel.GetLabel = GetLabel;
6004
7135
  })(EntityLabel || (EntityLabel = {}));
6005
7136
 
7137
+ var EntityLabelGroup;
7138
+ (function (EntityLabelGroup) {
7139
+ /*
7140
+ * Keeps the query response useful even when the Entity has no loaded visual yet.
7141
+ */
7142
+ function FromEntity(entity) {
7143
+ var _a, _b, _c, _d;
7144
+ const location = (_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.Location;
7145
+ const pivot = (_b = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _b === void 0 ? void 0 : _b.GeometryWorldPivot;
7146
+ let cartographic;
7147
+ if ((pivot === null || pivot === void 0 ? void 0 : pivot.length) >= 3 && pivot.every(Number.isFinite)) {
7148
+ cartographic = Cartographic.fromCartesian(Cartesian3.fromArray(pivot));
7149
+ }
7150
+ return {
7151
+ entityId: (_c = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _c === void 0 ? void 0 : _c.ID,
7152
+ text: (_d = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _d === void 0 ? void 0 : _d.Name,
7153
+ longitude: cartographic ? Math$1.toDegrees(cartographic.longitude) : location === null || location === void 0 ? void 0 : location.longitude,
7154
+ latitude: cartographic ? Math$1.toDegrees(cartographic.latitude) : location === null || location === void 0 ? void 0 : location.latitude,
7155
+ altitude: cartographic ? cartographic.height : location === null || location === void 0 ? void 0 : location.altitude,
7156
+ entity
7157
+ };
7158
+ }
7159
+ EntityLabelGroup.FromEntity = FromEntity;
7160
+ /**
7161
+ * Returns if we need to resolve Entity data to create the label data.
7162
+ */
7163
+ function NeedsEntityData(options) {
7164
+ var _a;
7165
+ if (!options) {
7166
+ return false;
7167
+ }
7168
+ if ((_a = options.attributes) === null || _a === void 0 ? void 0 : _a.length) {
7169
+ return true;
7170
+ }
7171
+ return [
7172
+ options.text, options.icon, options.iconUrl, options.iconClass,
7173
+ options.iconColor, options.iconStatus, options.iconPriority, options.iconScale
7174
+ ].some(Array.isArray);
7175
+ }
7176
+ EntityLabelGroup.NeedsEntityData = NeedsEntityData;
7177
+ // How many entities to read at once, so a long hand-picked list does not arrive as a burst.
7178
+ const HYDRATE_AT_ONCE = 8;
7179
+ /*
7180
+ * Reads the entities behind items that name one without carrying it.
7181
+ */
7182
+ async function Hydrate(items, api, signal) {
7183
+ if (!api) {
7184
+ return items;
7185
+ }
7186
+ const hydrated = items.slice();
7187
+ for (let start = 0; start < hydrated.length; start += HYDRATE_AT_ONCE) {
7188
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
7189
+ return hydrated;
7190
+ }
7191
+ await Promise.all(hydrated.slice(start, start + HYDRATE_AT_ONCE).map(async (item, at) => {
7192
+ if (!(item === null || item === void 0 ? void 0 : item.entityId) || item.entity) {
7193
+ return;
7194
+ }
7195
+ try {
7196
+ const { entity } = await Entity$1.Get({ api, entityId: item.entityId, migrated: true });
7197
+ if (entity) {
7198
+ hydrated[start + at] = { ...item, entity };
7199
+ }
7200
+ }
7201
+ catch {
7202
+ // Rip.
7203
+ }
7204
+ }));
7205
+ }
7206
+ return hydrated;
7207
+ }
7208
+ EntityLabelGroup.Hydrate = Hydrate;
7209
+ /*
7210
+ * Uses the existing DataLab grammar, discarding actions and projections that cannot return Entities.
7211
+ */
7212
+ async function Loader(source, api, signal, hydrate) {
7213
+ var _a;
7214
+ var _b;
7215
+ if (source.type === "provider") {
7216
+ return (offset, limit, signal) => source.loadPage({ offset, limit, signal });
7217
+ }
7218
+ if (source.type === "entityIds" || source.type === "json") {
7219
+ const items = (source.type === "entityIds" ? source.entityIds.map(entityId => ({ entityId })) :
7220
+ typeof source.items === "string" ? JSON.parse(source.items) : source.items);
7221
+ if (!Array.isArray(items)) {
7222
+ throw new Error("Label JSON must be an array of label items");
7223
+ }
7224
+ return async (offset, limit, signal) => {
7225
+ const page = items.slice(offset, offset + limit);
7226
+ return {
7227
+ items: hydrate ? await Hydrate(page, api, signal) : page,
7228
+ hasMore: offset + limit < items.length
7229
+ };
7230
+ };
7231
+ }
7232
+ if (!api) {
7233
+ throw new Error("DataLab label sources require API getters on the visual register");
7234
+ }
7235
+ let query;
7236
+ if (source.type === "queryId") {
7237
+ const saved = (await DataLab.Get({ api, queryId: source.queryId, req: { noCache: true, abortSignal: signal } })).query;
7238
+ const selection = (_a = saved === null || saved === void 0 ? void 0 : saved.PrimarySelectionCriteria) !== null && _a !== void 0 ? _a : saved === null || saved === void 0 ? void 0 : saved.PrimarySelection;
7239
+ if (!selection) {
7240
+ throw new Error("Saved label query has no selection criteria");
7241
+ }
7242
+ query = { PrimarySelection: selection };
7243
+ }
7244
+ else {
7245
+ query = typeof source.query === "string" ? JSON.parse(source.query) : source.query;
7246
+ }
7247
+ if (!(query === null || query === void 0 ? void 0 : query.PrimarySelection)) {
7248
+ throw new Error("Label query requires PrimarySelection");
7249
+ }
7250
+ query = JSON.parse(JSON.stringify(query));
7251
+ delete query.Action;
7252
+ query.PrimarySelection.Select = { Type: DataLab.ESelectType.Entity };
7253
+ (_b = query.PrimarySelection).OrderBy || (_b.OrderBy = "Bruce/ID");
7254
+ query.Expand = query.Expand ? `${query.Expand},location` : "location";
7255
+ return async (offset, limit, signal) => {
7256
+ const response = await DataLab.Run({ api, query, skip: offset, load: limit, migrated: true, req: { noCache: true, abortSignal: signal } });
7257
+ const entities = response.entities || response.items || [];
7258
+ return {
7259
+ items: entities.map(FromEntity),
7260
+ hasMore: response.totalCount != null ? offset + entities.length < response.totalCount : entities.length === limit
7261
+ };
7262
+ };
7263
+ }
7264
+ EntityLabelGroup.Loader = Loader;
7265
+ /*
7266
+ * Snapshot settings without dropping Calculator's optional runtime function values.
7267
+ */
7268
+ function copyOptions(value) {
7269
+ if (Array.isArray(value)) {
7270
+ return value.map(copyOptions);
7271
+ }
7272
+ if (value && typeof value === "object") {
7273
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, copyOptions(item)]));
7274
+ }
7275
+ return value;
7276
+ }
7277
+ /*
7278
+ * Evaluate source attributes during register refresh, never in the camera-frame layout loop.
7279
+ */
7280
+ function resolveIcon(value, entity) {
7281
+ if (!Array.isArray(value))
7282
+ return value;
7283
+ try {
7284
+ return Calculator.GetString(value, entity || {});
7285
+ }
7286
+ catch {
7287
+ // An invalid expression on one item must not prevent the remaining labels from rendering.
7288
+ return null;
7289
+ }
7290
+ }
7291
+ /*
7292
+ * Owns membership independently of manual labelled states, with atomic replacement and stale-load cancellation.
7293
+ */
7294
+ class Manager {
7295
+ constructor(register, changed = () => { }) {
7296
+ this.register = register;
7297
+ this.changed = changed;
7298
+ this.active = new Map();
7299
+ this.pending = new Map();
7300
+ this.standalone = new Map();
7301
+ this.standaloneItems = new Map();
7302
+ this.members = new Map();
7303
+ this.definitions = new Map();
7304
+ this.visibility = new Map();
7305
+ this.errors = new Map();
7306
+ }
7307
+ notify() {
7308
+ try {
7309
+ this.changed();
7310
+ }
7311
+ catch (error) {
7312
+ console.error("Label group observer failed", error);
7313
+ }
7314
+ }
7315
+ Refresh(id) {
7316
+ const definition = this.definitions.get(id);
7317
+ if (!definition)
7318
+ return Promise.reject(new Error(`Unknown label group: ${id}`));
7319
+ return this.Set({ ...definition, visible: this.visibility.get(id) });
7320
+ }
7321
+ SetVisible(id, visible) {
7322
+ var _a;
7323
+ if (!this.definitions.has(id))
7324
+ return false;
7325
+ if (this.visibility.get(id) === visible)
7326
+ return true;
7327
+ this.visibility.set(id, visible);
7328
+ this.refresh(new Set(((_a = this.active.get(id)) === null || _a === void 0 ? void 0 : _a.items.keys()) || []));
7329
+ this.notify();
7330
+ return true;
7331
+ }
7332
+ async Set(definition) {
7333
+ var _a, _b, _c, _d, _e, _f, _g, _h;
7334
+ if (!(definition === null || definition === void 0 ? void 0 : definition.id) || !definition.source) {
7335
+ throw new Error("Label group requires id and source");
7336
+ }
7337
+ definition = { ...definition, options: copyOptions(definition.options) };
7338
+ const pageSize = (_a = definition.pageSize) !== null && _a !== void 0 ? _a : 500;
7339
+ const maximum = (_b = definition.maxItems) !== null && _b !== void 0 ? _b : 50000;
7340
+ if (!Number.isInteger(pageSize) || pageSize < 1 || !Number.isInteger(maximum) || maximum < 1) {
7341
+ throw new Error("Label pageSize and maxItems must be positive integers");
7342
+ }
7343
+ ValidateOptions(definition.options);
7344
+ (_c = this.pending.get(definition.id)) === null || _c === void 0 ? void 0 : _c.abort();
7345
+ const controller = new AbortController();
7346
+ this.pending.set(definition.id, controller);
7347
+ this.definitions.set(definition.id, definition);
7348
+ const wasVisible = this.visibility.get(definition.id);
7349
+ this.visibility.set(definition.id, (_e = (_d = definition.visible) !== null && _d !== void 0 ? _d : this.visibility.get(definition.id)) !== null && _e !== void 0 ? _e : true);
7350
+ if (wasVisible != null && wasVisible !== this.visibility.get(definition.id)) {
7351
+ this.refresh(new Set(((_f = this.active.get(definition.id)) === null || _f === void 0 ? void 0 : _f.items.keys()) || []));
7352
+ }
7353
+ this.errors.delete(definition.id);
7354
+ this.notify();
7355
+ const cancelled = () => ({ id: definition.id, entityIds: [], truncated: false, cancelled: true });
7356
+ const items = new Map();
7357
+ let truncated = false;
7358
+ try {
7359
+ const load = await Loader(definition.source, (_g = this.register.ApiGetters) === null || _g === void 0 ? void 0 : _g.GetBruceApi(), controller.signal, NeedsEntityData(definition.options));
7360
+ let offset = 0;
7361
+ for (let pageNumber = 0;; pageNumber++) {
7362
+ if (controller.signal.aborted || this.register.Disposed) {
7363
+ return cancelled();
7364
+ }
7365
+ if (pageNumber >= 10000) {
7366
+ throw new Error("Label source exceeded 10000 pages");
7367
+ }
7368
+ const page = await load(offset, Math.min(pageSize, maximum - items.size), controller.signal);
7369
+ if (controller.signal.aborted || this.register.Disposed)
7370
+ return cancelled();
7371
+ if (!Array.isArray(page === null || page === void 0 ? void 0 : page.items)) {
7372
+ throw new Error("Label source returned an invalid page");
7373
+ }
7374
+ for (const item of page.items) {
7375
+ if (!item || typeof item.entityId !== "string" || !item.entityId) {
7376
+ throw new Error("Label item requires entityId");
7377
+ }
7378
+ if (!items.has(item.entityId) && items.size >= maximum) {
7379
+ truncated = true;
7380
+ break;
7381
+ }
7382
+ items.set(item.entityId, item);
7383
+ }
7384
+ if (!page.hasMore) {
7385
+ break;
7386
+ }
7387
+ if (items.size >= maximum) {
7388
+ truncated = true;
7389
+ break;
7390
+ }
7391
+ const next = (_h = page.nextOffset) !== null && _h !== void 0 ? _h : offset + page.items.length;
7392
+ if (!Number.isSafeInteger(next) || next <= offset) {
7393
+ throw new Error("Label source pagination did not advance");
7394
+ }
7395
+ offset = next;
7396
+ }
7397
+ const previous = this.active.get(definition.id);
7398
+ const affected = new Set([...((previous === null || previous === void 0 ? void 0 : previous.items.keys()) || []), ...items.keys()]);
7399
+ this.unindex(definition.id);
7400
+ // Reinsertion makes the most recently committed matching group the presentation owner.
7401
+ this.active.delete(definition.id);
7402
+ this.active.set(definition.id, { definition, items, truncated });
7403
+ for (const id of items.keys()) {
7404
+ if (!this.members.has(id)) {
7405
+ this.members.set(id, new Set());
7406
+ }
7407
+ this.members.get(id).add(definition.id);
7408
+ }
7409
+ this.refresh(affected);
7410
+ return { id: definition.id, entityIds: Array.from(items.keys()), truncated, cancelled: false };
7411
+ }
7412
+ catch (error) {
7413
+ if (controller.signal.aborted || this.register.Disposed) {
7414
+ return cancelled();
7415
+ }
7416
+ this.errors.set(definition.id, error instanceof Error ? error.message : String(error));
7417
+ throw error;
7418
+ }
7419
+ finally {
7420
+ if (this.pending.get(definition.id) === controller) {
7421
+ this.pending.delete(definition.id);
7422
+ this.notify();
7423
+ }
7424
+ }
7425
+ }
7426
+ Get() {
7427
+ return Array.from(this.definitions.keys(), id => {
7428
+ var _a;
7429
+ const group = this.active.get(id);
7430
+ return {
7431
+ id,
7432
+ entityIds: Array.from((group === null || group === void 0 ? void 0 : group.items.keys()) || []),
7433
+ truncated: (_a = group === null || group === void 0 ? void 0 : group.truncated) !== null && _a !== void 0 ? _a : false,
7434
+ cancelled: false,
7435
+ visible: this.visibility.get(id) !== false,
7436
+ loading: this.pending.has(id),
7437
+ error: this.errors.get(id)
7438
+ };
7439
+ });
7440
+ }
7441
+ Has(entityId, menuItemId) {
7442
+ for (const id of this.members.get(entityId) || []) {
7443
+ const group = this.active.get(id);
7444
+ if (group && this.visibility.get(id) !== false && (!group.definition.menuItemId || group.definition.menuItemId === menuItemId)) {
7445
+ return true;
7446
+ }
7447
+ }
7448
+ return false;
7449
+ }
7450
+ Resolve(entityId, menuItemId) {
7451
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
7452
+ const ids = Array.from(this.members.get(entityId) || []).reverse();
7453
+ for (const id of ids) {
7454
+ const group = this.active.get(id);
7455
+ if (this.visibility.get(id) === false) {
7456
+ continue;
7457
+ }
7458
+ if (group.definition.menuItemId && group.definition.menuItemId !== menuItemId) {
7459
+ continue;
7460
+ }
7461
+ const item = group.items.get(entityId);
7462
+ const entity = (_a = item.entity) !== null && _a !== void 0 ? _a : { Bruce: { ID: entityId, Name: item.text } };
7463
+ let iconPriority = (_b = group.definition.options) === null || _b === void 0 ? void 0 : _b.iconPriority;
7464
+ if (Array.isArray(iconPriority)) {
7465
+ try {
7466
+ iconPriority = Calculator.GetNumber(iconPriority, entity);
7467
+ }
7468
+ catch {
7469
+ iconPriority = null;
7470
+ }
7471
+ }
7472
+ let iconScale = (_c = group.definition.options) === null || _c === void 0 ? void 0 : _c.iconScale;
7473
+ if (Array.isArray(iconScale)) {
7474
+ try {
7475
+ iconScale = Calculator.GetNumber(iconScale, entity);
7476
+ }
7477
+ catch {
7478
+ iconScale = null;
7479
+ }
7480
+ if (!Number.isFinite(iconScale)) {
7481
+ iconScale = 1;
7482
+ }
7483
+ }
7484
+ if (iconScale != null) {
7485
+ iconScale = Math.max(0, iconScale);
7486
+ }
7487
+ const textSetting = (_d = group.definition.options) === null || _d === void 0 ? void 0 : _d.text;
7488
+ let resolvedText = resolveIcon(textSetting, entity);
7489
+ const expression = Array.isArray(textSetting);
7490
+ if (expression && !(resolvedText === null || resolvedText === void 0 ? void 0 : resolvedText.trim())) {
7491
+ resolvedText = undefined;
7492
+ }
7493
+ const fallbackText = [item.text, (_e = entity.Bruce) === null || _e === void 0 ? void 0 : _e.Name, (_f = this.register.GetRego({ entityId, menuItemId })) === null || _f === void 0 ? void 0 : _f.name, entityId]
7494
+ .find(text => typeof text === "string" && text.trim().length > 0);
7495
+ const options = {
7496
+ ...group.definition.options,
7497
+ text: resolvedText !== null && resolvedText !== void 0 ? resolvedText : fallbackText,
7498
+ iconScale: iconScale,
7499
+ icon: resolveIcon((_g = group.definition.options) === null || _g === void 0 ? void 0 : _g.icon, entity),
7500
+ iconUrl: resolveIcon((_h = group.definition.options) === null || _h === void 0 ? void 0 : _h.iconUrl, entity),
7501
+ iconClass: resolveIcon((_j = group.definition.options) === null || _j === void 0 ? void 0 : _j.iconClass, entity),
7502
+ iconColor: resolveIcon((_k = group.definition.options) === null || _k === void 0 ? void 0 : _k.iconColor, entity),
7503
+ iconBackground: (_l = group.definition.options) === null || _l === void 0 ? void 0 : _l.iconBackground,
7504
+ iconPriority: Number.isFinite(iconPriority) ? iconPriority : item.iconPriority,
7505
+ iconStatus: (_o = resolveIcon((_m = group.definition.options) === null || _m === void 0 ? void 0 : _m.iconStatus, entity)) !== null && _o !== void 0 ? _o : item.iconStatus
7506
+ };
7507
+ if (options.attributes && item.entity && !(expression && resolvedText != null)) {
7508
+ options.text = [
7509
+ (_r = (_p = options.text) !== null && _p !== void 0 ? _p : (_q = item.entity.Bruce) === null || _q === void 0 ? void 0 : _q.Name) !== null && _r !== void 0 ? _r : entityId,
7510
+ ...options.attributes.map(path => Entity$1.GetValue({ entity: item.entity, path })).filter(value => value != null).map(String)
7511
+ ].join("\n");
7512
+ }
7513
+ return { groupId: id, options, item };
7514
+ }
7515
+ return null;
7516
+ }
7517
+ RefreshEntity(entityId) {
7518
+ var _a, _b, _c, _d, _e, _f, _g;
7519
+ const match = this.Resolve(entityId);
7520
+ const registered = this.register.GetRego({ entityId });
7521
+ if (!registered && match && this.standalone.has(entityId) && this.standaloneItems.get(entityId) === match.item) {
7522
+ this.standalone.get(entityId).Configure({ ...match.options, text: (_b = (_a = match.options.text) !== null && _a !== void 0 ? _a : match.item.text) !== null && _b !== void 0 ? _b : entityId }, match.groupId);
7523
+ return;
7524
+ }
7525
+ (_c = this.standalone.get(entityId)) === null || _c === void 0 ? void 0 : _c.Dispose();
7526
+ this.standalone.delete(entityId);
7527
+ this.standaloneItems.delete(entityId);
7528
+ if (this.register.Disposed || registered) {
7529
+ return;
7530
+ }
7531
+ if (!match) {
7532
+ return;
7533
+ }
7534
+ const item = match.item;
7535
+ if (![item.longitude, item.latitude, (_d = item.altitude) !== null && _d !== void 0 ? _d : 0].every(Number.isFinite) || Math.abs(item.latitude) > 90) {
7536
+ return;
7537
+ }
7538
+ const visual = new Entity({
7539
+ position: Cartesian3.fromDegrees(item.longitude, item.latitude, item.altitude || 0),
7540
+ point: { heightReference: HeightReference.NONE, pixelSize: 0 }
7541
+ });
7542
+ this.standalone.set(entityId, new EntityLabel.Label({
7543
+ viewer: this.register.Viewer,
7544
+ visualRegister: this.register,
7545
+ api: (_e = this.register.ApiGetters) === null || _e === void 0 ? void 0 : _e.GetBruceApi(),
7546
+ rego: { entityId, visual, name: item.text || entityId, priority: 0 },
7547
+ options: { ...match.options, text: (_g = (_f = match.options.text) !== null && _f !== void 0 ? _f : item.text) !== null && _g !== void 0 ? _g : entityId },
7548
+ groupId: match.groupId,
7549
+ visible: () => {
7550
+ const state = this.register.GetState({ entityId });
7551
+ return !state.hidden && (!this.register.GetIsIsolatedAny() || state.isolated || this.register.GetIsolationOpacity() != null);
7552
+ }
7553
+ }));
7554
+ this.standaloneItems.set(entityId, item);
7555
+ }
7556
+ Remove(id) {
7557
+ var _a;
7558
+ const known = this.definitions.delete(id);
7559
+ this.visibility.delete(id);
7560
+ this.errors.delete(id);
7561
+ (_a = this.pending.get(id)) === null || _a === void 0 ? void 0 : _a.abort();
7562
+ this.pending.delete(id);
7563
+ const group = this.active.get(id);
7564
+ if (!group) {
7565
+ // Nothing to unindex. Say so only if there was a definition to forget.
7566
+ if (known) {
7567
+ this.notify();
7568
+ }
7569
+ return;
7570
+ }
7571
+ this.unindex(id);
7572
+ this.active.delete(id);
7573
+ this.refresh(new Set(group.items.keys()));
7574
+ this.notify();
7575
+ }
7576
+ Clear() {
7577
+ for (const controller of this.pending.values()) {
7578
+ controller.abort();
7579
+ }
7580
+ this.pending.clear();
7581
+ const affected = new Set(this.members.keys());
7582
+ this.active.clear();
7583
+ this.members.clear();
7584
+ this.definitions.clear();
7585
+ this.visibility.clear();
7586
+ this.errors.clear();
7587
+ this.refresh(affected);
7588
+ this.notify();
7589
+ }
7590
+ Dispose() {
7591
+ for (const controller of this.pending.values()) {
7592
+ controller.abort();
7593
+ }
7594
+ for (const label of this.standalone.values()) {
7595
+ label.Dispose();
7596
+ }
7597
+ this.pending.clear();
7598
+ this.standalone.clear();
7599
+ this.standaloneItems.clear();
7600
+ this.active.clear();
7601
+ this.members.clear();
7602
+ this.definitions.clear();
7603
+ this.visibility.clear();
7604
+ this.errors.clear();
7605
+ }
7606
+ unindex(groupId) {
7607
+ var _a;
7608
+ for (const id of ((_a = this.active.get(groupId)) === null || _a === void 0 ? void 0 : _a.items.keys()) || []) {
7609
+ const groups = this.members.get(id);
7610
+ groups === null || groups === void 0 ? void 0 : groups.delete(groupId);
7611
+ if (!(groups === null || groups === void 0 ? void 0 : groups.size)) {
7612
+ this.members.delete(id);
7613
+ }
7614
+ }
7615
+ }
7616
+ refresh(ids) {
7617
+ var _a;
7618
+ // Creation drains through the register's bounded update queue, including source-only labels.
7619
+ for (const id of ids) {
7620
+ (_a = this.standalone.get(id)) === null || _a === void 0 ? void 0 : _a.Dispose();
7621
+ this.standalone.delete(id);
7622
+ this.standaloneItems.delete(id);
7623
+ }
7624
+ this.register.ForceUpdate({ entityIds: Array.from(ids) });
7625
+ this.register.Viewer.scene.requestRender();
7626
+ }
7627
+ }
7628
+ EntityLabelGroup.Manager = Manager;
7629
+ /*
7630
+ * Rejects invalid geometry settings before they can create unbounded spatial-index work.
7631
+ */
7632
+ function ValidateOptions(options) {
7633
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
7634
+ if (!options) {
7635
+ return;
7636
+ }
7637
+ if (options.iconBackground != null && typeof options.iconBackground !== "boolean") {
7638
+ throw new Error("Label iconBackground must be a boolean");
7639
+ }
7640
+ for (const value of [options.icon, options.iconUrl, options.iconClass, options.iconColor, options.text]) {
7641
+ if (value != null && typeof value !== "string" && !Array.isArray(value)) {
7642
+ throw new Error("Label icons must be strings or Calculator field arrays");
7643
+ }
7644
+ }
7645
+ if (options.iconScale != null && !Array.isArray(options.iconScale) && !Number.isFinite(options.iconScale)) {
7646
+ throw new Error("Label iconScale must be finite or a Calculator field array");
7647
+ }
7648
+ if (options.iconPriority != null && !Array.isArray(options.iconPriority) && !Number.isFinite(options.iconPriority)) {
7649
+ throw new Error("Label iconPriority must be finite or a Calculator field array");
7650
+ }
7651
+ for (const value of [options.iconSize, options.leaderLength, options.near, options.far, (_a = options.cluster) === null || _a === void 0 ? void 0 : _a.pixelRange, (_b = options.pointerAvoidance) === null || _b === void 0 ? void 0 : _b.gap, (_c = options.pointerAvoidance) === null || _c === void 0 ? void 0 : _c.delay, (_d = options.pointerAvoidance) === null || _d === void 0 ? void 0 : _d.transitionDuration, (_e = options.anchorRing) === null || _e === void 0 ? void 0 : _e.nearDistance, (_f = options.anchorRing) === null || _f === void 0 ? void 0 : _f.nearRadius, (_g = options.anchorRing) === null || _g === void 0 ? void 0 : _g.hoverDistance, (_h = options.anchorRing) === null || _h === void 0 ? void 0 : _h.hoverRadius, (_j = options.anchorRing) === null || _j === void 0 ? void 0 : _j.transitionDuration]) {
7652
+ if (value != null && (!Number.isFinite(value) || value < 0)) {
7653
+ throw new Error("Label dimensions and distances must be finite and nonnegative");
7654
+ }
7655
+ }
7656
+ if (((_k = options.near) !== null && _k !== void 0 ? _k : 0) > ((_l = options.far) !== null && _l !== void 0 ? _l : Infinity)) {
7657
+ throw new Error("Label near must not exceed far");
7658
+ }
7659
+ if (options.overlapOpacity != null && (!Number.isFinite(options.overlapOpacity) || options.overlapOpacity < 0 || options.overlapOpacity > 1)) {
7660
+ throw new Error("Label overlapOpacity must be between zero and one");
7661
+ }
7662
+ if (((_m = options.cluster) === null || _m === void 0 ? void 0 : _m.minSize) != null && (!Number.isInteger(options.cluster.minSize) || options.cluster.minSize < 2)) {
7663
+ throw new Error("Label cluster minSize must be an integer of at least two");
7664
+ }
7665
+ }
7666
+ EntityLabelGroup.ValidateOptions = ValidateOptions;
7667
+ })(EntityLabelGroup || (EntityLabelGroup = {}));
7668
+
6006
7669
  /**
6007
7670
  * Utility for rendering parabolas between two points on the map.
6008
7671
  */
@@ -7497,7 +9160,7 @@ function updateEntityShow(viewer, rego, show) {
7497
9160
  }
7498
9161
  }
7499
9162
  function updateEntity(viewer, entityId, register, refresh, cache = {}) {
7500
- var _a;
9163
+ var _a, _b, _c;
7501
9164
  if (!entityId) {
7502
9165
  // What are we doing?
7503
9166
  return;
@@ -7610,20 +9273,25 @@ function updateEntity(viewer, entityId, register, refresh, cache = {}) {
7610
9273
  }
7611
9274
  }
7612
9275
  }
7613
- if (state.labelled && visible) {
9276
+ const labelGroup = register.GetLabelGroupFor(rego.entityId, rego.menuItemId);
9277
+ if ((state.labelled || labelGroup) && visible) {
7614
9278
  EntityLabel.Attach({
7615
9279
  rego,
7616
9280
  viewer,
7617
- api: register.ApiGetters.GetBruceApi(),
7618
- visualRegister: register
9281
+ api: (_b = register.ApiGetters) === null || _b === void 0 ? void 0 : _b.GetBruceApi(),
9282
+ visualRegister: register,
9283
+ options: labelGroup === null || labelGroup === void 0 ? void 0 : labelGroup.options,
9284
+ groupId: labelGroup === null || labelGroup === void 0 ? void 0 : labelGroup.groupId,
9285
+ persistent: true
7619
9286
  });
7620
9287
  }
7621
- else {
9288
+ else if (!visible || ((_c = EntityLabel.GetLabel({ rego })) === null || _c === void 0 ? void 0 : _c.Persistent)) {
7622
9289
  EntityLabel.Detatch({
7623
- rego
9290
+ rego, force: true
7624
9291
  });
7625
9292
  }
7626
9293
  }
9294
+ register.RefreshLabelGroupEntity(entityId);
7627
9295
  }
7628
9296
  function markEntity(register, rego, visual, ignoreParent) {
7629
9297
  const vis = visual;
@@ -7738,6 +9406,7 @@ var VisualsRegister;
7738
9406
  }
7739
9407
  return this.onUpdate;
7740
9408
  }
9409
+ get OnLabelGroupsChanged() { return this.labelGroupsChanged; }
7741
9410
  constructor(params) {
7742
9411
  this.id = ObjectUtils.UId();
7743
9412
  this.disposed = false;
@@ -7776,10 +9445,35 @@ var VisualsRegister;
7776
9445
  this.menuItemSources = new Map();
7777
9446
  // Set when the data source order no longer matches the zIndexes seen, so it is re-applied once.
7778
9447
  this.sourceOrderDirty = false;
9448
+ this.labelGroupsChanged = new BruceEvent();
9449
+ this.hoverRingState = {};
9450
+ this.retiringHoverLabels = new Map();
7779
9451
  /*
7780
9452
  * Tilesets in the scene, keyed by the tileset itself.
7781
9453
  */
7782
9454
  this.tilesets = new Map();
9455
+ this.ClearLabelHover = () => {
9456
+ if (!this.labelHover && !this.hoverLabel) {
9457
+ return;
9458
+ }
9459
+ this.ReleaseHoverLabel();
9460
+ this.labelHover = undefined;
9461
+ clearTimeout(this.hoverRingReset);
9462
+ this.hoverRingReset = setTimeout(() => { this.hoverRingState = {}; }, 400);
9463
+ const doc = this.viewer.container.ownerDocument;
9464
+ doc.removeEventListener("pointermove", this.CheckLabelHoverBounds, true);
9465
+ this.viewer.container.removeEventListener("pointerleave", this.CheckLabelHoverBounds);
9466
+ doc.defaultView.removeEventListener("blur", this.ClearLabelHover);
9467
+ if (!this.viewer.isDestroyed()) {
9468
+ this.viewer.scene.requestRender();
9469
+ }
9470
+ };
9471
+ this.CheckLabelHoverBounds = (event) => {
9472
+ const bounds = this.viewer.canvas.getBoundingClientRect();
9473
+ if (event.clientX < bounds.left || event.clientX >= bounds.right || event.clientY < bounds.top || event.clientY >= bounds.bottom) {
9474
+ this.ClearLabelHover();
9475
+ }
9476
+ };
7783
9477
  this.viewer = params.viewer;
7784
9478
  this.apiGetters = params.apiGetters;
7785
9479
  this.cameraCullerDispose = VisualRegisterCuller.Monitor({
@@ -7975,6 +9669,7 @@ var VisualsRegister;
7975
9669
  const BATCH_SIZE = 300;
7976
9670
  if (this.updateQueue.length) {
7977
9671
  this.updateQueueInterval = setInterval(() => {
9672
+ var _a;
7978
9673
  if (this.Disposed || (!this.viewer || !this.viewer.scene || this.viewer.isDestroyed())) {
7979
9674
  clearInterval(this.updateQueueInterval);
7980
9675
  this.updateQueueInterval = null;
@@ -7987,6 +9682,8 @@ var VisualsRegister;
7987
9682
  this.updateQueueIds.delete(entityId);
7988
9683
  // Check if still registered.
7989
9684
  if (entityId && (!this.rego[entityId] || !this.rego[entityId].length)) {
9685
+ (_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.RefreshEntity(entityId);
9686
+ delete this.updateQueueSettings[entityId];
7990
9687
  continue;
7991
9688
  }
7992
9689
  updateEntity(this.viewer, entityId, this, this.updateQueueSettings[entityId] == null ? false : this.updateQueueSettings[entityId], cache);
@@ -8644,8 +10341,23 @@ var VisualsRegister;
8644
10341
  markEntity(this, rego, rego.visual, false);
8645
10342
  }
8646
10343
  Dispose() {
8647
- var _a, _b, _c, _d;
8648
- (_a = this.cameraCullerDispose) === null || _a === void 0 ? void 0 : _a.call(this, {
10344
+ var _a, _b, _c, _d, _e;
10345
+ this.ClearLabelHover();
10346
+ clearTimeout(this.hoverRingReset);
10347
+ this.hoverRingState = {};
10348
+ for (const { label, timer } of this.retiringHoverLabels.values()) {
10349
+ clearTimeout(timer);
10350
+ EntityLabel.Detatch({ label });
10351
+ }
10352
+ this.retiringHoverLabels.clear();
10353
+ (_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.Dispose();
10354
+ this.labelGroupsChanged.Clear();
10355
+ for (const regos of Object.values(this.rego)) {
10356
+ for (const rego of regos)
10357
+ EntityLabel.Detatch({ rego, force: true });
10358
+ }
10359
+ EntityLabel.SetContainer(this.viewer);
10360
+ (_b = this.cameraCullerDispose) === null || _b === void 0 ? void 0 : _b.call(this, {
8649
10361
  uncull: false
8650
10362
  });
8651
10363
  this.cameraCullerDispose = null;
@@ -8661,7 +10373,7 @@ var VisualsRegister;
8661
10373
  this.disposed = true;
8662
10374
  for (const record of Array.from(this.menuItemSources.values())) {
8663
10375
  try {
8664
- (_c = (_b = this.viewer) === null || _b === void 0 ? void 0 : _b.dataSources) === null || _c === void 0 ? void 0 : _c.remove(record.source, true);
10376
+ (_d = (_c = this.viewer) === null || _c === void 0 ? void 0 : _c.dataSources) === null || _d === void 0 ? void 0 : _d.remove(record.source, true);
8665
10377
  }
8666
10378
  catch (e) {
8667
10379
  console.error(e);
@@ -8670,7 +10382,7 @@ var VisualsRegister;
8670
10382
  this.menuItemSources.clear();
8671
10383
  // Hand every tileset's show flag back to its owner, holding whatever the owner last asked for.
8672
10384
  for (const rego of Array.from(this.tilesets.values())) {
8673
- (_d = rego.gate) === null || _d === void 0 ? void 0 : _d.Release();
10385
+ (_e = rego.gate) === null || _e === void 0 ? void 0 : _e.Release();
8674
10386
  }
8675
10387
  this.tilesets.clear();
8676
10388
  }
@@ -9622,7 +11334,7 @@ var VisualsRegister;
9622
11334
  * @returns
9623
11335
  */
9624
11336
  RemoveRegos(params) {
9625
- var _a, _b, _c;
11337
+ var _a, _b, _c, _d, _e;
9626
11338
  let { entityId, menuItemId, doUpdate, relation, retainTagIds, requestRender, doRemove, source, lazy, onComplete } = params;
9627
11339
  if (doUpdate == null || doUpdate === undefined) {
9628
11340
  doUpdate = true;
@@ -9643,7 +11355,7 @@ var VisualsRegister;
9643
11355
  }
9644
11356
  }
9645
11357
  const removeOne = (entityId) => {
9646
- var _a, _b;
11358
+ var _a, _b, _c;
9647
11359
  const entityRegos = this.rego[entityId];
9648
11360
  if (!entityRegos) {
9649
11361
  return;
@@ -9658,7 +11370,7 @@ var VisualsRegister;
9658
11370
  return;
9659
11371
  }
9660
11372
  EntityLabel.Detatch({
9661
- rego
11373
+ rego, force: true
9662
11374
  });
9663
11375
  // Never tear down the graphic the caller is registering: the renderers hand back
9664
11376
  // the same object when they reuse it, so removing it here destroys what replaces it.
@@ -9683,15 +11395,14 @@ var VisualsRegister;
9683
11395
  (_a = this.onUpdate) === null || _a === void 0 ? void 0 : _a.Trigger(update);
9684
11396
  }
9685
11397
  // Only worth refreshing while the Entity is still drawn by something else, since
9686
- // losing this rego can change which of the survivors wins. With none left there is
9687
- // nothing to restyle, and queueing one per removed Entity is what turns disabling a
9688
- // large menu item into minutes of scheduled work.
11398
+ // losing this rego can change which of the survivors wins.
9689
11399
  if (doesInclude && ((_b = this.rego[entityId]) === null || _b === void 0 ? void 0 : _b.length)) {
9690
11400
  this.queueUpdate({
9691
11401
  entityId: entityId,
9692
11402
  refresh: false
9693
11403
  });
9694
11404
  }
11405
+ (_c = this.labelGroups) === null || _c === void 0 ? void 0 : _c.RefreshEntity(entityId);
9695
11406
  };
9696
11407
  const finish = () => {
9697
11408
  this.releaseMenuItemCollection(menuItemId);
@@ -9722,7 +11433,7 @@ var VisualsRegister;
9722
11433
  return;
9723
11434
  }
9724
11435
  EntityLabel.Detatch({
9725
- rego
11436
+ rego, force: true
9726
11437
  });
9727
11438
  // Never tear down the graphic the caller is registering: the renderers hand back
9728
11439
  // the same object when they reuse it, so removing it here destroys what replaces it.
@@ -9741,7 +11452,7 @@ var VisualsRegister;
9741
11452
  if (source !== EUpdateSource.TREE_CASCADE) {
9742
11453
  (_b = this.onUpdate) === null || _b === void 0 ? void 0 : _b.Trigger(update);
9743
11454
  }
9744
- this.rego[entityId] = entityRegos.filter(r => r.menuItemId !== menuItemId);
11455
+ this.rego[entityId] = this.rego[entityId].filter(r => r.menuItemId !== menuItemId);
9745
11456
  this.unindexMenuItemEntityIfAbsent(menuItemId, entityId);
9746
11457
  if (doUpdate && menuItemId) {
9747
11458
  this.queueUpdate({
@@ -9750,6 +11461,7 @@ var VisualsRegister;
9750
11461
  });
9751
11462
  }
9752
11463
  }
11464
+ (_c = this.labelGroups) === null || _c === void 0 ? void 0 : _c.RefreshEntity(entityId);
9753
11465
  }
9754
11466
  else if (relation) {
9755
11467
  const relationKey = RelationRenderEngine.GetRenderGroupId(relation);
@@ -9761,7 +11473,7 @@ var VisualsRegister;
9761
11473
  continue;
9762
11474
  }
9763
11475
  EntityLabel.Detatch({
9764
- rego
11476
+ rego, force: true
9765
11477
  });
9766
11478
  removeEntity(this.viewer, rego.visual);
9767
11479
  rego.visual = null;
@@ -9770,6 +11482,7 @@ var VisualsRegister;
9770
11482
  delete this.rego[entityId];
9771
11483
  }
9772
11484
  this.unindexMenuItemEntityIfAbsent(rego.menuItemId, entityId);
11485
+ (_d = this.labelGroups) === null || _d === void 0 ? void 0 : _d.RefreshEntity(entityId);
9773
11486
  const update = {
9774
11487
  type: EVisualUpdateType.Remove,
9775
11488
  entityId: rego.entityId,
@@ -9779,7 +11492,7 @@ var VisualsRegister;
9779
11492
  update.source = source;
9780
11493
  }
9781
11494
  if (source !== EUpdateSource.TREE_CASCADE) {
9782
- (_c = this.onUpdate) === null || _c === void 0 ? void 0 : _c.Trigger(update);
11495
+ (_e = this.onUpdate) === null || _e === void 0 ? void 0 : _e.Trigger(update);
9783
11496
  }
9784
11497
  if (doUpdate) {
9785
11498
  this.queueUpdate({
@@ -9798,7 +11511,7 @@ var VisualsRegister;
9798
11511
  * @param params
9799
11512
  */
9800
11513
  RemoveRegosByVisuals(params) {
9801
- var _a, _b;
11514
+ var _a, _b, _c;
9802
11515
  const { doRemove = true, requestRender = true, source, doUpdate, menuItemId } = params;
9803
11516
  const removedEntityIds = new Set();
9804
11517
  for (const visual of params.visuals) {
@@ -9813,9 +11526,8 @@ var VisualsRegister;
9813
11526
  continue;
9814
11527
  }
9815
11528
  if (rego.visual === visual) {
9816
- // The primary graphic for this rego - remove the rego entirely.
9817
11529
  EntityLabel.Detatch({
9818
- rego
11530
+ rego, force: true
9819
11531
  });
9820
11532
  if (doRemove != false) {
9821
11533
  removeEntity(this.viewer, rego.visual);
@@ -9844,9 +11556,10 @@ var VisualsRegister;
9844
11556
  });
9845
11557
  }
9846
11558
  removedEntityIds.add(entityId);
11559
+ (_b = this.labelGroups) === null || _b === void 0 ? void 0 : _b.RefreshEntity(entityId);
9847
11560
  }
9848
11561
  else {
9849
- const siblings = (_b = rego.visual) === null || _b === void 0 ? void 0 : _b["_siblingGraphics"];
11562
+ const siblings = (_c = rego.visual) === null || _c === void 0 ? void 0 : _c["_siblingGraphics"];
9850
11563
  if (siblings === null || siblings === void 0 ? void 0 : siblings.length) {
9851
11564
  if (doRemove != false && siblings.includes(visual)) {
9852
11565
  removeEntity(this.viewer, visual);
@@ -10113,6 +11826,204 @@ var VisualsRegister;
10113
11826
  }
10114
11827
  return map;
10115
11828
  }
11829
+ ReleaseHoverLabel(immediate = false) {
11830
+ const label = this.hoverLabel;
11831
+ this.hoverLabel = undefined;
11832
+ if (!label || label.Disposed || label.Persistent) {
11833
+ return;
11834
+ }
11835
+ if (immediate) {
11836
+ EntityLabel.Detatch({ label });
11837
+ return;
11838
+ }
11839
+ label.SetHoverVisible(false);
11840
+ const key = JSON.stringify([label.Rego.entityId, label.Rego.menuItemId || ""]);
11841
+ const previous = this.retiringHoverLabels.get(key);
11842
+ if (previous) {
11843
+ clearTimeout(previous.timer);
11844
+ EntityLabel.Detatch({ label: previous.label });
11845
+ }
11846
+ // Retain briefly beyond the fade to bridge debounced same-target hover updates.
11847
+ const timer = setTimeout(() => {
11848
+ this.retiringHoverLabels.delete(key);
11849
+ EntityLabel.Detatch({ label });
11850
+ }, 240);
11851
+ this.retiringHoverLabels.set(key, { label, timer });
11852
+ }
11853
+ /*
11854
+ * Owns temporary positioning separately from persistent label membership and world anchors.
11855
+ */
11856
+ SetLabelHover(params) {
11857
+ var _a, _b;
11858
+ if (this.disposed) {
11859
+ return;
11860
+ }
11861
+ if (!(params === null || params === void 0 ? void 0 : params.entityId)) {
11862
+ this.ClearLabelHover();
11863
+ return;
11864
+ }
11865
+ if (params.position && (!Number.isFinite(params.position.x) || !Number.isFinite(params.position.y) ||
11866
+ params.position.x < 0 || params.position.y < 0 ||
11867
+ params.position.x >= this.viewer.canvas.clientWidth || params.position.y >= this.viewer.canvas.clientHeight)) {
11868
+ this.ClearLabelHover();
11869
+ return;
11870
+ }
11871
+ const changed = ((_a = this.labelHover) === null || _a === void 0 ? void 0 : _a.entityId) !== params.entityId || ((_b = this.labelHover) === null || _b === void 0 ? void 0 : _b.menuItemId) !== params.menuItemId;
11872
+ if (changed) {
11873
+ this.ClearLabelHover();
11874
+ }
11875
+ clearTimeout(this.hoverRingReset);
11876
+ this.labelHover = {
11877
+ entityId: params.entityId,
11878
+ menuItemId: params.menuItemId,
11879
+ position: params.position ? { ...params.position } : undefined,
11880
+ createIfMissing: params.createIfMissing !== false
11881
+ };
11882
+ const doc = this.viewer.container.ownerDocument;
11883
+ doc.addEventListener("pointermove", this.CheckLabelHoverBounds, true);
11884
+ this.viewer.container.addEventListener("pointerleave", this.CheckLabelHoverBounds);
11885
+ doc.defaultView.addEventListener("blur", this.ClearLabelHover);
11886
+ this.SyncLabelHover();
11887
+ this.viewer.scene.requestRender();
11888
+ }
11889
+ GetLabelHoverPosition(entityId, menuItemId) {
11890
+ const hover = this.labelHover;
11891
+ return (hover === null || hover === void 0 ? void 0 : hover.entityId) === entityId && (hover.menuItemId == null || hover.menuItemId === menuItemId) ? hover.position : undefined;
11892
+ }
11893
+ SyncLabelHover() {
11894
+ var _a, _b, _c, _d;
11895
+ const hover = this.labelHover;
11896
+ if (!hover || this.disposed) {
11897
+ return;
11898
+ }
11899
+ if ((_a = this.hoverLabel) === null || _a === void 0 ? void 0 : _a.Disposed) {
11900
+ this.hoverLabel = undefined;
11901
+ }
11902
+ if (!hover.createIfMissing) {
11903
+ this.ReleaseHoverLabel(true);
11904
+ return;
11905
+ }
11906
+ const group = this.GetLabelGroupFor(hover.entityId, hover.menuItemId);
11907
+ let rego = this.GetRego(hover);
11908
+ const key = JSON.stringify([hover.entityId, hover.menuItemId || ""]);
11909
+ const retiring = this.retiringHoverLabels.get(key);
11910
+ if (retiring) {
11911
+ clearTimeout(retiring.timer);
11912
+ this.retiringHoverLabels.delete(key);
11913
+ if (!retiring.label.Disposed) {
11914
+ this.hoverLabel = retiring.label;
11915
+ }
11916
+ }
11917
+ const existing = rego && EntityLabel.GetLabel({ rego });
11918
+ const state = this.GetState(hover);
11919
+ if ((!rego && !group) || state.hidden || ((_b = rego === null || rego === void 0 ? void 0 : rego.visual) === null || _b === void 0 ? void 0 : _b.show) === false ||
11920
+ (this.GetIsIsolatedAny() && !state.isolated && this.GetIsolationOpacity() == null) ||
11921
+ (existing && existing !== this.hoverLabel && existing.HasText) ||
11922
+ (group && group.options.presentation !== "icon")) {
11923
+ this.ReleaseHoverLabel(true);
11924
+ return;
11925
+ }
11926
+ if (this.hoverLabel && !this.hoverLabel.Disposed) {
11927
+ this.hoverLabel.SetHoverVisible(true);
11928
+ return;
11929
+ }
11930
+ // Query-only icon groups have coordinates but no registered scene visual to attach to.
11931
+ if (!rego && group && [group.item.longitude, group.item.latitude, (_c = group.item.altitude) !== null && _c !== void 0 ? _c : 0].every(Number.isFinite)) {
11932
+ rego = {
11933
+ entityId: hover.entityId,
11934
+ menuItemId: hover.menuItemId,
11935
+ name: group.options.text,
11936
+ priority: 0,
11937
+ visual: new Entity({
11938
+ position: Cartesian3.fromDegrees(group.item.longitude, group.item.latitude, group.item.altitude || 0),
11939
+ point: {
11940
+ pixelSize: 0,
11941
+ heightReference: HeightReference.NONE
11942
+ }
11943
+ })
11944
+ };
11945
+ }
11946
+ if (!rego) {
11947
+ return;
11948
+ }
11949
+ const params = {
11950
+ rego,
11951
+ viewer: this.viewer,
11952
+ visualRegister: this,
11953
+ api: (_d = this.ApiGetters) === null || _d === void 0 ? void 0 : _d.GetBruceApi(),
11954
+ persistent: false,
11955
+ transient: true,
11956
+ hoverRingState: this.hoverRingState,
11957
+ options: {
11958
+ presentation: "text",
11959
+ text: group === null || group === void 0 ? void 0 : group.options.text,
11960
+ selectOnClick: false
11961
+ },
11962
+ visible: () => {
11963
+ var _a, _b;
11964
+ const current = this.GetState(hover);
11965
+ return (!this.disposed && !current.hidden && ((_a = rego.visual) === null || _a === void 0 ? void 0 : _a.show) !== false &&
11966
+ Boolean(this.GetRego(hover) || ((_b = this.labelGroups) === null || _b === void 0 ? void 0 : _b.Has(hover.entityId, hover.menuItemId))) &&
11967
+ (!this.GetIsIsolatedAny() || current.isolated || this.GetIsolationOpacity() != null));
11968
+ }
11969
+ };
11970
+ this.hoverLabel = existing || group ? new EntityLabel.Label(params) : EntityLabel.Attach(params);
11971
+ }
11972
+ SetLabelContainer(container) {
11973
+ if (this.disposed) {
11974
+ return;
11975
+ }
11976
+ EntityLabel.SetContainer(this.viewer, container);
11977
+ }
11978
+ ClearLabelContainer() {
11979
+ this.SetLabelContainer();
11980
+ }
11981
+ /*
11982
+ * Adds source-driven membership without changing manual labelled state or legacy bookmark snapshots.
11983
+ */
11984
+ SetLabelGroup(group) {
11985
+ if (this.disposed) {
11986
+ return Promise.reject(new Error("Visual register is disposed"));
11987
+ }
11988
+ if (!this.labelGroups) {
11989
+ this.labelGroups = new EntityLabelGroup.Manager(this, () => this.labelGroupsChanged.Trigger(undefined));
11990
+ }
11991
+ return this.labelGroups.Set(group);
11992
+ }
11993
+ RemoveLabelGroup(id) {
11994
+ var _a;
11995
+ (_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.Remove(id);
11996
+ }
11997
+ RefreshLabelGroup(id) {
11998
+ var _a, _b;
11999
+ if (this.disposed) {
12000
+ return Promise.reject(new Error("Visual register is disposed"));
12001
+ }
12002
+ return (_b = (_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.Refresh(id)) !== null && _b !== void 0 ? _b : Promise.reject(new Error(`Unknown label group: ${id}`));
12003
+ }
12004
+ SetLabelGroupVisible(id, visible) {
12005
+ var _a, _b;
12006
+ return (_b = (_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.SetVisible(id, visible)) !== null && _b !== void 0 ? _b : false;
12007
+ }
12008
+ ClearLabelGroups() {
12009
+ var _a;
12010
+ (_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.Clear();
12011
+ }
12012
+ GetLabelGroups() {
12013
+ var _a;
12014
+ return ((_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.Get()) || [];
12015
+ }
12016
+ GetLabelGroupFor(entityId, menuItemId) {
12017
+ var _a;
12018
+ return (_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.Resolve(entityId, menuItemId);
12019
+ }
12020
+ RefreshLabelGroupEntity(entityId) {
12021
+ var _a, _b;
12022
+ (_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.RefreshEntity(entityId);
12023
+ if (((_b = this.labelHover) === null || _b === void 0 ? void 0 : _b.entityId) === entityId) {
12024
+ this.SyncLabelHover();
12025
+ }
12026
+ }
10116
12027
  }
10117
12028
  VisualsRegister.Register = Register;
10118
12029
  })(VisualsRegister || (VisualsRegister = {}));
@@ -10239,6 +12150,8 @@ function GetValue(viewer, obj) {
10239
12150
  }
10240
12151
  class Point {
10241
12152
  constructor(lon, lat, id) {
12153
+ this.radius = 0;
12154
+ this.minDistance = 0;
10242
12155
  this.lon = lon;
10243
12156
  this.lat = lat;
10244
12157
  this.id = id;
@@ -10256,8 +12169,8 @@ class Circle {
10256
12169
  let xDist = Math.abs(range.x - this.x);
10257
12170
  let yDist = Math.abs(range.y - this.y);
10258
12171
  let r = this.radius;
10259
- let w = range.width;
10260
- let h = range.height;
12172
+ let w = range.width / 2;
12173
+ let h = range.height / 2;
10261
12174
  if (xDist > (w + r) || yDist > (h + r)) {
10262
12175
  return false;
10263
12176
  }
@@ -10271,8 +12184,7 @@ class Circle {
10271
12184
  Contains(point) {
10272
12185
  const dx = this.x - point.lon;
10273
12186
  const dy = this.y - point.lat;
10274
- const distance = Math.sqrt(dx * dx + dy * dy);
10275
- return distance <= this.radius;
12187
+ return dx * dx + dy * dy <= this.radius * this.radius;
10276
12188
  }
10277
12189
  }
10278
12190
  class Rectangle$1 {
@@ -10309,13 +12221,13 @@ class Quad {
10309
12221
  let y = this.boundary.y;
10310
12222
  let w = this.boundary.w / 2;
10311
12223
  let h = this.boundary.h / 2;
10312
- let ne = new Rectangle$1(x + w / 2, y - h / 2, w, h);
12224
+ let ne = new Rectangle$1(x + w, y - h, w, h);
10313
12225
  this.northeast = new Quad(ne, this.capacity);
10314
- let nw = new Rectangle$1(x - w / 2, y - h / 2, w, h);
12226
+ let nw = new Rectangle$1(x - w, y - h, w, h);
10315
12227
  this.northwest = new Quad(nw, this.capacity);
10316
- let se = new Rectangle$1(x + w / 2, y + h / 2, w, h);
12228
+ let se = new Rectangle$1(x + w, y + h, w, h);
10317
12229
  this.southeast = new Quad(se, this.capacity);
10318
- let sw = new Rectangle$1(x - w / 2, y + h / 2, w, h);
12230
+ let sw = new Rectangle$1(x - w, y + h, w, h);
10319
12231
  this.southwest = new Quad(sw, this.capacity);
10320
12232
  this.divided = true;
10321
12233
  }
@@ -10323,7 +12235,8 @@ class Quad {
10323
12235
  if (!this.boundary.Contains(point)) {
10324
12236
  return false;
10325
12237
  }
10326
- if (this.points.length < this.capacity) {
12238
+ // Coincident points must terminate in a bucket rather than subdividing indefinitely.
12239
+ if (this.points.length < this.capacity || this.boundary.w < 1e-10) {
10327
12240
  this.points.push(point);
10328
12241
  return true;
10329
12242
  }
@@ -10553,496 +12466,635 @@ function getClusterImage(params) {
10553
12466
  }
10554
12467
  return null;
10555
12468
  }
10556
- const FORCE_UPDATE_BATCH_SIZE = 1000;
10557
- const FORCE_UPDATE_BATCH_DELAY = 100;
10558
- class PointClustering {
10559
- constructor(register, menuItemId, settings) {
10560
- this.minDistance = 5000;
10561
- this.minTotalCount = 0;
10562
- this.disposed = false;
10563
- this.registeredEntityIds = new Set();
10564
- // Queue to force update entities.
10565
- this.updateEntityQueue = [];
10566
- this.register = register;
10567
- this.viewer = register.Viewer;
10568
- this.menuItemId = menuItemId;
10569
- let boundary = new Rectangle$1(0, 0, 360, 180);
10570
- this.quadTree = new Quad(boundary, 30);
10571
- this.prevClusteredEntities = new Set();
10572
- this.currClusteredEntities = new Set();
10573
- this.clusterEntities = new Map();
10574
- this.updateQueue = new DelayQueue(() => {
10575
- this.doUpdate();
10576
- }, 1000);
10577
- this.listenCamera();
10578
- this.listenSelection();
10579
- if (settings) {
10580
- if (!isNaN(+settings.minDistance)) {
10581
- this.minDistance = +settings.minDistance;
10582
- }
10583
- if (!isNaN(+settings.minTotalCount)) {
10584
- this.minTotalCount = +settings.minTotalCount;
10585
- }
10586
- }
10587
- }
10588
- Update() {
10589
- this.updateQueue.Call();
12469
+ const clusterPickers = new WeakMap();
12470
+ /*
12471
+ * One capture listener set per viewer prevents host canvas picking from undoing cluster selection.
12472
+ */
12473
+ function getClusterPicker(viewer) {
12474
+ let picker = clusterPickers.get(viewer);
12475
+ if (picker) {
12476
+ return picker;
10590
12477
  }
10591
- queueForceUpdate(entityIds) {
10592
- for (let i = 0; i < entityIds.length; i++) {
10593
- if (this.updateEntityQueue.includes(entityIds[i])) {
10594
- continue;
12478
+ const canvas = viewer.canvas;
12479
+ picker = {
12480
+ owners: new Set(),
12481
+ targets: new Map(),
12482
+ remove: () => { }
12483
+ };
12484
+ let gesture;
12485
+ let released;
12486
+ const handler = (event) => {
12487
+ if (viewer.isDestroyed())
12488
+ return;
12489
+ const stop = () => { event.preventDefault(); event.stopImmediatePropagation(); };
12490
+ if (event.type === "pointermove") {
12491
+ if (gesture && Math.hypot(event.clientX - gesture.x, event.clientY - gesture.y) > 6) {
12492
+ gesture.moved = true;
10595
12493
  }
10596
- this.updateEntityQueue.push(entityIds[i]);
12494
+ return;
10597
12495
  }
10598
- this.runForceUpdateQueue();
10599
- }
10600
- runForceUpdateQueue() {
10601
- if (!this.updateEntityQueue.length) {
12496
+ if (event.type === "pointercancel") {
12497
+ if (gesture) {
12498
+ released = { ...gesture, moved: true };
12499
+ gesture = null;
12500
+ if (released.target) {
12501
+ stop();
12502
+ }
12503
+ }
10602
12504
  return;
10603
12505
  }
10604
- if (this.queueInterval) {
12506
+ if (event.button !== 0) {
10605
12507
  return;
10606
12508
  }
10607
- this.queueInterval = setInterval(() => {
10608
- if (this.disposed) {
10609
- clearInterval(this.queueInterval);
10610
- this.queueInterval = null;
12509
+ if (event.type === "pointerup" || event.type === "mouseup") {
12510
+ // Only consume releases whose press we owned, even if the cursor left the billboard.
12511
+ if (!gesture) {
10611
12512
  return;
10612
12513
  }
10613
- const ids = this.updateEntityQueue.splice(0, FORCE_UPDATE_BATCH_SIZE);
10614
- this.register.ForceUpdate({
10615
- entityIds: ids
10616
- });
10617
- if (!this.updateEntityQueue.length) {
10618
- clearInterval(this.queueInterval);
10619
- this.queueInterval = null;
12514
+ gesture.moved || (gesture.moved = Math.hypot(event.clientX - gesture.x, event.clientY - gesture.y) > 6);
12515
+ released = gesture;
12516
+ gesture = null;
12517
+ if (released.target) {
12518
+ stop();
10620
12519
  }
10621
- }, FORCE_UPDATE_BATCH_DELAY);
10622
- }
10623
- /**
10624
- * Starts listening to camera changes.
10625
- * This will trigger the clustering update whenever camera is moved.
10626
- */
10627
- listenCamera() {
10628
- const viewer = this.viewer;
10629
- let lastPos3d;
10630
- const hasMoved = () => {
10631
- var _a;
10632
- const cameraPos = (_a = viewer === null || viewer === void 0 ? void 0 : viewer.camera) === null || _a === void 0 ? void 0 : _a.positionCartographic;
10633
- if (!cameraPos) {
10634
- return false;
12520
+ return;
12521
+ }
12522
+ const rect = canvas.getBoundingClientRect();
12523
+ const picked = viewer.scene.pick(new Cartesian2(event.clientX - rect.left, event.clientY - rect.top));
12524
+ const select = picker.targets.get(picked === null || picked === void 0 ? void 0 : picked.id);
12525
+ if (event.type === "pointerdown" || event.type === "mousedown") {
12526
+ released = null;
12527
+ gesture = { target: select ? picked.id : null, x: event.clientX, y: event.clientY, moved: false };
12528
+ if (gesture.target) {
12529
+ stop();
10635
12530
  }
10636
- const pos3d = Cartographic.toCartesian(cameraPos);
10637
- if (!lastPos3d) {
10638
- lastPos3d = pos3d;
10639
- return false;
12531
+ return;
12532
+ }
12533
+ if (released) {
12534
+ const owned = !!released.target;
12535
+ const invalid = released.moved || released.target !== (picked === null || picked === void 0 ? void 0 : picked.id);
12536
+ released = null;
12537
+ if (!owned) {
12538
+ return;
10640
12539
  }
10641
- const distance = Cartesian3.distance(pos3d, lastPos3d);
10642
- lastPos3d = pos3d;
10643
- // 50 meter movement at least.
10644
- return distance > 50;
10645
- };
10646
- this.listenCameraRemoval = viewer.scene.camera.changed.addEventListener(() => {
10647
- if (hasMoved()) {
10648
- this.updateQueue.Call();
12540
+ if (invalid) {
12541
+ stop();
12542
+ return;
10649
12543
  }
10650
- });
10651
- }
10652
- /**
10653
- * Stops listening to camera changes.
10654
- */
10655
- unlistenCamera() {
10656
- if (this.listenCameraRemoval) {
10657
- this.listenCameraRemoval();
10658
- this.listenCameraRemoval = null;
10659
12544
  }
12545
+ if (!select) {
12546
+ return;
12547
+ }
12548
+ stop();
12549
+ if (event.type === "click") {
12550
+ select();
12551
+ }
12552
+ };
12553
+ const types = ["pointerdown", "pointermove", "pointerup", "pointercancel", "mousedown", "mouseup", "click", "dblclick"];
12554
+ if (canvas === null || canvas === void 0 ? void 0 : canvas.addEventListener) {
12555
+ for (const type of types) {
12556
+ canvas.addEventListener(type, handler, true);
12557
+ }
12558
+ picker.remove = () => {
12559
+ for (const type of types) {
12560
+ canvas.removeEventListener(type, handler, true);
12561
+ }
12562
+ };
10660
12563
  }
10661
- /**
10662
- * Starts listening to entity selection changes.
10663
- * Selected entities are not clustered.
10664
- */
10665
- listenSelection() {
10666
- this.listenUpdateRemoval = this.register.OnUpdate.Subscribe((data) => {
10667
- if (data.type == VisualsRegister.EVisualUpdateType.DeselectAll) {
10668
- this.updateQueue.Call();
12564
+ clusterPickers.set(viewer, picker);
12565
+ return picker;
12566
+ }
12567
+ /*
12568
+ * Count tiers are bounded so large clusters do not cover the scene with enormous billboards.
12569
+ */
12570
+ function PointClusterSize(count) {
12571
+ return count >= 10000 ? 140 : count >= 1000 ? 120 : count >= 100 ? 90 : count >= 10 ? 80 : 70;
12572
+ }
12573
+ class PointClustering {
12574
+ constructor(register, menuItemId, settings = {}) {
12575
+ this.register = register;
12576
+ this.menuItemId = menuItemId;
12577
+ this.settings = settings;
12578
+ this.minDistance = 5000;
12579
+ this.minTotalCount = 0;
12580
+ this.currClusteredEntities = new Set();
12581
+ this.clusterEntities = new Map();
12582
+ this.clusterViews = new Map();
12583
+ this.registeredEntityIds = new Set();
12584
+ this.records = new Map();
12585
+ this.disposed = false;
12586
+ this.removals = [];
12587
+ this.selectionQueued = false;
12588
+ this.viewer = register.Viewer;
12589
+ this.settings = settings = settings || {};
12590
+ if (Number.isFinite(+settings.minDistance)) {
12591
+ this.minDistance = Math.max(0, +settings.minDistance);
12592
+ }
12593
+ if (Number.isFinite(+settings.minTotalCount)) {
12594
+ this.minTotalCount = Math.max(0, +settings.minTotalCount);
12595
+ }
12596
+ this.quadTree = new Quad(new Rectangle$1(0, 0, 360, 180), 30);
12597
+ this.updateQueue = new DelayQueue(() => this.doUpdate(), 1000);
12598
+ this.picker = getClusterPicker(this.viewer);
12599
+ this.picker.owners.add(this);
12600
+ this.removals.push(this.viewer.scene.camera.changed.addEventListener(() => this.Update()));
12601
+ this.removals.push(this.viewer.scene.terrainProviderChanged.addEventListener(() => this.Update()));
12602
+ this.removals.push(register.OnUpdate.Subscribe(data => {
12603
+ if (data.type === VisualsRegister.EVisualUpdateType.DeselectAll ||
12604
+ (data.entityId && this.registeredEntityIds.has(data.entityId) && data.selected != null)) {
12605
+ if (!this.selectionQueued) {
12606
+ this.selectionQueued = true;
12607
+ Promise.resolve().then(() => {
12608
+ this.selectionQueued = false;
12609
+ if (!this.disposed)
12610
+ this.refreshSelection();
12611
+ });
12612
+ }
10669
12613
  }
10670
- else if (data.entityId &&
10671
- data.selected != null &&
10672
- data.type == VisualsRegister.EVisualUpdateType.Update &&
10673
- this.registeredEntityIds.has(data.entityId)) {
10674
- this.updateQueue.Call();
12614
+ if ("isolated" in data || (data.entityId && this.registeredEntityIds.has(data.entityId) && "hidden" in data)) {
12615
+ this.Update();
10675
12616
  }
10676
- });
12617
+ }));
10677
12618
  }
10678
- unlistenSelection() {
10679
- if (this.listenUpdateRemoval) {
10680
- this.listenUpdateRemoval();
10681
- this.listenUpdateRemoval = null;
12619
+ Update() {
12620
+ if (!this.disposed) {
12621
+ this.updateQueue.Call();
10682
12622
  }
10683
12623
  }
10684
- /**
10685
- * Removes all clusters and updates entities to no longer be suppressed.
10686
- * @returns
10687
- */
12624
+ GetClusters() {
12625
+ return Array.from(this.clusterViews.values(), view => ({ ...view, entityIds: [...view.entityIds] }));
12626
+ }
10688
12627
  Dispose() {
10689
12628
  if (this.disposed) {
10690
12629
  return;
10691
12630
  }
10692
- for (let [, entity] of Array.from(this.clusterEntities)) {
10693
- this.viewer.entities.remove(entity);
10694
- }
10695
- this.clusterEntities.clear();
10696
- this.unlistenCamera();
10697
- this.unlistenSelection();
10698
12631
  this.disposed = true;
10699
- this.updateEntityQueue = [];
10700
- // Restore entities.
10701
- const toUpdateIds = [];
10702
- for (let id of Array.from(this.registeredEntityIds)) {
10703
- const rego = this.register.GetRego({
10704
- entityId: id,
10705
- menuItemId: this.menuItemId
10706
- });
10707
- if (rego && rego.overrideShow) {
12632
+ this.updateQueue.Dispose();
12633
+ for (const remove of this.removals) {
12634
+ remove();
12635
+ }
12636
+ for (const id of this.clusterEntities.keys()) {
12637
+ this.removeClusterEntity(id);
12638
+ }
12639
+ this.picker.owners.delete(this);
12640
+ if (!this.picker.owners.size) {
12641
+ this.picker.remove();
12642
+ clusterPickers.delete(this.viewer);
12643
+ }
12644
+ const restore = [];
12645
+ for (const id of this.registeredEntityIds) {
12646
+ const rego = this.register.GetRego({ entityId: id, menuItemId: this.menuItemId });
12647
+ if ((rego === null || rego === void 0 ? void 0 : rego.overrideShow) === false) {
10708
12648
  rego.overrideShow = null;
10709
- toUpdateIds.push(id);
12649
+ restore.push(id);
10710
12650
  }
10711
12651
  }
10712
- if (toUpdateIds.length) {
10713
- this.register.ForceUpdate({
10714
- entityIds: toUpdateIds,
10715
- });
12652
+ this.records.clear();
12653
+ this.registeredEntityIds.clear();
12654
+ this.currClusteredEntities.clear();
12655
+ if (restore.length) {
12656
+ this.register.ForceUpdate({ entityIds: restore });
10716
12657
  }
10717
12658
  }
10718
- /**
10719
- * Calculates center of given points.
10720
- * @param points
10721
- * @returns
10722
- */
10723
- calculateCentroid(points) {
10724
- let lonSum = 0;
10725
- let latSum = 0;
10726
- for (let point of points) {
10727
- lonSum += point.lon;
10728
- latSum += point.lat;
12659
+ refreshSelection() {
12660
+ for (const view of this.clusterViews.values()) {
12661
+ const selected = view.entityIds.length > 0 && view.entityIds.every(entityId => this.register.GetIsSelected({ entityId, menuItemId: this.menuItemId }));
12662
+ if (selected === view.selected)
12663
+ continue;
12664
+ view.selected = selected;
12665
+ if (selected)
12666
+ CesiumEntityStyler.Select({ viewer: this.viewer, entity: view.entity, requestRender: false });
12667
+ else
12668
+ CesiumEntityStyler.Deselect({ viewer: this.viewer, entity: view.entity, requestRender: false });
10729
12669
  }
10730
- const lonAvg = lonSum / points.length;
10731
- const latAvg = latSum / points.length;
10732
- return new Point(lonAvg, latAvg, "");
12670
+ this.viewer.scene.requestRender();
10733
12671
  }
10734
- /**
10735
- * Gathers current clusters and renders them.
10736
- * @returns
10737
- */
10738
- doUpdate() {
10739
- if (this.disposed) {
12672
+ selectCluster(clusterId) {
12673
+ const view = this.clusterViews.get(clusterId);
12674
+ if (this.disposed || !view)
10740
12675
  return;
12676
+ this.register.ClearSelected({ requestRender: false });
12677
+ this.register.SetSelected({ entityIds: view.entityIds, selected: true });
12678
+ this.refreshSelection();
12679
+ }
12680
+ resolveGeometry(visual, seen = new Set()) {
12681
+ var _a, _b, _c;
12682
+ if (!visual || seen.has(visual)) {
12683
+ return null;
10741
12684
  }
10742
- // 1: Update precision.
10743
- // This defines how far apart these clusters can be.
10744
- const cameraHeight = this.viewer.camera.positionCartographic.height;
10745
- this.getClusterSpacing(cameraHeight);
10746
- // 2: Get clusters.
10747
- const { clusters, noLongerClustered } = this.getClusters();
10748
- const entitiesToUpdate = [];
10749
- // 3: Remove all cesium cluster entities that are no longer clustered.
10750
- for (let id of Array.from(noLongerClustered)) {
10751
- this.removeClusterEntity(id);
12685
+ seen.add(visual);
12686
+ const value = (property) => GetValue(this.viewer, property);
12687
+ const valid = (p) => p && [p.x, p.y, p.z].every(Number.isFinite) && Cartesian3.magnitudeSquared(p) > 0;
12688
+ const kind = visual.model || visual.box || visual.cylinder || visual.ellipsoid ? "model" :
12689
+ visual.polygon || visual.rectangle || visual.ellipse ? "polygon" :
12690
+ visual.polyline || visual.corridor || visual.wall || visual.polylineVolume ? "polyline" :
12691
+ visual.point || visual.billboard || visual.label ? "point" : "other";
12692
+ const configured = (_a = this.settings.minDistanceByType) === null || _a === void 0 ? void 0 : _a[kind];
12693
+ let minDistance = Number.isFinite(configured) ? Math.max(0, configured) : this.minDistance * (kind === "point" || kind === "other" ? 1 : 2);
12694
+ const graphic = (visual.model || visual.polygon || visual.rectangle || visual.ellipse || visual.polyline ||
12695
+ visual.corridor || visual.wall || visual.polylineVolume || visual.billboard || visual.point || visual.label || visual.box || visual.cylinder || visual.ellipsoid);
12696
+ let heightRef = (_b = value(graphic === null || graphic === void 0 ? void 0 : graphic.heightReference)) !== null && _b !== void 0 ? _b : HeightReference.NONE;
12697
+ if (visual.polyline) {
12698
+ heightRef = value(visual.polyline.clampToGround) ? HeightReference.CLAMP_TO_GROUND : HeightReference.NONE;
12699
+ }
12700
+ let positions;
12701
+ let height;
12702
+ if (visual.polygon) {
12703
+ const hierarchy = value(visual.polygon.hierarchy);
12704
+ positions = hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.positions;
12705
+ if (!value(visual.polygon.perPositionHeight)) {
12706
+ height = value(visual.polygon.height);
12707
+ if (height == null && value(visual.polygon.extrudedHeight) == null) {
12708
+ heightRef = HeightReference.CLAMP_TO_GROUND;
12709
+ }
12710
+ height !== null && height !== void 0 ? height : (height = 0);
12711
+ }
12712
+ }
12713
+ else if (visual.rectangle) {
12714
+ const rectangle = value(visual.rectangle.coordinates);
12715
+ if (rectangle) {
12716
+ positions = [Rectangle.northwest(rectangle), Rectangle.northeast(rectangle),
12717
+ Rectangle.southwest(rectangle), Rectangle.southeast(rectangle)].map(c => Cartographic.toCartesian(c));
12718
+ }
12719
+ height = value(visual.rectangle.height);
12720
+ if (height == null && value(visual.rectangle.extrudedHeight) == null) {
12721
+ heightRef = HeightReference.CLAMP_TO_GROUND;
12722
+ }
12723
+ height !== null && height !== void 0 ? height : (height = 0);
12724
+ }
12725
+ else if (visual.polyline || visual.corridor || visual.wall || visual.polylineVolume) {
12726
+ positions = value(graphic.positions);
12727
+ if (visual.wall && positions) {
12728
+ const minimum = value(visual.wall.minimumHeights);
12729
+ const maximum = value(visual.wall.maximumHeights);
12730
+ positions = positions.flatMap((position, index) => {
12731
+ var _a, _b;
12732
+ if (!valid(position)) {
12733
+ return [];
12734
+ }
12735
+ const carto = Cartographic.fromCartesian(position);
12736
+ if (!carto) {
12737
+ return [];
12738
+ }
12739
+ return ([(_a = minimum === null || minimum === void 0 ? void 0 : minimum[index]) !== null && _a !== void 0 ? _a : 0, (_b = maximum === null || maximum === void 0 ? void 0 : maximum[index]) !== null && _b !== void 0 ? _b : carto.height]
12740
+ .filter(Number.isFinite).map(height => Cartesian3.fromRadians(carto.longitude, carto.latitude, height)));
12741
+ });
12742
+ }
10752
12743
  }
10753
- // 4: Stop hiding entities that are no longer clustered.
10754
- const previousEntityIds = new Set(this.registeredEntityIds);
10755
- for (let id of Array.from(previousEntityIds)) {
10756
- if (!this.currClusteredEntities.has(id)) {
10757
- const rego = this.register.GetRego({
10758
- entityId: id,
10759
- menuItemId: this.menuItemId
12744
+ if (visual.corridor || visual.ellipse) {
12745
+ height = value(graphic.height);
12746
+ if (height == null && value(graphic.extrudedHeight) == null)
12747
+ heightRef = HeightReference.CLAMP_TO_GROUND;
12748
+ height !== null && height !== void 0 ? height : (height = 0);
12749
+ }
12750
+ if (!(positions === null || positions === void 0 ? void 0 : positions.length)) {
12751
+ const position = value(visual.position);
12752
+ if (valid(position)) {
12753
+ positions = [position];
12754
+ }
12755
+ }
12756
+ const children = visual._siblingGraphics || [];
12757
+ // Container entities often copy their first child's position; don't count that anchor twice.
12758
+ if (!graphic && children.length) {
12759
+ positions = [];
12760
+ minDistance = 0;
12761
+ }
12762
+ const resolved = [];
12763
+ for (const position of positions || []) {
12764
+ if (!valid(position))
12765
+ continue;
12766
+ let source = position;
12767
+ if (height != null) {
12768
+ const carto = Cartographic.fromCartesian(position);
12769
+ if (!carto)
12770
+ continue;
12771
+ source = Cartesian3.fromRadians(carto.longitude, carto.latitude, height);
12772
+ }
12773
+ const actual = DrawingUtils.EnsurePosHeight({
12774
+ viewer: this.viewer,
12775
+ pos3d: source,
12776
+ desiredHeightRef: HeightReference.NONE,
12777
+ heightRef
12778
+ });
12779
+ if (valid(actual)) {
12780
+ resolved.push(actual);
12781
+ }
12782
+ }
12783
+ const extrudedHeight = value(graphic === null || graphic === void 0 ? void 0 : graphic.extrudedHeight);
12784
+ if (Number.isFinite(extrudedHeight)) {
12785
+ const reference = (_c = value(graphic.extrudedHeightReference)) !== null && _c !== void 0 ? _c : HeightReference.NONE;
12786
+ for (const position of positions || []) {
12787
+ if (!valid(position)) {
12788
+ continue;
12789
+ }
12790
+ const carto = Cartographic.fromCartesian(position);
12791
+ if (!carto) {
12792
+ continue;
12793
+ }
12794
+ const actual = DrawingUtils.EnsurePosHeight({
12795
+ viewer: this.viewer,
12796
+ pos3d: Cartesian3.fromRadians(carto.longitude, carto.latitude, extrudedHeight),
12797
+ desiredHeightRef: HeightReference.NONE, heightRef: reference
10760
12798
  });
10761
- if (rego && rego.overrideShow != null) {
10762
- rego.overrideShow = null;
10763
- entitiesToUpdate.push(id);
12799
+ if (valid(actual)) {
12800
+ resolved.push(actual);
10764
12801
  }
10765
12802
  }
10766
12803
  }
10767
- const getScale = (count) => {
10768
- // const MIN_SCALE = 15;
10769
- // const MAX_SCALE = 80;
10770
- // const SCALE_PER_POINT = 1.01;
10771
- // let scale = MIN_SCALE + (count * SCALE_PER_POINT);
10772
- // scale = Math.min(scale, MAX_SCALE);
10773
- // return scale;
10774
- if (count >= 10000) {
10775
- return 140;
12804
+ const spheres = [];
12805
+ const anchors = [];
12806
+ if (resolved.length) {
12807
+ const sphere = BoundingSphere.fromPoints(resolved);
12808
+ // A chord midpoint can lie underground. Preserve the average rendered vertex altitude.
12809
+ const center = Cartographic.fromCartesian(sphere.center);
12810
+ if (center) {
12811
+ const altitude = resolved.reduce((sum, p) => sum + Cartographic.fromCartesian(p).height, 0) / resolved.length;
12812
+ const position = Cartesian3.fromRadians(center.longitude, center.latitude, altitude);
12813
+ sphere.radius += Cartesian3.distance(position, sphere.center);
12814
+ sphere.center = position;
12815
+ }
12816
+ if (visual.ellipse) {
12817
+ sphere.radius = Math.max(sphere.radius, value(visual.ellipse.semiMajorAxis) || 0, value(visual.ellipse.semiMinorAxis) || 0);
12818
+ }
12819
+ if (visual.box) {
12820
+ sphere.radius = Math.max(sphere.radius, Cartesian3.magnitude(value(visual.box.dimensions) || Cartesian3.ZERO) / 2);
12821
+ }
12822
+ if (visual.ellipsoid) {
12823
+ sphere.radius = Math.max(sphere.radius, Cartesian3.maximumComponent(value(visual.ellipsoid.radii) || Cartesian3.ZERO));
12824
+ }
12825
+ if (visual.cylinder) {
12826
+ sphere.radius = Math.max(sphere.radius, Math.hypot((value(visual.cylinder.length) || 0) / 2, Math.max(value(visual.cylinder.topRadius) || 0, value(visual.cylinder.bottomRadius) || 0)));
10776
12827
  }
10777
- if (count >= 1000) {
10778
- return 120;
12828
+ if (visual.corridor) {
12829
+ sphere.radius += (value(visual.corridor.width) || 0) / 2;
10779
12830
  }
10780
- if (count >= 100) {
10781
- return 90;
12831
+ if (visual.polylineVolume) {
12832
+ const shape = value(visual.polylineVolume.shape) || [];
12833
+ sphere.radius += shape.reduce((radius, p) => Math.max(radius, Math.hypot(p.x, p.y)), 0);
10782
12834
  }
10783
- if (count >= 10) {
10784
- return 80;
12835
+ spheres.push(sphere);
12836
+ anchors.push(sphere.center);
12837
+ }
12838
+ for (const child of children) {
12839
+ const result = this.resolveGeometry(child, seen);
12840
+ if (!result) {
12841
+ continue;
12842
+ }
12843
+ spheres.push(new BoundingSphere(result.position, result.radius));
12844
+ anchors.push(result.position);
12845
+ minDistance = Math.max(minDistance, result.minDistance);
12846
+ }
12847
+ if (!spheres.length) {
12848
+ return null;
12849
+ }
12850
+ const sphere = spheres.length === 1 ? spheres[0] : BoundingSphere.fromBoundingSpheres(spheres);
12851
+ if (!valid(sphere.center) || !Number.isFinite(sphere.radius)) {
12852
+ return null;
12853
+ }
12854
+ if (spheres.length > 1) {
12855
+ const carto = Cartographic.fromCartesian(sphere.center);
12856
+ if (!carto) {
12857
+ return null;
10785
12858
  }
10786
- return 70;
12859
+ const height = anchors.reduce((sum, p) => sum + Cartographic.fromCartesian(p).height, 0) / anchors.length;
12860
+ const position = Cartesian3.fromRadians(carto.longitude, carto.latitude, height);
12861
+ sphere.radius += Cartesian3.distance(position, sphere.center);
12862
+ sphere.center = position;
12863
+ }
12864
+ return { position: sphere.center, radius: sphere.radius, minDistance };
12865
+ }
12866
+ appearance(id) {
12867
+ var _a, _b, _c, _d, _e, _f;
12868
+ const { visual, metadata } = this.records.get(id);
12869
+ const data = metadata.entity || { Bruce: { ID: id } };
12870
+ let priority;
12871
+ let status;
12872
+ try {
12873
+ priority = Array.isArray(this.settings.iconPriority) ? Calculator.GetNumber(this.settings.iconPriority, data) : this.settings.iconPriority;
12874
+ status = Array.isArray(this.settings.iconStatus) ? Calculator.GetString(this.settings.iconStatus, data) : this.settings.iconStatus;
12875
+ }
12876
+ catch {
12877
+ priority = undefined;
12878
+ status = undefined;
12879
+ }
12880
+ const rawImage = GetValue(this.viewer, (_a = visual.billboard) === null || _a === void 0 ? void 0 : _a.image);
12881
+ const imageKey = (_b = visual.billboard) === null || _b === void 0 ? void 0 : _b._billboardImgKey;
12882
+ const iconUrl = (_c = metadata.iconUrl) !== null && _c !== void 0 ? _c : (typeof rawImage === "string" ? rawImage : undefined);
12883
+ // Original URLs retain status tokens after the point renderer converts an image into a data URI.
12884
+ const inferenceUrl = (_e = (_d = metadata.iconUrl) !== null && _d !== void 0 ? _d : imageKey) !== null && _e !== void 0 ? _e : iconUrl;
12885
+ const colorGraphic = visual.point || visual.polygon || visual.polyline || visual.corridor || visual.ellipse || visual.model;
12886
+ const color = colorGraphic ? CesiumEntityStyler.GetDefaultColor(this.viewer, colorGraphic) : null;
12887
+ let colorStatus;
12888
+ if (color && color.alpha > 0 && color.red > 0.5 && color.blue < 0.4) {
12889
+ colorStatus = color.green > 0.4 ? "warning" : "red";
12890
+ }
12891
+ const bgColor = color === null || color === void 0 ? void 0 : color.toCssColorString();
12892
+ return {
12893
+ priority: GetClusterIconPriority({
12894
+ iconPriority: Number.isFinite(priority) ? priority : metadata.iconPriority,
12895
+ iconStatus: (_f = status !== null && status !== void 0 ? status : metadata.iconStatus) !== null && _f !== void 0 ? _f : colorStatus, icon: metadata.icon, iconUrl: inferenceUrl
12896
+ }),
12897
+ iconUrl, bgColor,
12898
+ txtColor: color && color.red * 0.299 + color.green * 0.587 + color.blue * 0.114 > 0.5 ? "black" : "white"
10787
12899
  };
10788
- /**
10789
- * Generate circle with label in center.
10790
- * @param count
10791
- * @returns
10792
- */
10793
- const getCanvas = (count) => {
12900
+ }
12901
+ doUpdate() {
12902
+ var _a;
12903
+ if (this.disposed) {
12904
+ return;
12905
+ }
12906
+ const previous = new Set(this.currClusteredEntities);
12907
+ const { clusters } = this.getClusters();
12908
+ const live = new Set(clusters.map(cluster => cluster.center.id));
12909
+ for (const id of this.clusterEntities.keys()) {
12910
+ if (!live.has(id)) {
12911
+ this.removeClusterEntity(id);
12912
+ }
12913
+ }
12914
+ const changed = new Set();
12915
+ for (const id of new Set([...previous, ...this.registeredEntityIds])) {
12916
+ if (this.currClusteredEntities.has(id)) {
12917
+ continue;
12918
+ }
12919
+ const rego = this.register.GetRego({ entityId: id, menuItemId: this.menuItemId });
12920
+ if ((rego === null || rego === void 0 ? void 0 : rego.overrideShow) === false) {
12921
+ rego.overrideShow = null;
12922
+ changed.add(id);
12923
+ }
12924
+ }
12925
+ for (const cluster of clusters) {
12926
+ const clusterId = cluster.center.id;
12927
+ let representative = cluster.center;
12928
+ let appearance = this.appearance(representative.id);
12929
+ if (this.settings.representative !== "nearest") {
12930
+ for (const point of cluster.points) {
12931
+ const candidate = this.appearance(point.id);
12932
+ const distance = Cartesian3.distanceSquared(point.position, this.viewer.camera.positionWC || this.viewer.camera.position);
12933
+ const representativeDistance = Cartesian3.distanceSquared(representative.position, this.viewer.camera.positionWC || this.viewer.camera.position);
12934
+ if (candidate.priority > appearance.priority || (candidate.priority === appearance.priority &&
12935
+ (distance < representativeDistance || (distance === representativeDistance &&
12936
+ point.id.localeCompare(representative.id) < 0)))) {
12937
+ representative = point;
12938
+ appearance = candidate;
12939
+ }
12940
+ }
12941
+ }
12942
+ const position = new Cartesian3();
12943
+ for (const point of cluster.points) {
12944
+ Cartesian3.add(position, point.position, position);
12945
+ }
12946
+ Cartesian3.divideByScalar(position, cluster.points.length, position);
12947
+ const count = cluster.points.length;
10794
12948
  const params = {
10795
- size: getScale(count),
10796
- txtColor: this.pointColorTxt,
10797
- bgColor: this.pointColorBg,
12949
+ size: PointClusterSize(count),
10798
12950
  text: String(count),
10799
- iconUrl: this.iconUrl
12951
+ iconUrl: appearance.iconUrl,
12952
+ bgColor: appearance.bgColor,
12953
+ txtColor: appearance.txtColor
10800
12954
  };
10801
- return getClusterImage(params);
10802
- };
10803
- // 5: iterate over clusters and add/update them as Cesium entities.
10804
- for (let cluster of clusters) {
10805
- const clusterId = cluster.center.id;
10806
- let clusterEntity = this.clusterEntities.get(clusterId);
10807
- const centroid = this.calculateCentroid(cluster.points);
10808
- const count = cluster.points.length;
10809
- if (clusterEntity) {
10810
- clusterEntity.position = new ConstantPositionProperty(Cartesian3.fromDegrees(centroid.lon, centroid.lat, 150));
10811
- clusterEntity.billboard.image = new CallbackProperty(() => {
10812
- return getCanvas(count);
10813
- }, false),
10814
- clusterEntity.billboard.width = new ConstantProperty(getScale(cluster.points.length));
10815
- clusterEntity.billboard.height = new ConstantProperty(getScale(cluster.points.length));
10816
- }
10817
- else {
10818
- clusterEntity = this.viewer.entities.add({
10819
- position: Cartesian3.fromDegrees(centroid.lon, centroid.lat, 150),
12955
+ const image = new CallbackProperty(() => getClusterImage(params), false);
12956
+ let entity = this.clusterEntities.get(clusterId);
12957
+ if (!entity) {
12958
+ entity = this.viewer.entities.add({
12959
+ position,
10820
12960
  billboard: {
12961
+ image,
12962
+ color: Color.WHITE,
12963
+ width: params.size,
12964
+ height: params.size,
10821
12965
  heightReference: HeightReference.NONE,
10822
- image: new CallbackProperty(() => {
10823
- return getCanvas(count);
10824
- }, false),
10825
- width: getScale(count),
10826
- height: getScale(count),
10827
12966
  verticalOrigin: VerticalOrigin.CENTER,
10828
12967
  horizontalOrigin: HorizontalOrigin.CENTER,
10829
- disableDepthTestDistance: Number.POSITIVE_INFINITY,
10830
- scaleByDistance: new NearFarScalar(1.5e2, 0.5, 3.0e7, 0.1),
12968
+ disableDepthTestDistance: Infinity,
12969
+ scaleByDistance: new NearFarScalar(1.5e2, 0.5, 3.0e7, 0.1)
10831
12970
  }
10832
12971
  });
10833
- this.clusterEntities.set(clusterId, clusterEntity);
12972
+ this.clusterEntities.set(clusterId, entity);
12973
+ if (this.settings.selectOnClick !== false)
12974
+ this.picker.targets.set(entity, () => this.selectCluster(clusterId));
12975
+ }
12976
+ else {
12977
+ entity.position = new ConstantPositionProperty(position);
12978
+ entity.billboard.image = image;
12979
+ entity.billboard.width = new ConstantProperty(params.size);
12980
+ entity.billboard.height = new ConstantProperty(params.size);
10834
12981
  }
10835
- for (let i = 0; i < cluster.points.length; i++) {
10836
- const entityId = cluster.points[i].id;
12982
+ const old = this.clusterViews.get(clusterId);
12983
+ this.clusterViews.set(clusterId, {
12984
+ entity,
12985
+ entityIds: cluster.points.map(point => point.id),
12986
+ representativeId: representative.id,
12987
+ selected: (_a = old === null || old === void 0 ? void 0 : old.selected) !== null && _a !== void 0 ? _a : false
12988
+ });
12989
+ _loadClusterImage(params).then(() => {
12990
+ if (!this.disposed && !this.viewer.isDestroyed()) {
12991
+ this.viewer.scene.requestRender();
12992
+ }
12993
+ });
12994
+ for (const point of cluster.points) {
10837
12995
  const rego = this.register.GetRego({
10838
- entityId: entityId,
12996
+ entityId: point.id,
10839
12997
  menuItemId: this.menuItemId
10840
12998
  });
10841
- if (rego && rego.overrideShow != false) {
12999
+ if (rego && rego.overrideShow !== false) {
10842
13000
  rego.overrideShow = false;
10843
- entitiesToUpdate.push(entityId);
13001
+ changed.add(point.id);
10844
13002
  }
10845
13003
  }
10846
13004
  }
10847
- this.queueForceUpdate(entitiesToUpdate);
10848
- // 6: Iterate over existing cluster entities and remove those that are no longer clustered.
10849
- for (let [clusterId, clusterEntity] of Array.from(this.clusterEntities)) {
10850
- if (!clusters.find(x => x.center.id == clusterId)) {
10851
- this.viewer.entities.remove(clusterEntity);
10852
- this.clusterEntities.delete(clusterId);
10853
- }
13005
+ if (changed.size) {
13006
+ this.register.ForceUpdate({ entityIds: Array.from(changed) });
10854
13007
  }
10855
- this.viewer.scene.requestRender();
13008
+ this.refreshSelection();
10856
13009
  }
10857
- getClusterSpacing(distanceFromCluster) {
10858
- // Distance thresholds in meters from the cluster.
10859
- const distanceThresholds = [3000, 4000, 5300, 6000, 7000];
10860
- // Distance increments in degrees.
10861
- const distanceIncrements = [0.001, 0.002, 0.01, 0.03, 0.09];
10862
- let index = 0;
10863
- if (distanceFromCluster) {
10864
- for (let i = 0; i < distanceThresholds.length; i++) {
10865
- if (distanceFromCluster > distanceThresholds[i]) {
10866
- index = i;
10867
- }
10868
- }
10869
- }
10870
- return distanceIncrements[index];
13010
+ getClusterSpacing(distance) {
13011
+ return distance > 7000 ? 0.09 : distance > 6000 ? 0.03 : distance > 5300 ? 0.01 : distance > 4000 ? 0.002 : 0.001;
10871
13012
  }
10872
13013
  getClusters() {
13014
+ const previous = new Set(this.currClusteredEntities);
10873
13015
  this.currClusteredEntities.clear();
10874
- let clusters = [];
10875
- let processedPoints = new Set();
10876
- const cameraPosition = this.viewer.camera.position;
10877
- // We won't cluster selected entities.
10878
- const selectedIds = this.register.GetSelected();
10879
- // Only cluster if total registered points exceeds minTotalCount.
10880
- let validClusters = [];
10881
- if (this.registeredEntityIds.size >= this.minTotalCount) {
10882
- const MIN_CAMERA_DISTANCE = this.minDistance;
10883
- const processQuad = (quad) => {
10884
- // Distance to quad.
10885
- // TODO: Needs improvement.
10886
- const distanceToQuad = quad.GetDistanceToQuad(cameraPosition);
10887
- // Skip quads that are too close.
10888
- if (distanceToQuad >= MIN_CAMERA_DISTANCE) {
10889
- for (let point of quad.points) {
10890
- // Skip points already processed in previous clusters.
10891
- if (selectedIds.includes(point.id) || processedPoints.has(point.id)) {
10892
- continue;
10893
- }
10894
- // Skip points closer than MIN_CAMERA_DISTANCE meters to the camera.
10895
- const cartesian3 = Cartesian3.fromDegrees(point.lon, point.lat);
10896
- const distanceFromCluster = Cartesian3.distance(cartesian3, cameraPosition);
10897
- if (distanceFromCluster <= MIN_CAMERA_DISTANCE) {
10898
- continue;
10899
- }
10900
- let found = [];
10901
- let nearbyPoints = quad.Query(new Circle(point.lon, point.lat, this.getClusterSpacing(distanceFromCluster)), found);
10902
- nearbyPoints = nearbyPoints.filter(x => !processedPoints.has(x.id) && !selectedIds.includes(x.id));
10903
- if (nearbyPoints.length > 1) {
10904
- let cluster = { center: point, points: [] };
10905
- for (let nearby of nearbyPoints) {
10906
- if (!cluster.points.includes(nearby)) {
10907
- cluster.points.push(nearby);
10908
- processedPoints.add(nearby.id);
10909
- this.currClusteredEntities.add(nearby.id);
10910
- }
10911
- }
10912
- clusters.push(cluster);
10913
- }
10914
- }
10915
- }
10916
- if (quad.divided) {
10917
- processQuad(quad.northwest);
10918
- processQuad(quad.northeast);
10919
- processQuad(quad.southwest);
10920
- processQuad(quad.southeast);
10921
- }
10922
- };
10923
- processQuad(this.quadTree);
10924
- // Filter out clusters with only one point.
10925
- validClusters = clusters.filter((cluster) => cluster.points.length > 1);
13016
+ this.quadTree = new Quad(new Rectangle$1(0, 0, 360, 180), 30);
13017
+ const points = [];
13018
+ const distances = new Map();
13019
+ const camera = this.viewer.camera.positionWC || this.viewer.camera.position;
13020
+ const isolated = this.register.GetIsIsolatedAny();
13021
+ for (const [id, record] of this.records) {
13022
+ const state = this.register.GetState({ entityId: id, menuItemId: this.menuItemId });
13023
+ if (state.hidden || (isolated && !state.isolated)) {
13024
+ continue;
13025
+ }
13026
+ const geometry = this.resolveGeometry(record.visual);
13027
+ if (!geometry) {
13028
+ continue;
13029
+ }
13030
+ const { position, radius, minDistance } = geometry;
13031
+ const carto = Cartographic.fromCartesian(position);
13032
+ if (!carto) {
13033
+ continue;
13034
+ }
13035
+ const point = new Point(Math$1.toDegrees(carto.longitude), Math$1.toDegrees(carto.latitude), id);
13036
+ point.position = position;
13037
+ point.radius = radius;
13038
+ point.minDistance = minDistance;
13039
+ if (!this.quadTree.Insert(point)) {
13040
+ continue;
13041
+ }
13042
+ points.push(point);
13043
+ distances.set(id, Cartesian3.distance(position, camera));
10926
13044
  }
10927
- // Get the entity IDs that are no longer clustered
10928
- const noLongerClustered = new Set(Array.from(this.prevClusteredEntities).filter((id) => !this.currClusteredEntities.has(id)));
10929
- // Update the previous clustered entities ref.
10930
- this.prevClusteredEntities = new Set(this.currClusteredEntities);
10931
- return { clusters: validClusters, noLongerClustered };
10932
- }
10933
- /**
10934
- * Removes Cesium cluster entity.
10935
- * @param clusterId
10936
- */
10937
- removeClusterEntity(clusterId) {
10938
- const clusterEntity = this.clusterEntities.get(clusterId);
10939
- if (clusterEntity) {
10940
- this.viewer.entities.remove(clusterEntity);
10941
- this.clusterEntities.delete(clusterId);
13045
+ const clusters = points.length < this.minTotalCount ? [] :
13046
+ ClusterItems({
13047
+ items: points.sort((a, b) => distances.get(a.id) - distances.get(b.id) || a.id.localeCompare(b.id)),
13048
+ id: point => point.id,
13049
+ eligible: point => Math.max(0, distances.get(point.id) - point.radius) > point.minDistance,
13050
+ neighbours: point => this.quadTree.Query(new Circle(point.lon, point.lat, this.getClusterSpacing(distances.get(point.id))), [])
13051
+ }).map(cluster => ({ center: cluster.representative, points: cluster.members }));
13052
+ for (const cluster of clusters) {
13053
+ for (const point of cluster.points) {
13054
+ this.currClusteredEntities.add(point.id);
13055
+ }
10942
13056
  }
13057
+ return {
13058
+ clusters,
13059
+ noLongerClustered: new Set(Array.from(previous).filter(id => !this.currClusteredEntities.has(id)))
13060
+ };
10943
13061
  }
10944
- convertCartesianToCartographic(cartesian3) {
10945
- const carto = Cartographic.fromCartesian(cartesian3);
10946
- const lon = Math$1.toDegrees(carto.longitude);
10947
- const lat = Math$1.toDegrees(carto.latitude);
10948
- const id = lon + ',' + lat;
10949
- return new Point(lon, lat, id);
10950
- }
10951
- /**
10952
- * Adds new entity to the clustering logic.
10953
- * @param id Bruce entity ID.
10954
- * @param cartesian3 entity's position.
10955
- */
10956
- addPoint(id, cartesian3) {
10957
- let point = this.convertCartesianToCartographic(cartesian3);
10958
- point.id = id;
10959
- return this.quadTree.Insert(point);
13062
+ removeClusterEntity(id) {
13063
+ const entity = this.clusterEntities.get(id);
13064
+ if (!entity)
13065
+ return;
13066
+ this.picker.targets.delete(entity);
13067
+ this.viewer.entities.remove(entity);
13068
+ this.clusterEntities.delete(id);
13069
+ this.clusterViews.delete(id);
10960
13070
  }
10961
- /**
10962
- * Adds entity to clustering logic.
10963
- * Will return false if entity could not be clustered and therefor should not be hidden.
10964
- * @param id
10965
- * @param entity
10966
- * @param queueUpdate
10967
- * @returns
13071
+ /*
13072
+ * Metadata is additive; existing three-argument calls keep their original signature.
10968
13073
  */
10969
- AddEntity(id, entity, queueUpdate = true) {
10970
- if (this.disposed) {
10971
- return false;
10972
- }
10973
- if (!entity) {
10974
- return false;
10975
- }
10976
- if (entity.polygon || entity.polyline) {
10977
- return false;
10978
- }
10979
- const pos3d = GetValue(this.viewer, entity.position);
10980
- if (!(pos3d === null || pos3d === void 0 ? void 0 : pos3d.x)) {
13074
+ AddEntity(id, visual, queueUpdate = true, metadata = {}) {
13075
+ if (this.disposed || !visual) {
10981
13076
  return false;
10982
13077
  }
10983
- if (!this.pointColorBg && entity.point) {
10984
- const pointColorBg = GetValue(this.viewer, entity.point.color);
10985
- if (pointColorBg) {
10986
- let cColor = null;
10987
- if (pointColorBg instanceof Object) {
10988
- cColor = new Color(pointColorBg.red, pointColorBg.green, pointColorBg.blue, pointColorBg.alpha);
10989
- }
10990
- else if (typeof pointColorBg === "string") {
10991
- cColor = Color.fromCssColorString(pointColorBg);
10992
- }
10993
- // Determine if text color should instead be black based on background.
10994
- // cColor contains r,g,b,a values where r,g,b are in the range [0,1].
10995
- if (cColor) {
10996
- const brightness = (cColor.red * 299 + cColor.green * 587 + cColor.blue * 114) / 1000;
10997
- if (brightness > 0.5) {
10998
- this.pointColorTxt = "black";
10999
- }
11000
- else {
11001
- this.pointColorTxt = "white";
11002
- }
11003
- this.pointColorBg = cColor.toCssColorString();
11004
- }
11005
- }
11006
- }
11007
- if (!this.iconUrl && entity.billboard) {
11008
- const iconUrl = GetValue(this.viewer, entity.billboard.image);
11009
- if (typeof iconUrl == "string") {
11010
- this.iconUrl = iconUrl;
11011
- }
11012
- }
11013
- if (this.registeredEntityIds.has(id)) {
11014
- this.RemoveEntity(id, false);
11015
- }
11016
- const added = this.addPoint(id, pos3d);
11017
- if (!added) {
13078
+ if (!this.resolveGeometry(visual)) {
13079
+ this.RemoveEntity(id, queueUpdate);
11018
13080
  return false;
11019
13081
  }
13082
+ this.records.set(id, { visual, metadata });
11020
13083
  this.registeredEntityIds.add(id);
11021
13084
  if (queueUpdate) {
11022
- this.updateQueue.Call();
13085
+ this.Update();
11023
13086
  }
11024
13087
  return true;
11025
13088
  }
11026
- /**
11027
- * Removes entity from clustering logic.
11028
- * Warning: This will not reveal the entity, suppressShow will remain true.
11029
- * This is made with the assumption that the entity is being removed from viewer.
11030
- * @param id
11031
- * @param queueUpdate
11032
- * @returns
11033
- */
11034
13089
  RemoveEntity(id, queueUpdate = false) {
11035
13090
  if (this.disposed) {
11036
13091
  return;
11037
13092
  }
11038
- let pointToRemove = this.quadTree.Find(id);
11039
- if (pointToRemove) {
11040
- this.quadTree.Remove(pointToRemove);
11041
- if (queueUpdate) {
11042
- this.updateQueue.Call();
11043
- }
11044
- }
13093
+ this.records.delete(id);
11045
13094
  this.registeredEntityIds.delete(id);
13095
+ if (queueUpdate) {
13096
+ this.Update();
13097
+ }
11046
13098
  }
11047
13099
  }
11048
13100
 
@@ -11747,7 +13799,7 @@ var EntitiesRenderManager;
11747
13799
  });
11748
13800
  this.useGeojson = item.renderAsGeojson == true;
11749
13801
  if (item.enableClustering) {
11750
- this.clustering = new PointClustering(this.visualsManager, this.item.id);
13802
+ this.clustering = new PointClustering(this.visualsManager, this.item.id, this.item.clustering);
11751
13803
  }
11752
13804
  }
11753
13805
  Init(params) {
@@ -12783,8 +14835,8 @@ var EntitiesRenderManager;
12783
14835
  // The baseline data source must be editable.
12784
14836
  const canEdit = !((_b = entity.Bruce.Outline) === null || _b === void 0 ? void 0 : _b.find(x => x.Baseline && !x.Editable));
12785
14837
  const visual = rego === null || rego === void 0 ? void 0 : rego.visual;
14838
+ const wasClustered = this.clustering ? this.clustering.AddEntity(id, cEntity, false, { entity }) : false;
12786
14839
  if (!visual || visual != cEntity) {
12787
- const wasClustered = this.clustering ? this.clustering.AddEntity(id, cEntity, false) : false;
12788
14840
  const tagIds = entity.Bruce["Layer.ID"];
12789
14841
  const rego = {
12790
14842
  canEdit: canEdit,
@@ -14230,8 +16282,8 @@ var EntitiesIdsRenderManager;
14230
16282
  menuItemId: this.item.id
14231
16283
  });
14232
16284
  const visual = rego === null || rego === void 0 ? void 0 : rego.visual;
16285
+ const clustered = this.clustering ? this.clustering.AddEntity(id, cEntity, false, { entity }) : false;
14233
16286
  if (!visual || visual != cEntity) {
14234
- const clustered = this.clustering ? this.clustering.AddEntity(id, cEntity, false) : false;
14235
16287
  this.visualsManager.AddRego({
14236
16288
  rego: {
14237
16289
  canEdit: true,
@@ -48212,7 +50264,7 @@ var StyleUtils;
48212
50264
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
48213
50265
  })(StyleUtils || (StyleUtils = {}));
48214
50266
 
48215
- const VERSION = "7.3.9";
50267
+ const VERSION = "7.4.1";
48216
50268
  /**
48217
50269
  * Updates the environment instance used by bruce-cesium to one specified.
48218
50270
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.
@@ -48229,5 +50281,5 @@ const getENVIRONMENT = () => {
48229
50281
  return ENVIRONMENT;
48230
50282
  };
48231
50283
 
48232
- export { VERSION, setENVIRONMENT, getENVIRONMENT, CesiumAnimatedInOut, DisplacedSurfacePrimitive, CesiumAnimatedProperty, isStyleChanged, isOutlineChanged$1 as isOutlineChanged, StyleEffective, EntityRenderEngine, EntityRenderEngineModel3d, EntityRenderEnginePoint, EntityRenderEnginePolygon, EntityRenderEnginePolyline, TextureFrameSeriesAnimator, TextureValueLabels, MenuItemCreator, MenuItemManager, CesiumParabola, EntityLabel, LiveCursor, SharedGetters, DataSourceStaticKmlManager, DataLabRenderManager, EntitiesIdsRenderManager, EntitiesLoadedRenderManager, EntitiesRenderManager, EntityRenderManager, AssemblyRenderManager, GoogleSearchRenderManager, RelationsRenderManager, RenderManager, TilesetArbRenderManager, TilesetCadRenderManager, TilesetEntitiesRenderManager, TilesetGooglePhotosRenderManager, TilesetOsmRenderManager, TilesetPointcloudRenderManager, TileRenderEngine, createTileset, TilesetRenderEngine, ViewRenderEngine, VisualRegisterCuller, VisualsRegister, XGridsRenderEngine, CesiumEntityStyler, DrawingUtils, EntityUtils, MeasureUtils, StyleUtils, CESIUM_INSPECTOR_KEY, CESIUM_TIMELINE_KEY, CESIUM_TIMELINE_LIVE_KEY, CESIUM_TIMELINE_LIVE_PADDING_KEY, CESIUM_TIMELINE_INTERVAL_KEY, CESIUM_MODEL_SPACE_KEY, DEFAULT_LIVE_PADDING_SECONDS, DEFAULT_BRIGHTNESS_SHIFT, DEFAULT_LIGHT_INTENSITY, LIGHTING_SETTINGS_VERSION, LIGHT_INTENSITY_COMPENSATION, MAX_COMPENSATED_LIGHT_INTENSITY, ViewUtils, CesiumViewMonitor, ViewGroundArea, ViewerEventTracker, ViewerUtils, Draw3dPolygon, Draw3dPolyline, MeasureCreator, Walkthrough, WidgetControlViewBar, WidgetControlViewBarSearch, WidgetLeftPanelTab, WidgetLeftPanelTabBookmarks, WidgetLeftPanelTabDashboard, EMenuItemState, WidgetLeftPanelTabMenuItems, Widget, VIEWER_BOOKMARKS_WIDGET_KEY, WidgetBookmarks, WidgetBranding, ECursor, WidgetCursorBar, WidgetEmbeddedInfoView, WidgetInfoView, VIEWER_LEFT_PANEL_WIDGET_KEY, VIEWER_LEFT_PANEL_CSS_VAR_LEFT, WidgetLeftPanel, WidgetNavCompass$$1 as WidgetNavCompass, VIEWER_VIEW_BAR_WIDGET_KEY, WidgetViewBar };
50284
+ export { VERSION, setENVIRONMENT, getENVIRONMENT, ClusterItems, PointClustering, PointClusterSize, CesiumAnimatedInOut, DisplacedSurfacePrimitive, CesiumAnimatedProperty, isStyleChanged, isOutlineChanged$1 as isOutlineChanged, StyleEffective, EntityRenderEngine, EntityRenderEngineModel3d, EntityRenderEnginePoint, EntityRenderEnginePolygon, EntityRenderEnginePolyline, TextureFrameSeriesAnimator, TextureValueLabels, MenuItemCreator, MenuItemManager, CesiumParabola, EntityLabel, EntityLabelGroup, LiveCursor, SharedGetters, DataSourceStaticKmlManager, DataLabRenderManager, EntitiesIdsRenderManager, EntitiesLoadedRenderManager, EntitiesRenderManager, EntityRenderManager, AssemblyRenderManager, GoogleSearchRenderManager, RelationsRenderManager, RenderManager, TilesetArbRenderManager, TilesetCadRenderManager, TilesetEntitiesRenderManager, TilesetGooglePhotosRenderManager, TilesetOsmRenderManager, TilesetPointcloudRenderManager, TileRenderEngine, createTileset, TilesetRenderEngine, ViewRenderEngine, VisualRegisterCuller, VisualsRegister, XGridsRenderEngine, CesiumEntityStyler, DrawingUtils, EntityUtils, MeasureUtils, StyleUtils, CESIUM_INSPECTOR_KEY, CESIUM_TIMELINE_KEY, CESIUM_TIMELINE_LIVE_KEY, CESIUM_TIMELINE_LIVE_PADDING_KEY, CESIUM_TIMELINE_INTERVAL_KEY, CESIUM_MODEL_SPACE_KEY, DEFAULT_LIVE_PADDING_SECONDS, DEFAULT_BRIGHTNESS_SHIFT, DEFAULT_LIGHT_INTENSITY, LIGHTING_SETTINGS_VERSION, LIGHT_INTENSITY_COMPENSATION, MAX_COMPENSATED_LIGHT_INTENSITY, ViewUtils, CesiumViewMonitor, ViewGroundArea, ViewerEventTracker, ViewerUtils, Draw3dPolygon, Draw3dPolyline, MeasureCreator, Walkthrough, WidgetControlViewBar, WidgetControlViewBarSearch, WidgetLeftPanelTab, WidgetLeftPanelTabBookmarks, WidgetLeftPanelTabDashboard, EMenuItemState, WidgetLeftPanelTabMenuItems, Widget, VIEWER_BOOKMARKS_WIDGET_KEY, WidgetBookmarks, WidgetBranding, ECursor, WidgetCursorBar, WidgetEmbeddedInfoView, WidgetInfoView, VIEWER_LEFT_PANEL_WIDGET_KEY, VIEWER_LEFT_PANEL_CSS_VAR_LEFT, WidgetLeftPanel, WidgetNavCompass$$1 as WidgetNavCompass, VIEWER_VIEW_BAR_WIDGET_KEY, WidgetViewBar };
48233
50285
  //# sourceMappingURL=bruce-cesium.es5.js.map