bruce-cesium 7.3.8 โ†’ 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/bruce-cesium.es5.js +2911 -626
  2. package/dist/bruce-cesium.es5.js.map +1 -1
  3. package/dist/bruce-cesium.umd.js +2910 -623
  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/entity-gatherer.js +249 -16
  8. package/dist/lib/rendering/entity-gatherer.js.map +1 -1
  9. package/dist/lib/rendering/render-managers/common/entity-label-group.js +482 -0
  10. package/dist/lib/rendering/render-managers/common/entity-label-group.js.map +1 -0
  11. package/dist/lib/rendering/render-managers/common/entity-label.js +1213 -155
  12. package/dist/lib/rendering/render-managers/common/entity-label.js.map +1 -1
  13. package/dist/lib/rendering/render-managers/common/historic-utils.js +64 -7
  14. package/dist/lib/rendering/render-managers/common/historic-utils.js.map +1 -1
  15. package/dist/lib/rendering/render-managers/common/point-clustering-core.js +85 -0
  16. package/dist/lib/rendering/render-managers/common/point-clustering-core.js.map +1 -0
  17. package/dist/lib/rendering/render-managers/common/point-clustering.js +566 -419
  18. package/dist/lib/rendering/render-managers/common/point-clustering.js.map +1 -1
  19. package/dist/lib/rendering/render-managers/entities/entities-ids-render-manager.js +1 -1
  20. package/dist/lib/rendering/render-managers/entities/entities-ids-render-manager.js.map +1 -1
  21. package/dist/lib/rendering/render-managers/entities/entities-render-manager.js +2 -2
  22. package/dist/lib/rendering/render-managers/entities/entities-render-manager.js.map +1 -1
  23. package/dist/lib/rendering/visuals-register.js +273 -24
  24. package/dist/lib/rendering/visuals-register.js.map +1 -1
  25. package/dist/types/bruce-cesium.d.ts +4 -1
  26. package/dist/types/rendering/entity-gatherer.d.ts +27 -4
  27. package/dist/types/rendering/render-managers/common/entity-label-group.d.ts +104 -0
  28. package/dist/types/rendering/render-managers/common/entity-label.d.ts +138 -1
  29. package/dist/types/rendering/render-managers/common/historic-utils.d.ts +12 -4
  30. package/dist/types/rendering/render-managers/common/point-clustering-core.d.ts +17 -0
  31. package/dist/types/rendering/render-managers/common/point-clustering.d.ts +37 -72
  32. package/dist/types/rendering/visuals-register.d.ts +43 -2
  33. package/package.json +1 -1
@@ -5283,6 +5283,86 @@
5283
5283
  return gate;
5284
5284
  }
5285
5285
 
5286
+ /*
5287
+ * Status semantics are shared by label and scene-entity clusters; opaque image pixels are not inferred.
5288
+ * A bit jank but helps keep current 'important' labels higher than others.
5289
+ */
5290
+ function GetClusterIconPriority(options) {
5291
+ if (Number.isFinite(options === null || options === void 0 ? void 0 : options.iconPriority)) {
5292
+ return options.iconPriority;
5293
+ }
5294
+ const statusPriority = (value) => {
5295
+ const tokens = value.toLowerCase().split(/[^a-z0-9]+/);
5296
+ if (tokens.some(token => ["critical", "fatal", "error", "fault", "failed", "alarm", "danger", "red"].includes(token))) {
5297
+ return 3;
5298
+ }
5299
+ if (tokens.some(token => ["warning", "warn", "degraded", "amber", "orange", "yellow"].includes(token))) {
5300
+ return 2;
5301
+ }
5302
+ if (tokens.some(token => ["unknown", "offline"].includes(token))) {
5303
+ return 1;
5304
+ }
5305
+ return 0;
5306
+ };
5307
+ if ((options === null || options === void 0 ? void 0 : options.iconStatus) != null) {
5308
+ return statusPriority(options.iconStatus);
5309
+ }
5310
+ const glyph = (options === null || options === void 0 ? void 0 : options.icon) || "";
5311
+ if (/[๐Ÿ”ด๐ŸŸฅโŒ๐Ÿšจ]/u.test(glyph)) {
5312
+ return 3;
5313
+ }
5314
+ if (/[๐ŸŸ ๐ŸŸกโš ]/u.test(glyph)) {
5315
+ return 2;
5316
+ }
5317
+ let filename = "";
5318
+ if ((options === null || options === void 0 ? void 0 : options.iconUrl) && !/^(data|blob):/i.test(options.iconUrl)) {
5319
+ try {
5320
+ filename = decodeURIComponent(new URL(options.iconUrl, "https://label.invalid").pathname.split("/").pop());
5321
+ }
5322
+ catch {
5323
+ // Ignore malformed URLs when inferring status.
5324
+ }
5325
+ }
5326
+ return Math.max(statusPriority(glyph), statusPriority(filename));
5327
+ }
5328
+ /*
5329
+ * Groups eligible neighbours around stable representatives without coupling membership to its renderer.
5330
+ */
5331
+ function ClusterItems(params) {
5332
+ const eligible = new Map();
5333
+ for (const item of params.items) {
5334
+ if (!params.eligible || params.eligible(item)) {
5335
+ eligible.set(params.id(item), item);
5336
+ }
5337
+ }
5338
+ const assigned = new Set();
5339
+ const result = [];
5340
+ const minimum = Math.max(2, params.minSize || 2);
5341
+ for (const [id, item] of eligible) {
5342
+ if (assigned.has(id)) {
5343
+ continue;
5344
+ }
5345
+ const members = new Map([[id, item]]);
5346
+ for (const neighbour of params.neighbours(item)) {
5347
+ const key = params.id(neighbour);
5348
+ if (eligible.has(key) && !assigned.has(key)) {
5349
+ members.set(key, eligible.get(key));
5350
+ }
5351
+ }
5352
+ if (members.size < minimum) {
5353
+ continue;
5354
+ }
5355
+ for (const key of members.keys()) {
5356
+ assigned.add(key);
5357
+ }
5358
+ result.push({
5359
+ representative: item,
5360
+ members: Array.from(members.values())
5361
+ });
5362
+ }
5363
+ return result;
5364
+ }
5365
+
5286
5366
  /**
5287
5367
  * Returns cesium property's value.
5288
5368
  * This will check if it's one that changes over time, or just a fixed value.
@@ -5301,10 +5381,564 @@
5301
5381
  return obj;
5302
5382
  }
5303
5383
  (function (EntityLabel) {
5384
+ const ICON_BACKGROUND_BLEND = 0.8;
5385
+ const ICON_BACKGROUND_ALPHA = 0.45;
5386
+ const TEXT_MAX_WIDTH = 200;
5304
5387
  // Key where the label instance is stored within the cesium entity.
5305
5388
  // Eg: rego[ATTACH_KEY] = new EntityLabel(...);
5306
5389
  EntityLabel.ATTACH_KEY = "nextspace-navigator-entity-label";
5390
+ /*
5391
+ * Indexes screen rectangles so sparse scenes do not compare every label with every other label.
5392
+ */
5393
+ class ScreenIndex {
5394
+ constructor() {
5395
+ this.cells = new Map();
5396
+ this.large = [];
5397
+ this.all = [];
5398
+ }
5399
+ keys(frame, padding = 0) {
5400
+ const x0 = Math.floor((frame.x - padding) / 64);
5401
+ const y0 = Math.floor((frame.y - padding) / 64);
5402
+ const x1 = Math.floor((frame.x + frame.width + padding) / 64);
5403
+ const y1 = Math.floor((frame.y + frame.height + padding) / 64);
5404
+ if ((x1 - x0 + 1) * (y1 - y0 + 1) > 4096) {
5405
+ return null;
5406
+ }
5407
+ const keys = [];
5408
+ for (let x = x0; x <= x1; x++) {
5409
+ for (let y = y0; y <= y1; y++) {
5410
+ keys.push(`${x},${y}`);
5411
+ }
5412
+ }
5413
+ return keys;
5414
+ }
5415
+ Add(frame) {
5416
+ this.all.push(frame);
5417
+ const keys = this.keys(frame);
5418
+ if (!keys) {
5419
+ this.large.push(frame);
5420
+ return;
5421
+ }
5422
+ for (const key of keys) {
5423
+ if (!this.cells.has(key)) {
5424
+ this.cells.set(key, []);
5425
+ }
5426
+ this.cells.get(key).push(frame);
5427
+ }
5428
+ }
5429
+ Query(frame, padding = 0) {
5430
+ const keys = this.keys(frame, padding);
5431
+ const found = new Set(keys ? this.large : this.all);
5432
+ for (const key of keys || []) {
5433
+ for (const item of this.cells.get(key) || []) {
5434
+ found.add(item);
5435
+ }
5436
+ }
5437
+ return (Array.from(found).filter(other => frame.x <= other.x + other.width + padding && frame.x + frame.width + padding >= other.x &&
5438
+ frame.y <= other.y + other.height + padding && frame.y + frame.height + padding >= other.y));
5439
+ }
5440
+ HasOverlap(frame) {
5441
+ var _a;
5442
+ const intersects = (other) => (frame.x <= other.x + other.width && frame.x + frame.width >= other.x &&
5443
+ frame.y <= other.y + other.height && frame.y + frame.height >= other.y);
5444
+ const keys = this.keys(frame);
5445
+ if ((keys ? this.large : this.all).some(intersects)) {
5446
+ return true;
5447
+ }
5448
+ for (const key of keys || []) {
5449
+ if ((_a = this.cells.get(key)) === null || _a === void 0 ? void 0 : _a.some(intersects)) {
5450
+ return true;
5451
+ }
5452
+ }
5453
+ return false;
5454
+ }
5455
+ }
5456
+ /*
5457
+ * Includes the stem in collision checks without treating two crossing stems as text occlusion.
5458
+ */
5459
+ function leaderBounds(frame) {
5460
+ if (!frame.hasLeader || !Number.isFinite(frame.anchorX) || !Number.isFinite(frame.anchorY)) {
5461
+ return undefined;
5462
+ }
5463
+ const endX = Math.max(frame.x, Math.min(frame.anchorX, frame.x + frame.width));
5464
+ const endY = Math.max(frame.y, Math.min(frame.anchorY, frame.y + frame.height));
5465
+ if (Math.hypot(frame.anchorX - endX, frame.anchorY - endY) < .01) {
5466
+ return undefined;
5467
+ }
5468
+ return {
5469
+ ...frame,
5470
+ x: Math.min(frame.anchorX, endX) - 1,
5471
+ y: Math.min(frame.anchorY, endY) + .01,
5472
+ width: Math.abs(frame.anchorX - endX) + 2,
5473
+ height: Math.max(0, Math.abs(frame.anchorY - endY) - .02)
5474
+ };
5475
+ }
5476
+ /*
5477
+ * Remembers sides, never rendered positions: its own output cannot trigger another layout pass.
5478
+ */
5479
+ class PlacementMemory {
5480
+ constructor() {
5481
+ this.previous = new Map();
5482
+ this.nextOrder = 0;
5483
+ }
5484
+ Resolve(frames) {
5485
+ var _a, _b;
5486
+ const live = new Set(frames.map(frame => frame.id));
5487
+ const moved = (frame) => {
5488
+ var _a;
5489
+ const old = (_a = this.previous.get(frame.id)) === null || _a === void 0 ? void 0 : _a.frame;
5490
+ return (!old ||
5491
+ old.transient !== frame.transient || old.canFlip !== frame.canFlip ||
5492
+ Math.hypot(frame.x - old.x, frame.y - old.y) >= 24 ||
5493
+ Math.abs(frame.width - old.width) >= 8 || Math.abs(frame.height - old.height) >= 8);
5494
+ };
5495
+ const persistent = frames.filter(frame => !frame.transient);
5496
+ const changes = (persistent.filter(moved).length +
5497
+ Array.from(this.previous.values()).filter(old => !old.frame.transient && !live.has(old.frame.id)).length);
5498
+ // A hover or one arrival cannot rearrange incumbents; a substantial batch/camera change can.
5499
+ const restructure = this.previous.size > 0 && changes >= Math.max(2, Math.ceil(persistent.length * 0.3));
5500
+ const fixed = (frame) => !restructure && !frame.transient && this.previous.has(frame.id) && !moved(frame);
5501
+ const ordered = ([...frames].sort((a, b) => {
5502
+ var _a, _b, _c, _d;
5503
+ return Number(fixed(b)) - Number(fixed(a)) ||
5504
+ Number(Boolean(a.transient)) - Number(Boolean(b.transient)) ||
5505
+ ((_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) ||
5506
+ a.depth - b.depth || a.id.localeCompare(b.id);
5507
+ }));
5508
+ const text = new ScreenIndex(), leaders = new ScreenIndex();
5509
+ const placed = new Map();
5510
+ const next = new Map();
5511
+ const candidate = (frame, side) => {
5512
+ var _a;
5513
+ return (side === "left" && frame.canFlip ?
5514
+ { ...frame, x: Math.max(0, Math.min(frame.anchorX - frame.width, ((_a = frame.viewportWidth) !== null && _a !== void 0 ? _a : Infinity) - frame.width)) } :
5515
+ frame);
5516
+ };
5517
+ const score = (frame, padding = 0) => {
5518
+ const box = {
5519
+ ...frame,
5520
+ x: frame.x - padding + .01,
5521
+ y: frame.y - padding + .01,
5522
+ width: Math.max(0, frame.width + padding * 2 - .02),
5523
+ height: Math.max(0, frame.height + padding * 2 - .02)
5524
+ };
5525
+ const stem = leaderBounds(frame);
5526
+ return (Number(text.HasOverlap(box)) * 4 + Number(leaders.HasOverlap(box)) +
5527
+ Number(Boolean(stem && text.HasOverlap(stem))));
5528
+ };
5529
+ for (const frame of ordered) {
5530
+ const old = this.previous.get(frame.id);
5531
+ let side = frame.canFlip ? (_a = old === null || old === void 0 ? void 0 : old.side) !== null && _a !== void 0 ? _a : "right" : "right";
5532
+ let chosen = candidate(frame, side);
5533
+ if (!fixed(frame) && frame.canFlip) {
5534
+ const otherSide = side === "left" ? "right" : "left";
5535
+ const other = candidate(frame, otherSide);
5536
+ // An established side only changes when the alternative has a little breathing room.
5537
+ if (score(other, old ? 4 : 0) < score(chosen)) {
5538
+ side = otherSide;
5539
+ chosen = other;
5540
+ }
5541
+ }
5542
+ placed.set(frame.id, { frame: chosen, side });
5543
+ 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++ });
5544
+ text.Add(chosen);
5545
+ const stem = leaderBounds(chosen);
5546
+ if (stem) {
5547
+ leaders.Add(stem);
5548
+ }
5549
+ }
5550
+ this.previous = next;
5551
+ return placed;
5552
+ }
5553
+ }
5554
+ EntityLabel.PlacementMemory = PlacementMemory;
5555
+ /*
5556
+ * Prefer declared semantics; only infers from recognisable glyphs and filename tokens, never image pixels.
5557
+ */
5558
+ EntityLabel.IconPriority = GetClusterIconPriority;
5559
+ /*
5560
+ * Resolves clustering and stable text placement before depth fading; highlighted labels receive priority.
5561
+ */
5562
+ function Layout(input, memory = new PlacementMemory()) {
5563
+ var _a, _b, _c, _d, _e, _f, _g, _h;
5564
+ const frames = input.filter(frame => [frame.x, frame.y, frame.width, frame.height, frame.depth].every(Number.isFinite))
5565
+ .sort((a, b) => (b.priority || 0) - (a.priority || 0) || a.depth - b.depth || a.id.localeCompare(b.id));
5566
+ const result = new Map();
5567
+ const entityKeys = new Set();
5568
+ const groups = new Map();
5569
+ for (const frame of frames) {
5570
+ const duplicate = frame.entityKey && entityKeys.has(frame.entityKey);
5571
+ result.set(frame.id, { frame, visible: !duplicate, opacity: frame.opacity, zIndex: 0, members: [], entityIds: frame.entityId ? [frame.entityId] : [] });
5572
+ if (duplicate) {
5573
+ continue;
5574
+ }
5575
+ if (frame.entityKey)
5576
+ entityKeys.add(frame.entityKey);
5577
+ if (!((_b = (_a = frame.options) === null || _a === void 0 ? void 0 : _a.cluster) === null || _b === void 0 ? void 0 : _b.enabled) || frame.priority > 0) {
5578
+ continue;
5579
+ }
5580
+ const key = JSON.stringify([frame.group || "", frame.options.presentation || "text"]);
5581
+ if (!groups.has(key)) {
5582
+ groups.set(key, []);
5583
+ }
5584
+ groups.get(key).push(frame);
5585
+ }
5586
+ for (const group of groups.values()) {
5587
+ // Summary and badge dimensions are output, never the next frame's membership input.
5588
+ const candidates = group.map(frame => frame.extendedGeometry && frame.canFlip &&
5589
+ frame.options.cluster.pixelRange == null ? { ...frame, x: frame.anchorX, y: frame.anchorY, width: 1, height: 1 } :
5590
+ frame.clusterBounds ? { ...frame, ...frame.clusterBounds } : frame);
5591
+ const index = new ScreenIndex();
5592
+ for (const frame of candidates) {
5593
+ index.Add(frame);
5594
+ }
5595
+ const clusters = ClusterItems({
5596
+ items: candidates,
5597
+ id: frame => frame.id,
5598
+ minSize: group[0].options.cluster.minSize,
5599
+ neighbours: frame => {
5600
+ var _a;
5601
+ return (index.Query(frame, (_a = frame.options.cluster.pixelRange) !== null && _a !== void 0 ? _a : (frame.options.presentation === "icon" ? 24 : 8)));
5602
+ }
5603
+ });
5604
+ for (const cluster of clusters) {
5605
+ let chosen = cluster.representative;
5606
+ if ((chosen.options.presentation || "text") !== "text" && chosen.options.cluster.representative !== "nearest") {
5607
+ let priority = EntityLabel.IconPriority(chosen.options);
5608
+ for (const member of cluster.members) {
5609
+ const candidate = EntityLabel.IconPriority(member.options);
5610
+ if (candidate > priority) {
5611
+ chosen = member;
5612
+ priority = candidate;
5613
+ }
5614
+ }
5615
+ }
5616
+ const representative = result.get(chosen.id);
5617
+ representative.members = cluster.members.map(frame => frame.id);
5618
+ representative.entityIds = Array.from(new Set(cluster.members.map(frame => frame.entityId).filter(Boolean)));
5619
+ if (chosen.options.presentation !== "icon") {
5620
+ const values = new Set();
5621
+ const stableMembers = [...cluster.members].sort((a, b) => (a.entityKey || a.entityId || a.id).localeCompare(b.entityKey || b.entityId || b.id));
5622
+ for (const member of stableMembers) {
5623
+ 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();
5624
+ if (value) {
5625
+ values.add(value);
5626
+ }
5627
+ }
5628
+ if (values.size > 1) {
5629
+ 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()); });
5630
+ if (description) {
5631
+ representative.text = ((_f = description.text) !== null && _f !== void 0 ? _f : description.options.text).trim();
5632
+ representative.summaryMore = true;
5633
+ }
5634
+ else {
5635
+ const rows = Array.from(values).slice(0, 2);
5636
+ if (values.size > 2)
5637
+ rows.push(`+${values.size - 2} moreโ€ฆ`);
5638
+ representative.text = rows.join("\n");
5639
+ }
5640
+ }
5641
+ }
5642
+ for (const frame of cluster.members) {
5643
+ if (frame !== chosen)
5644
+ result.get(frame.id).visible = false;
5645
+ }
5646
+ }
5647
+ }
5648
+ const placed = memory.Resolve(frames.filter(frame => result.get(frame.id).visible));
5649
+ const foreground = new ScreenIndex(), foregroundLeaders = new ScreenIndex();
5650
+ let zIndex = frames.length + 1;
5651
+ for (const original of frames) {
5652
+ const placement = result.get(original.id);
5653
+ if (!placement.visible) {
5654
+ continue;
5655
+ }
5656
+ const chosen = placed.get(original.id);
5657
+ const frame = placement.frame = chosen.frame;
5658
+ placement.textSide = chosen.side;
5659
+ const stem = leaderBounds(frame);
5660
+ 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) };
5661
+ const overlap = foreground.HasOverlap(box) || foregroundLeaders.HasOverlap(box) || Boolean(stem && foreground.HasOverlap(stem));
5662
+ if (overlap) {
5663
+ 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));
5664
+ }
5665
+ placement.zIndex = zIndex--;
5666
+ foreground.Add(frame);
5667
+ if (stem) {
5668
+ foregroundLeaders.Add(stem);
5669
+ }
5670
+ }
5671
+ return Array.from(result.values());
5672
+ }
5673
+ EntityLabel.Layout = Layout;
5674
+ const layers = new WeakMap();
5675
+ const containers = new WeakMap();
5676
+ /*
5677
+ * Moves the shared overlay without recreating labels or resolving their sources.
5678
+ */
5679
+ function SetContainer(viewer, container) {
5680
+ const layer = layers.get(viewer);
5681
+ if (container && (container === (layer === null || layer === void 0 ? void 0 : layer.element) || (layer === null || layer === void 0 ? void 0 : layer.element.contains(container)))) {
5682
+ throw new Error("Label container cannot be inside the label overlay");
5683
+ }
5684
+ if (container) {
5685
+ containers.set(viewer, container);
5686
+ }
5687
+ else
5688
+ containers.delete(viewer);
5689
+ layer === null || layer === void 0 ? void 0 : layer.Move();
5690
+ }
5691
+ EntityLabel.SetContainer = SetContainer;
5692
+ /*
5693
+ * Owns one scene callback per viewer and separates projection/measurement from DOM writes.
5694
+ */
5695
+ class Layer {
5696
+ constructor(viewer) {
5697
+ this.viewer = viewer;
5698
+ this.element = document.createElement("div");
5699
+ this.coordinates = { x: 0, y: 0, scaleX: 1, scaleY: 1 };
5700
+ this.participants = new Set();
5701
+ this.placementMemory = new PlacementMemory();
5702
+ this.forwardingWheel = false;
5703
+ this.pendingDrag = null;
5704
+ this.TrackPointer = (event) => {
5705
+ var _a, _b;
5706
+ if (event.pointerType && event.pointerType !== "mouse" && event.pointerType !== "pen")
5707
+ return;
5708
+ const rect = (_b = (_a = this.viewer.canvas).getBoundingClientRect) === null || _b === void 0 ? void 0 : _b.call(_a);
5709
+ if (!rect || event.clientX < rect.left || event.clientX >= rect.right || event.clientY < rect.top || event.clientY >= rect.bottom) {
5710
+ if (this.pointerClient)
5711
+ this.ClearPointer();
5712
+ return;
5713
+ }
5714
+ this.pointerClient = { x: event.clientX, y: event.clientY };
5715
+ this.viewer.scene.requestRender();
5716
+ };
5717
+ this.PointerOut = (event) => { if (!event.relatedTarget)
5718
+ this.ClearPointer(); };
5719
+ this.ClearPointer = () => {
5720
+ this.pointerClient = undefined;
5721
+ this.pointer = undefined;
5722
+ if (!this.viewer.isDestroyed()) {
5723
+ this.viewer.scene.requestRender();
5724
+ }
5725
+ };
5726
+ this.PreventNativeDrag = (event) => {
5727
+ event.preventDefault();
5728
+ };
5729
+ this.BeginDrag = (event) => {
5730
+ var _a, _b, _c;
5731
+ this.EndPendingDrag();
5732
+ const detection = Cesium.FeatureDetection;
5733
+ 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"]');
5734
+ const canvas = this.viewer.canvas;
5735
+ if (event.defaultPrevented || event.button !== 0 || event.pointerType !== "mouse" ||
5736
+ ((_c = detection.supportsPointerEvents) === null || _c === void 0 ? void 0 : _c.call(detection)) === false || !canvas.setPointerCapture || this.viewer.isDestroyed() ||
5737
+ !target || !this.element.contains(target) ||
5738
+ this.element.ownerDocument.defaultView.getComputedStyle(target).visibility !== "visible") {
5739
+ return;
5740
+ }
5741
+ const bounds = canvas.getBoundingClientRect();
5742
+ if (event.clientX < bounds.left || event.clientX >= bounds.right || event.clientY < bounds.top || event.clientY >= bounds.bottom) {
5743
+ return;
5744
+ }
5745
+ this.pendingDrag = event;
5746
+ const doc = this.element.ownerDocument;
5747
+ doc.addEventListener("pointermove", this.ContinueDrag, true);
5748
+ doc.addEventListener("pointerup", this.FinishPendingDrag, true);
5749
+ doc.addEventListener("pointercancel", this.FinishPendingDrag, true);
5750
+ doc.defaultView.addEventListener("blur", this.EndPendingDrag);
5751
+ };
5752
+ this.FinishPendingDrag = (event) => {
5753
+ var _a;
5754
+ if (event.pointerId === ((_a = this.pendingDrag) === null || _a === void 0 ? void 0 : _a.pointerId)) {
5755
+ this.EndPendingDrag();
5756
+ }
5757
+ };
5758
+ this.EndPendingDrag = () => {
5759
+ this.pendingDrag = null;
5760
+ const doc = this.element.ownerDocument;
5761
+ doc.removeEventListener("pointermove", this.ContinueDrag, true);
5762
+ doc.removeEventListener("pointerup", this.FinishPendingDrag, true);
5763
+ doc.removeEventListener("pointercancel", this.FinishPendingDrag, true);
5764
+ doc.defaultView.removeEventListener("blur", this.EndPendingDrag);
5765
+ };
5766
+ this.ContinueDrag = (event) => {
5767
+ const start = this.pendingDrag;
5768
+ if (!start || event.pointerId !== start.pointerId) {
5769
+ return;
5770
+ }
5771
+ if (!(event.buttons & 1) || this.viewer.isDestroyed() || !this.element.contains(start.target)) {
5772
+ this.EndPendingDrag();
5773
+ return;
5774
+ }
5775
+ // Keep ordinary clicks local; only a deliberate drag starts Cesium's camera gesture.
5776
+ if (Math.hypot(event.clientX - start.clientX, event.clientY - start.clientY) < 6) {
5777
+ return;
5778
+ }
5779
+ this.EndPendingDrag();
5780
+ const canvas = this.viewer.canvas;
5781
+ try {
5782
+ canvas.setPointerCapture(start.pointerId);
5783
+ }
5784
+ catch {
5785
+ // A cancelled pointer or unsupported capture still cannot start native image dragging.
5786
+ return;
5787
+ }
5788
+ const win = this.element.ownerDocument.defaultView;
5789
+ const copy = (source) => ({
5790
+ bubbles: true, cancelable: true, pointerId: source.pointerId, pointerType: source.pointerType,
5791
+ isPrimary: source.isPrimary, button: source.button, buttons: source.buttons,
5792
+ clientX: source.clientX, clientY: source.clientY, screenX: source.screenX, screenY: source.screenY,
5793
+ ctrlKey: source.ctrlKey, shiftKey: source.shiftKey, altKey: source.altKey, metaKey: source.metaKey
5794
+ });
5795
+ // Capture routes subsequent real move/up events to Cesium, including releases outside the label.
5796
+ canvas.dispatchEvent(new win.PointerEvent("pointerdown", copy(start)));
5797
+ canvas.dispatchEvent(new win.PointerEvent("pointermove", copy(event)));
5798
+ event.preventDefault();
5799
+ event.stopPropagation();
5800
+ this.viewer.scene.requestRender();
5801
+ };
5802
+ this.ForwardWheel = (event) => {
5803
+ var _a, _b, _c;
5804
+ if (this.forwardingWheel || event.defaultPrevented || this.viewer.isDestroyed()) {
5805
+ return;
5806
+ }
5807
+ 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"]');
5808
+ const canvas = this.viewer.canvas;
5809
+ const win = this.element.ownerDocument.defaultView;
5810
+ if (!target || !this.element.contains(target) || win.getComputedStyle(target).visibility !== "visible") {
5811
+ return;
5812
+ }
5813
+ const bounds = canvas.getBoundingClientRect();
5814
+ if (event.clientX < bounds.left || event.clientX >= bounds.right || event.clientY < bounds.top || event.clientY >= bounds.bottom) {
5815
+ return;
5816
+ }
5817
+ const position = {
5818
+ bubbles: true, cancelable: true, clientX: event.clientX, clientY: event.clientY,
5819
+ screenX: event.screenX, screenY: event.screenY, buttons: event.buttons,
5820
+ ctrlKey: event.ctrlKey, shiftKey: event.shiftKey, altKey: event.altKey, metaKey: event.metaKey,
5821
+ pointerId: 1, pointerType: "mouse", isPrimary: true
5822
+ };
5823
+ this.forwardingWheel = true;
5824
+ try {
5825
+ // Cesium's wheel zoom uses its last mouse position, not the wheel event's coordinates.
5826
+ const detection = Cesium.FeatureDetection;
5827
+ // Older Cesium versions opt Firefox out even when the browser exposes PointerEvent.
5828
+ canvas.dispatchEvent(((_c = detection.supportsPointerEvents) === null || _c === void 0 ? void 0 : _c.call(detection)) !== false && typeof win.PointerEvent === "function" ?
5829
+ new win.PointerEvent("pointermove", position) : new win.MouseEvent("mousemove", position));
5830
+ canvas.dispatchEvent(new win.WheelEvent("wheel", {
5831
+ ...position, deltaX: event.deltaX, deltaY: event.deltaY, deltaZ: event.deltaZ, deltaMode: event.deltaMode
5832
+ }));
5833
+ this.viewer.scene.requestRender();
5834
+ event.preventDefault();
5835
+ event.stopPropagation();
5836
+ }
5837
+ finally {
5838
+ this.forwardingWheel = false;
5839
+ }
5840
+ };
5841
+ this.element.className = "entity-label-layer";
5842
+ // Depth ordering belongs inside the map overlay, never in the application's UI stack.
5843
+ this.element.style.cssText = "position:absolute;inset:0;z-index:0;isolation:isolate;pointer-events:none;";
5844
+ this.Move();
5845
+ this.element.addEventListener("wheel", this.ForwardWheel, { passive: false });
5846
+ this.element.addEventListener("pointerdown", this.BeginDrag, true);
5847
+ this.element.addEventListener("dragstart", this.PreventNativeDrag);
5848
+ this.element.ownerDocument.addEventListener("pointermove", this.TrackPointer, true);
5849
+ this.element.ownerDocument.addEventListener("pointerout", this.PointerOut, true);
5850
+ this.element.ownerDocument.defaultView.addEventListener("blur", this.ClearPointer);
5851
+ this.remove = viewer.scene.postUpdate.addEventListener(() => this.Update());
5852
+ }
5853
+ Move() {
5854
+ (containers.get(this.viewer) || this.viewer.container).appendChild(this.element);
5855
+ this.Update();
5856
+ this.viewer.scene.requestRender();
5857
+ }
5858
+ MeasureCoordinates() {
5859
+ var _a;
5860
+ const canvas = this.viewer.canvas;
5861
+ const canvasRect = (_a = canvas.getBoundingClientRect) === null || _a === void 0 ? void 0 : _a.call(canvas);
5862
+ const rect = this.element.getBoundingClientRect();
5863
+ const scaleX = rect.width / this.element.offsetWidth || 1;
5864
+ const scaleY = rect.height / this.element.offsetHeight || 1;
5865
+ return {
5866
+ x: (((canvasRect === null || canvasRect === void 0 ? void 0 : canvasRect.left) || 0) - rect.left) / scaleX,
5867
+ y: (((canvasRect === null || canvasRect === void 0 ? void 0 : canvasRect.top) || 0) - rect.top) / scaleY,
5868
+ scaleX: ((canvasRect === null || canvasRect === void 0 ? void 0 : canvasRect.width) / canvas.clientWidth || 1) / scaleX,
5869
+ scaleY: ((canvasRect === null || canvasRect === void 0 ? void 0 : canvasRect.height) / canvas.clientHeight || 1) / scaleY
5870
+ };
5871
+ }
5872
+ Add(participant) {
5873
+ this.participants.add(participant);
5874
+ this.viewer.scene.requestRender();
5875
+ return () => {
5876
+ this.participants.delete(participant);
5877
+ if (!this.participants.size) {
5878
+ this.remove();
5879
+ this.element.removeEventListener("wheel", this.ForwardWheel);
5880
+ this.element.removeEventListener("pointerdown", this.BeginDrag, true);
5881
+ this.element.removeEventListener("dragstart", this.PreventNativeDrag);
5882
+ this.element.ownerDocument.removeEventListener("pointermove", this.TrackPointer, true);
5883
+ this.element.ownerDocument.removeEventListener("pointerout", this.PointerOut, true);
5884
+ this.element.ownerDocument.defaultView.removeEventListener("blur", this.ClearPointer);
5885
+ this.EndPendingDrag();
5886
+ this.element.remove();
5887
+ layers.delete(this.viewer);
5888
+ }
5889
+ };
5890
+ }
5891
+ Update() {
5892
+ var _a, _b;
5893
+ this.coordinates = this.MeasureCoordinates();
5894
+ const rect = (_b = (_a = this.viewer.canvas).getBoundingClientRect) === null || _b === void 0 ? void 0 : _b.call(_a);
5895
+ const pointer = this.pointerClient;
5896
+ 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 ?
5897
+ {
5898
+ x: (pointer.x - rect.left) * this.viewer.canvas.clientWidth / rect.width,
5899
+ y: (pointer.y - rect.top) * this.viewer.canvas.clientHeight / rect.height
5900
+ } : undefined;
5901
+ const projected = new Map();
5902
+ for (const participant of this.participants) {
5903
+ projected.set(participant, participant.project());
5904
+ }
5905
+ const placements = new Map(Layout(Array.from(projected.values()).filter(Boolean), this.placementMemory).map(item => [item.frame.id, item]));
5906
+ for (const [participant, frame] of projected) {
5907
+ participant.apply(frame ? placements.get(frame.id) : undefined);
5908
+ }
5909
+ }
5910
+ }
5911
+ let nextLabelId = 0;
5307
5912
  class Label {
5913
+ get HasText() { return this.options.presentation !== "icon"; }
5914
+ SetHoverVisible(visible) {
5915
+ var _a, _b, _c;
5916
+ if (!this.Transient || this.Persistent) {
5917
+ return;
5918
+ }
5919
+ if (!visible) {
5920
+ 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);
5921
+ // Retiring labels must not change the ring now owned by the next hover target.
5922
+ if (this.ringAnimation) {
5923
+ this.ringAnimation = { ...this.ringAnimation };
5924
+ }
5925
+ }
5926
+ if (this.hoverFade.to === Number(visible)) {
5927
+ return;
5928
+ }
5929
+ this.hoverFade = { from: this.HoverOpacity(), to: Number(visible), start: Date.now() };
5930
+ this.viewer.scene.requestRender();
5931
+ }
5932
+ HoverOpacity() {
5933
+ if (!this.Transient || this.Persistent) {
5934
+ return 1;
5935
+ }
5936
+ const progress = Math.min(1, Math.max(0, (Date.now() - this.hoverFade.start) / 120));
5937
+ if (progress < 1) {
5938
+ this.viewer.scene.requestRender();
5939
+ }
5940
+ return this.hoverFade.from + (this.hoverFade.to - this.hoverFade.from) * progress * progress * (3 - 2 * progress);
5941
+ }
5308
5942
  get Rego() {
5309
5943
  return this.rego;
5310
5944
  }
@@ -5317,10 +5951,18 @@
5317
5951
  set pos3d(val) {
5318
5952
  this._pos3d = val;
5319
5953
  this.prePositioned = !isNaN(val === null || val === void 0 ? void 0 : val.x);
5320
- this.createLabel(val);
5954
+ if (this.label && this.prePositioned)
5955
+ this.viewer.scene.requestRender();
5956
+ else
5957
+ this.createLabel(val);
5321
5958
  }
5322
5959
  constructor(params) {
5323
- var _a;
5960
+ var _a, _b, _c, _d;
5961
+ this.id = `label-${++nextLabelId}`;
5962
+ this.Persistent = false;
5963
+ this.Transient = false;
5964
+ this.hoverFade = { from: 0, to: 1, start: Date.now() };
5965
+ this.cursorMotion = { amount: 0, from: 0, target: 0, start: 0, position: undefined };
5324
5966
  // Counter to help differentiate between createLabel calls.
5325
5967
  this.counter = 0;
5326
5968
  // The label element itself.
@@ -5340,15 +5982,24 @@
5340
5982
  this.loadTimeout = null;
5341
5983
  this.loadedName = false;
5342
5984
  this.line = null;
5985
+ this.anchor = null;
5343
5986
  // Null = not attempted to load.
5344
5987
  this.additionalTexts = null;
5345
5988
  this.rego = params.rego;
5346
5989
  this.viewer = params.viewer;
5347
5990
  this.api = params.api;
5348
5991
  this.visualRegister = params.visualRegister;
5349
- this.prePositioned = !isNaN((_a = params.pos3d) === null || _a === void 0 ? void 0 : _a.x);
5992
+ this.options = params.options || {};
5993
+ this.groupId = params.groupId;
5994
+ this.Persistent = (_a = params.persistent) !== null && _a !== void 0 ? _a : Boolean(params.groupId);
5995
+ this.Transient = (_b = params.transient) !== null && _b !== void 0 ? _b : false;
5996
+ this.hoverRingState = params.hoverRingState;
5997
+ this.visible = params.visible;
5998
+ if (((_c = params.options) === null || _c === void 0 ? void 0 : _c.text) != null)
5999
+ this.additionalTexts = [];
6000
+ this.prePositioned = !isNaN((_d = params.pos3d) === null || _d === void 0 ? void 0 : _d.x);
5350
6001
  this.createLabel(params.pos3d);
5351
- if (!params.api && !BModels.ENVIRONMENT.IS_SELF_MANAGED) {
6002
+ if (!params.api && !BModels.ENVIRONMENT.IS_SELF_MANAGED && this.options.text == null) {
5352
6003
  this.api = BModels.ENVIRONMENT.Api().GetBruceApi();
5353
6004
  }
5354
6005
  }
@@ -5398,12 +6049,24 @@
5398
6049
  clearTimeout(this.loadTimeout);
5399
6050
  this.loadTimeout = null;
5400
6051
  }
6052
+ /*
6053
+ * Rebuilds only when configuration changes; state refreshes must not recreate DOM every frame.
6054
+ */
6055
+ Configure(options, groupId) {
6056
+ if (this.groupId === groupId && JSON.stringify(this.options) === JSON.stringify(options || {})) {
6057
+ return;
6058
+ }
6059
+ this.options = options || {};
6060
+ this.groupId = groupId;
6061
+ this.additionalTexts = this.options.text != null ? [] : null;
6062
+ this.createLabel();
6063
+ }
5401
6064
  /**
5402
6065
  * Creates cesium entity for the label if one does not exist.
5403
6066
  * @param starterPos3d optional position to create label at. Ignored for billboard labels.
5404
6067
  */
5405
6068
  async createLabel(starterPos3d) {
5406
- var _a, _b, _c, _d, _e, _f, _g, _h;
6069
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
5407
6070
  this.removeLabel();
5408
6071
  let pos3d = starterPos3d ? starterPos3d.clone() : null;
5409
6072
  const counter = ++this.counter;
@@ -5612,7 +6275,7 @@
5612
6275
  }
5613
6276
  clearTimeout(this.loadTimeout);
5614
6277
  this.loadTimeout = setTimeout(async () => {
5615
- var _a, _b, _c, _d;
6278
+ var _a, _b, _c, _d, _e;
5616
6279
  if (this.disposed || this.counter != counter) {
5617
6280
  return;
5618
6281
  }
@@ -5620,10 +6283,10 @@
5620
6283
  if (this.additionalTexts == null) {
5621
6284
  const additionalTexts = [];
5622
6285
  // Rego might not have a known or default Entity Type. Eg: OSM building.
5623
- if (this.rego.entityTypeId) {
6286
+ if (this.rego.entityTypeId || this.options.attributes) {
5624
6287
  let entityType = null;
5625
6288
  try {
5626
- const data = await BModels.EntityType.Get({
6289
+ const data = this.options.attributes ? null : await BModels.EntityType.Get({
5627
6290
  api: this.api,
5628
6291
  entityTypeId: this.rego.entityTypeId
5629
6292
  });
@@ -5638,14 +6301,14 @@
5638
6301
  }
5639
6302
  // Type could be deleted, however we'll just catch that to show a label instead of nothing.
5640
6303
  // This is not unusual for Tilesets where the Type that is baked might be outdated.
5641
- if (entityType) {
5642
- const attributes = (_b = (_a = entityType.VisualSettings) === null || _a === void 0 ? void 0 : _a.label) === null || _b === void 0 ? void 0 : _b.attributes;
6304
+ if (entityType || this.options.attributes) {
6305
+ 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;
5643
6306
  if (attributes) {
5644
6307
  try {
5645
6308
  const { entity } = await BModels.Entity.Get({
5646
6309
  api: this.api,
5647
6310
  entityId: this.rego.entityId,
5648
- 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,
6311
+ 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,
5649
6312
  schemaId: this.rego.schema,
5650
6313
  migrated: true
5651
6314
  });
@@ -5654,7 +6317,7 @@
5654
6317
  entity: entity,
5655
6318
  path: attributes[i]
5656
6319
  });
5657
- if (value) {
6320
+ if (value != null) {
5658
6321
  additionalTexts.push(value);
5659
6322
  }
5660
6323
  }
@@ -5683,12 +6346,166 @@
5683
6346
  if (this.disposed || this.counter != counter) {
5684
6347
  return;
5685
6348
  }
5686
- const labelContent = `
5687
- <div>
5688
- <div style="margin-bottom:4px">${text}</div>
5689
- ${additionalTexts.map(attr => `<div>${attr}</div>`).join('')}
5690
- </div>
5691
- `;
6349
+ const presentation = this.options.presentation || "text";
6350
+ let clickEntityIds = [];
6351
+ const selectOnClick = (_j = this.options.selectOnClick) !== null && _j !== void 0 ? _j : Boolean(this.groupId);
6352
+ const select = (event) => {
6353
+ event.preventDefault();
6354
+ event.stopPropagation();
6355
+ if (!selectOnClick || this.disposed || this.counter !== counter || !clickEntityIds.length || !this.visualRegister) {
6356
+ return;
6357
+ }
6358
+ this.visualRegister.ClearSelected({ requestRender: false });
6359
+ this.visualRegister.SetSelected({ entityIds: clickEntityIds, selected: true });
6360
+ };
6361
+ const makeClickable = (target) => {
6362
+ if (!selectOnClick || !this.visualRegister) {
6363
+ return;
6364
+ }
6365
+ target.style.pointerEvents = "auto";
6366
+ target.style.cursor = "pointer";
6367
+ target.style.userSelect = "none";
6368
+ target.setAttribute("role", "button");
6369
+ target.tabIndex = 0;
6370
+ target.addEventListener("click", select);
6371
+ target.addEventListener("keydown", event => {
6372
+ if (event.key === "Enter" || event.key === " ")
6373
+ select(event);
6374
+ });
6375
+ // Prevent viewer picking and camera gestures from interpreting label interactions.
6376
+ for (const type of ["pointerdown", "pointerup", "mousedown", "mouseup", "dblclick"]) {
6377
+ target.addEventListener(type, event => event.stopPropagation());
6378
+ }
6379
+ };
6380
+ const normalLeader = (_k = this.options.leader) !== null && _k !== void 0 ? _k : presentation === "text";
6381
+ let leader = normalLeader;
6382
+ const content = document.createElement("span");
6383
+ const originalText = (_l = this.options.text) !== null && _l !== void 0 ? _l : [text, ...additionalTexts].join("\n");
6384
+ let textElement;
6385
+ let sourceText;
6386
+ let sourceNaturalText;
6387
+ let iconElement;
6388
+ let iconContainer;
6389
+ content.className = "entity-label-content";
6390
+ content.style.cssText = "display:flex;align-items:center;gap:6px;";
6391
+ if (presentation === "icon-text-above") {
6392
+ content.style.flexDirection = "column-reverse";
6393
+ }
6394
+ if (presentation !== "text") {
6395
+ const icon = document.createElement("span");
6396
+ iconElement = icon;
6397
+ icon.className = "entity-label-icon";
6398
+ icon.style.cssText = `font-size:${(this.options.iconSize || 24) * ((_m = this.options.iconScale) !== null && _m !== void 0 ? _m : 1)}px;line-height:1;`;
6399
+ icon.style.textShadow = "0 1px 2px rgba(0, 0, 0, 0.25)";
6400
+ const iconColor = typeof this.options.iconColor === "string" ? this.options.iconColor : undefined;
6401
+ const iconBackground = this.options.iconBackground && this.options.iconScale !== 0;
6402
+ if (iconColor) {
6403
+ icon.style.color = iconColor;
6404
+ }
6405
+ if (this.options.iconUrl) {
6406
+ const image = document.createElement("img");
6407
+ image.draggable = false;
6408
+ image.alt = this.options.icon || "";
6409
+ const scale = (_o = this.options.iconScale) !== null && _o !== void 0 ? _o : 1;
6410
+ const size = this.options.iconSize;
6411
+ const mask = iconColor && iconBackground ? document.createElement("span") : icon;
6412
+ if (mask !== icon) {
6413
+ icon.appendChild(mask);
6414
+ }
6415
+ const target = iconColor ? mask : image;
6416
+ if (iconColor) {
6417
+ mask.style.display = "inline-block";
6418
+ mask.style.backgroundColor = iconColor;
6419
+ for (const prefix of ["", "-webkit-"]) {
6420
+ mask.style.setProperty(`${prefix}mask-image`, `url(${JSON.stringify(this.options.iconUrl)})`);
6421
+ mask.style.setProperty(`${prefix}mask-size`, "contain");
6422
+ mask.style.setProperty(`${prefix}mask-repeat`, "no-repeat");
6423
+ mask.style.setProperty(`${prefix}mask-position`, "center");
6424
+ }
6425
+ }
6426
+ const resize = () => {
6427
+ if (this.disposed || this.counter !== counter) {
6428
+ return;
6429
+ }
6430
+ const intrinsic = this.options.iconScale != null && size == null;
6431
+ target.style.width = `${(intrinsic ? image.naturalWidth || 24 : size || 24) * scale}px`;
6432
+ target.style.height = `${(intrinsic ? image.naturalHeight || 24 : size || 24) * scale}px`;
6433
+ this.viewer.scene.requestRender();
6434
+ };
6435
+ image.onload = resize;
6436
+ resize();
6437
+ image.src = this.options.iconUrl;
6438
+ if (!iconColor) {
6439
+ icon.appendChild(image);
6440
+ }
6441
+ }
6442
+ else if (typeof this.options.iconClass === "string" && this.options.iconClass) {
6443
+ icon.className = `entity-label-icon ${this.options.iconClass}`;
6444
+ }
6445
+ else {
6446
+ icon.textContent = this.options.icon || "โ—";
6447
+ }
6448
+ if (iconBackground) {
6449
+ const color = Cesium.Color.fromCssColorString(iconColor || "white") || Cesium.Color.WHITE;
6450
+ const luminance = 0.2126 * color.red + 0.7152 * color.green + 0.0722 * color.blue;
6451
+ const background = (luminance > 0.5 ? color.darken(ICON_BACKGROUND_BLEND, new Cesium.Color()) :
6452
+ color.brighten(ICON_BACKGROUND_BLEND, new Cesium.Color()));
6453
+ const size = (this.options.iconSize || 24) * ((_p = this.options.iconScale) !== null && _p !== void 0 ? _p : 1);
6454
+ icon.style.backgroundColor = background.withAlpha(ICON_BACKGROUND_ALPHA).toCssColorString();
6455
+ icon.style.borderRadius = "50%";
6456
+ icon.style.padding = `${size / 3}px`;
6457
+ icon.style.display = "inline-flex";
6458
+ icon.style.alignItems = "center";
6459
+ icon.style.justifyContent = "center";
6460
+ icon.style.minWidth = `${size}px`;
6461
+ icon.style.minHeight = `${size}px`;
6462
+ icon.style.boxSizing = "content-box";
6463
+ }
6464
+ if (this.options.iconScale === 0) {
6465
+ icon.style.display = "none";
6466
+ }
6467
+ makeClickable(icon);
6468
+ // Keep badges outside the glyph so font classes and image masks cannot affect them.
6469
+ iconContainer = document.createElement("span");
6470
+ iconContainer.style.cssText = "position:relative;display:inline-flex;flex:none;";
6471
+ iconContainer.appendChild(icon);
6472
+ content.appendChild(iconContainer);
6473
+ }
6474
+ if (presentation !== "icon") {
6475
+ textElement = document.createElement("span");
6476
+ textElement.className = "entity-label-text";
6477
+ const textStyle = `white-space:pre-wrap;overflow-wrap:anywhere;max-width:${TEXT_MAX_WIDTH}px;width:max-content;flex-shrink:0;`;
6478
+ textElement.style.cssText = textStyle;
6479
+ textElement.textContent = originalText;
6480
+ if (presentation === "text")
6481
+ makeClickable(textElement);
6482
+ content.appendChild(textElement);
6483
+ if ((_q = this.options.cluster) === null || _q === void 0 ? void 0 : _q.enabled) {
6484
+ sourceText = document.createElement("span");
6485
+ sourceText.className = "entity-label-source-text";
6486
+ sourceText.setAttribute("aria-hidden", "true");
6487
+ sourceText.style.cssText = textStyle + "position:absolute;visibility:hidden;pointer-events:none;";
6488
+ sourceText.textContent = originalText;
6489
+ content.appendChild(sourceText);
6490
+ // Measure the original unwrapped text, never a rendered cluster summary.
6491
+ sourceNaturalText = document.createElement("span");
6492
+ sourceNaturalText.setAttribute("aria-hidden", "true");
6493
+ sourceNaturalText.style.cssText = "position:absolute;visibility:hidden;pointer-events:none;white-space:pre;width:max-content;";
6494
+ sourceNaturalText.textContent = originalText;
6495
+ content.appendChild(sourceNaturalText);
6496
+ }
6497
+ }
6498
+ const countElement = document.createElement("span");
6499
+ countElement.className = "entity-label-count";
6500
+ 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;";
6501
+ if (iconContainer) {
6502
+ countElement.style.cssText += "position:absolute;left:100%;top:0;transform:translate(-50%,-50%);";
6503
+ iconContainer.appendChild(countElement);
6504
+ }
6505
+ if (!layers.has(this.viewer)) {
6506
+ layers.set(this.viewer, new Layer(this.viewer));
6507
+ }
6508
+ const layer = layers.get(this.viewer);
5692
6509
  let ele = null;
5693
6510
  let lineEle = null;
5694
6511
  if (this.label) {
@@ -5696,11 +6513,22 @@
5696
6513
  }
5697
6514
  else {
5698
6515
  ele = document.createElement("div");
5699
- ele.innerHTML = labelContent;
6516
+ ele.appendChild(content);
6517
+ if (!iconContainer) {
6518
+ ele.appendChild(countElement);
6519
+ }
6520
+ ele.className = "entity-label";
6521
+ ele.dataset.entityId = ((_r = this.rego) === null || _r === void 0 ? void 0 : _r.entityId) || "";
6522
+ ele.dataset.presentation = presentation;
6523
+ if (this.groupId) {
6524
+ ele.dataset.groupId = this.groupId;
6525
+ }
5700
6526
  ele.setAttribute("style", `
5701
6527
  position: absolute;
5702
6528
  z-index: 0;
5703
6529
  display: none;
6530
+ align-items: center;
6531
+ gap: 6px;
5704
6532
  pointer-events: none;
5705
6533
  padding: 6px 8px;
5706
6534
  border-radius: 6px;
@@ -5709,9 +6537,10 @@
5709
6537
  border-radius: 9px;
5710
6538
  color: #ffffff;
5711
6539
  text-shadow: 0 0 5px rgba(0, 0, 0, 0.8);
5712
- }`);
6540
+ `);
5713
6541
  this.label = ele;
5714
- this.viewer.container.appendChild(ele);
6542
+ ele.style.width = "max-content";
6543
+ layer.element.appendChild(ele);
5715
6544
  }
5716
6545
  if (this.line) {
5717
6546
  lineEle = this.line;
@@ -5728,7 +6557,10 @@
5728
6557
  box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
5729
6558
  `);
5730
6559
  this.line = lineEle;
5731
- // We'll draw a ball at the bottom of the line to make it look like it's connecting to the entity.
6560
+ lineEle.className = "entity-label-leader";
6561
+ if (this.options.leaderColor)
6562
+ lineEle.style.background = this.options.leaderColor;
6563
+ // A separate anchor keeps the stem out of the hollow centre when proximity opens the ring.
5732
6564
  const lineEleNode = document.createElement("div");
5733
6565
  lineEleNode.setAttribute("style", `
5734
6566
  position: absolute;
@@ -5741,35 +6573,20 @@
5741
6573
  top: -2px;
5742
6574
  left: -2px;
5743
6575
  `);
5744
- lineEle.appendChild(lineEleNode);
5745
- this.viewer.container.appendChild(lineEle);
6576
+ lineEleNode.className = "entity-label-anchor";
6577
+ // SVG preserves fractional strokes in scaled containers where CSS borders round to pixels.
6578
+ const ringSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
6579
+ ringSvg.style.cssText = "position:absolute;inset:0;width:100%;height:100%;overflow:visible;";
6580
+ ringSvg.setAttribute("aria-hidden", "true");
6581
+ ringSvg.setAttribute("preserveAspectRatio", "none");
6582
+ const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
6583
+ circle.setAttribute("fill", "none");
6584
+ ringSvg.appendChild(circle);
6585
+ lineEleNode.appendChild(ringSvg);
6586
+ this.anchor = lineEleNode;
6587
+ layer.element.appendChild(lineEleNode);
6588
+ layer.element.appendChild(lineEle);
5746
6589
  }
5747
- let _lastDistance = null;
5748
- let _lastCameraPos = null;
5749
- const getDistance = () => {
5750
- var _a;
5751
- if (this.disposed) {
5752
- return null;
5753
- }
5754
- const cameraPos = (_a = this.viewer.camera) === null || _a === void 0 ? void 0 : _a.position;
5755
- if (isNaN(cameraPos === null || cameraPos === void 0 ? void 0 : cameraPos.x)) {
5756
- return null;
5757
- }
5758
- // If camera hasn't moved much then we can use the last distance.
5759
- const TOLERANCE = 0.1;
5760
- if (_lastCameraPos) {
5761
- const totalMovement = Math.abs(_lastCameraPos.x - cameraPos.x) +
5762
- Math.abs(_lastCameraPos.y - cameraPos.y) +
5763
- Math.abs(_lastCameraPos.z - cameraPos.z);
5764
- if (totalMovement < TOLERANCE) {
5765
- return _lastDistance;
5766
- }
5767
- }
5768
- const distance = Cesium.Cartesian3.distance(cameraPos, this._pos3d);
5769
- _lastDistance = distance;
5770
- _lastCameraPos = cameraPos.clone();
5771
- return distance;
5772
- };
5773
6590
  let MAX_DISTANCE = Math.max(15000, sizeInM * 35);
5774
6591
  if (visual && visual instanceof Cesium.Entity) {
5775
6592
  // Consistent sized things.
@@ -5777,123 +6594,399 @@
5777
6594
  MAX_DISTANCE = Infinity;
5778
6595
  }
5779
6596
  }
5780
- const updateLabel = () => {
5781
- var _a;
5782
- if (this.disposed || isNaN((_a = this._pos3d) === null || _a === void 0 ? void 0 : _a.x)) {
5783
- return;
6597
+ const fitLeader = (anchorX, anchorY, desired, width, height, radius = 0) => {
6598
+ const above = Math.max(0, anchorY - height);
6599
+ const below = Math.max(0, this.viewer.canvas.clientHeight - anchorY - height);
6600
+ // Keep a short visible stem before switching sides; never aim it at the cursor or a box corner.
6601
+ const upwards = above >= Math.max(radius ? radius + 4 : 0, Math.min(8, desired)) || above >= below;
6602
+ const length = Math.min(desired, upwards ? above : below);
6603
+ const endY = anchorY + (upwards ? -length : length);
6604
+ return {
6605
+ x: Math.max(0, Math.min(anchorX, this.viewer.canvas.clientWidth - width)),
6606
+ y: upwards ? endY - height : endY, endY
6607
+ };
6608
+ };
6609
+ let cursorMotion = this.cursorMotion;
6610
+ const project = () => {
6611
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
6612
+ if (this.disposed || !this.viewer || this.viewer.isDestroyed() || ((_a = this.visible) === null || _a === void 0 ? void 0 : _a.call(this)) === false ||
6613
+ (presentation === "icon" && this.options.iconScale === 0)) {
6614
+ return null;
5784
6615
  }
5785
- if (!this.viewer || this.viewer.isDestroyed()) {
5786
- this.Dispose();
5787
- return;
6616
+ if (this.Transient && !this.Persistent && this.hoverFade.to === 0 &&
6617
+ (this.hoverFade.from === 0 || Date.now() - this.hoverFade.start >= 120)) {
6618
+ return null;
5788
6619
  }
5789
- const distance = getDistance();
5790
- if (isNaN(distance) || (!this.prePositioned && distance >= MAX_DISTANCE)) {
5791
- ele.style.display = "none";
5792
- lineEle.style.display = "none";
5793
- return;
6620
+ // Moving point/model positions must be sampled at the current clock time.
6621
+ if (!this.prePositioned && visual instanceof Cesium.Entity && visual.position) {
6622
+ const position = getValue(this.viewer, visual.position);
6623
+ if (position)
6624
+ this._pos3d = exports.DrawingUtils.EnsurePosHeight({
6625
+ desiredHeightRef: Cesium.HeightReference.NONE, heightRef, pos3d: position, viewer: this.viewer
6626
+ });
5794
6627
  }
5795
- const CESIUM = Cesium;
5796
- const _3dTo2D = Cesium.SceneTransforms.hasOwnProperty("wgs84ToWindowCoordinates") ? CESIUM.SceneTransforms.wgs84ToWindowCoordinates : CESIUM.SceneTransforms.worldToWindowCoordinates;
5797
- const lineStartPos2d = _3dTo2D(this.viewer.scene, this._pos3d);
5798
- // Invalid point.
5799
- if (isNaN(lineStartPos2d === null || lineStartPos2d === void 0 ? void 0 : lineStartPos2d.x)) {
5800
- ele.style.transform = "";
5801
- ele.style.opacity = "0";
5802
- lineEle.style.display = "none";
5803
- return;
6628
+ if (!Number.isFinite((_b = this._pos3d) === null || _b === void 0 ? void 0 : _b.x)) {
6629
+ return null;
6630
+ }
6631
+ const camera = this.viewer.camera;
6632
+ const cameraPosition = camera.positionWC || camera.position;
6633
+ const relative = Cesium.Cartesian3.subtract(this._pos3d, cameraPosition, new Cesium.Cartesian3());
6634
+ const depth = Cesium.Cartesian3.dot(relative, camera.directionWC || camera.direction);
6635
+ const distance = Cesium.Cartesian3.magnitude(relative);
6636
+ if (!Number.isFinite(distance) || depth <= 0 || distance < ((_c = this.options.near) !== null && _c !== void 0 ? _c : 0) ||
6637
+ distance > ((_d = this.options.far) !== null && _d !== void 0 ? _d : Infinity) || (!this.prePositioned && distance >= MAX_DISTANCE)) {
6638
+ return null;
5804
6639
  }
5805
- // Offset start by the pixel offset.
5806
- lineStartPos2d.x += pixelOffset.x;
5807
- lineStartPos2d.y += pixelOffset.y;
5808
- ele.style.display = "block";
5809
- lineEle.style.display = "block";
5810
- // Calculate opacity to apply based on distance between camera and pos3d.
5811
- // We'll make the label less transparent when further away.
5812
- const opacity = this.prePositioned || MAX_DISTANCE == Infinity ? 1 : 1 - (distance / MAX_DISTANCE);
5813
- if (opacity && opacity > 0.15) {
5814
- this.label.style.opacity = `${opacity}`;
5815
- // Calc where the line should end.
5816
- // We get the pos3d and add 'n' to it, then turn it back into 2d.
5817
- let lineEndPos3d = this._pos3d.clone();
5818
- const lineEndPoint = Cesium.Cartographic.fromCartesian(lineEndPos3d);
5819
- let addHeight = null;
5820
- // If we're pre-positioned that means the user is hovering over the entity.
5821
- // We will keep the height low to avoid looking goofy.
5822
- if (this.prePositioned) {
5823
- addHeight = distance / 24;
6640
+ const transforms = Cesium.SceneTransforms;
6641
+ const projectPosition = transforms.wgs84ToWindowCoordinates || transforms.worldToWindowCoordinates;
6642
+ const anchor = projectPosition(this.viewer.scene, this._pos3d);
6643
+ if (!Number.isFinite(anchor === null || anchor === void 0 ? void 0 : anchor.x) || !Number.isFinite(anchor === null || anchor === void 0 ? void 0 : anchor.y)) {
6644
+ return null;
6645
+ }
6646
+ if (leader && (anchor.x < 0 || anchor.y < 0 || anchor.x >= this.viewer.canvas.clientWidth || anchor.y >= this.viewer.canvas.clientHeight))
6647
+ return null;
6648
+ const transientPivot = (this.Transient && !this.Persistent ?
6649
+ (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);
6650
+ const hover = (presentation !== "icon" ?
6651
+ (this.Transient && !this.Persistent && this.hoverFade.to === 0 ? this.retainedHoverPosition :
6652
+ (_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);
6653
+ const reused = !this.Transient || this.Persistent;
6654
+ const now = Date.now();
6655
+ const animateCursor = () => {
6656
+ const progress = Math.min(1, Math.max(0, (now - cursorMotion.start) / 200));
6657
+ const eased = progress * progress * (3 - 2 * progress);
6658
+ cursorMotion.amount = cursorMotion.from + (cursorMotion.target - cursorMotion.from) * eased;
6659
+ return progress;
6660
+ };
6661
+ animateCursor();
6662
+ const cursorTarget = reused && hover ? 1 : 0;
6663
+ if (cursorMotion.target !== cursorTarget) {
6664
+ cursorMotion = { ...cursorMotion, from: cursorMotion.amount, target: cursorTarget, start: now };
6665
+ }
6666
+ if (hover) {
6667
+ cursorMotion.position = { x: hover.x, y: hover.y };
6668
+ }
6669
+ if (animateCursor() < 1 && cursorMotion.from !== cursorMotion.target) {
6670
+ this.viewer.scene.requestRender();
6671
+ }
6672
+ this.cursorMotion = cursorMotion;
6673
+ const cursorAmount = cursorMotion.amount;
6674
+ leader = normalLeader || cursorAmount > 0;
6675
+ let x = ((_l = transientPivot === null || transientPivot === void 0 ? void 0 : transientPivot.x) !== null && _l !== void 0 ? _l : anchor.x) + pixelOffset.x;
6676
+ let y = ((_m = transientPivot === null || transientPivot === void 0 ? void 0 : transientPivot.y) !== null && _m !== void 0 ? _m : anchor.y) + pixelOffset.y;
6677
+ if (cursorAmount > 0) {
6678
+ // A leader-less label starts centred on its anchor; preserve that box as it lifts away.
6679
+ if (!normalLeader) {
6680
+ x -= ele.offsetWidth / layer.coordinates.scaleX / 2;
6681
+ y += ele.offsetHeight / layer.coordinates.scaleY / 2;
6682
+ }
6683
+ x += (cursorMotion.position.x + pixelOffset.x - x) * cursorAmount;
6684
+ y += (cursorMotion.position.y + pixelOffset.y - y) * cursorAmount;
6685
+ }
6686
+ const anchorX = x;
6687
+ const anchorY = y;
6688
+ let leaderLength;
6689
+ if (leader) {
6690
+ if (!normalLeader) {
6691
+ leaderLength = 0;
6692
+ }
6693
+ else if (this.options.leaderLength != null) {
6694
+ leaderLength = Math.max(0, this.options.leaderLength);
6695
+ }
6696
+ else if (transientPivot) {
6697
+ // Cursor callouts must not jump when the next hover target has a different extent.
6698
+ leaderLength = 96;
5824
6699
  }
5825
6700
  else {
5826
- // If known then calculate based on size.
5827
- // If we're close then we'll favour the distance over the size.
5828
- if (sizeInM && sizeInM > 0) {
5829
- addHeight = Math.min(distance / 5, sizeInM * 1.2);
5830
- }
5831
- // If unknown then calculate based on camera height or distance.
5832
- // Radius is unknown for Tileset things. We could bake the radius in possibly as a feature property.
5833
- // We have to make it work for both labels against tall skyscrapers, and up-close assembly models.
5834
- else {
5835
- addHeight = Math.min(distance, this.viewer.camera.positionCartographic.height) / 7;
5836
- }
5837
- }
5838
- // The ratio is roughly 1m = 11px on the screen.
5839
- // So if we want to add additional labels then we'll need to adjust the height.
5840
- if (additionalTexts === null || additionalTexts === void 0 ? void 0 : additionalTexts.length) {
5841
- // Each additional text is about 16px tall.
5842
- // So we get the total px height and divide by 11 to get the meters.
5843
- addHeight += ((additionalTexts.length * 16) / 11);
5844
- }
5845
- // Safety check.
5846
- if (!addHeight || addHeight <= 0) {
5847
- addHeight = 0.001;
5848
- }
5849
- lineEndPoint.height += addHeight;
5850
- lineEndPos3d = Cesium.Cartographic.toCartesian(lineEndPoint);
5851
- const CESIUM = Cesium;
5852
- const _3dTo2D = Cesium.SceneTransforms.hasOwnProperty("wgs84ToWindowCoordinates") ? CESIUM.SceneTransforms.wgs84ToWindowCoordinates : CESIUM.SceneTransforms.worldToWindowCoordinates;
5853
- let lineEndPos2d = _3dTo2D(this.viewer.scene, lineEndPos3d);
5854
- // Invalid point.
5855
- if (isNaN(lineEndPos2d === null || lineEndPos2d === void 0 ? void 0 : lineEndPos2d.x)) {
5856
- ele.style.transform = "";
5857
- ele.style.opacity = "0";
5858
- lineEle.style.display = "none";
5859
- return;
6701
+ const point = Cesium.Cartographic.fromCartesian(this._pos3d);
6702
+ const height = (this.prePositioned ? distance / 24 : sizeInM > 0 ?
6703
+ Math.min(distance / 5, sizeInM * 1.2) : Math.min(distance, camera.positionCartographic.height) / 7);
6704
+ point.height += Math.max(0.001, height);
6705
+ const end = projectPosition(this.viewer.scene, Cesium.Cartographic.toCartesian(point));
6706
+ // A tall world-space leader can cross the camera plane while its anchor remains visible.
6707
+ 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;
6708
+ // Scale after projection so large areas and routes get shorter leaders at every zoom.
6709
+ if (visual instanceof Cesium.Entity && (visual.polygon || visual.polyline))
6710
+ leaderLength /= 3;
6711
+ }
6712
+ if (cursorAmount > 0) {
6713
+ leaderLength += (96 - leaderLength) * cursorAmount;
6714
+ }
6715
+ y = anchorY - leaderLength;
6716
+ }
6717
+ else if (hover && !reused) {
6718
+ x = hover.x + pixelOffset.x;
6719
+ y = hover.y + pixelOffset.y - 24;
6720
+ }
6721
+ // Hidden participants remain measurable; visibility never removes their layout boxes.
6722
+ const coordinates = layer.coordinates;
6723
+ const width = ele.offsetWidth / coordinates.scaleX;
6724
+ const height = ele.offsetHeight / coordinates.scaleY;
6725
+ let clusterBounds;
6726
+ if ((_o = this.options.cluster) === null || _o === void 0 ? void 0 : _o.enabled) {
6727
+ const textWidth = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetWidth) || 0;
6728
+ const textHeight = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetHeight) || 0;
6729
+ const iconWidth = (iconElement === null || iconElement === void 0 ? void 0 : iconElement.offsetWidth) || 0;
6730
+ const iconHeight = (iconElement === null || iconElement === void 0 ? void 0 : iconElement.offsetHeight) || 0;
6731
+ const gap = sourceText && iconWidth ? 6 : 0;
6732
+ const above = presentation === "icon-text-above";
6733
+ // Match the fixed content gap and label padding, excluding the count badge.
6734
+ const baseWidth = ((above ? Math.max(textWidth, iconWidth) : textWidth + iconWidth + gap) + 16) / coordinates.scaleX;
6735
+ const baseHeight = ((above ? textHeight + iconHeight + gap : Math.max(textHeight, iconHeight)) + 12) / coordinates.scaleY;
6736
+ clusterBounds = {
6737
+ x: leader ? x : x - baseWidth / 2,
6738
+ y: leader ? y - baseHeight : y - baseHeight / 2, width: baseWidth, height: baseHeight
6739
+ };
6740
+ }
6741
+ if (!leader) {
6742
+ x -= width / 2;
6743
+ y -= height / 2;
6744
+ }
6745
+ // Membership uses the unclamped source footprint, never the summary's changing dimensions.
6746
+ if (leader)
6747
+ ({ x, y } = fitLeader(anchorX, anchorY, leaderLength, width, height));
6748
+ const bounds = clusterBounds || { x, y, width, height };
6749
+ if (!leader && (bounds.x + bounds.width < 0 || bounds.y + bounds.height < 0 || bounds.x > this.viewer.canvas.clientWidth || bounds.y > this.viewer.canvas.clientHeight)) {
6750
+ return null;
6751
+ }
6752
+ 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 });
6753
+ const opacity = this.prePositioned || MAX_DISTANCE === Infinity ? 1 : 1 - distance / MAX_DISTANCE;
6754
+ return {
6755
+ canFlip: leader && presentation !== "icon", hasLeader: leader,
6756
+ textWrapped: ((sourceNaturalText === null || sourceNaturalText === void 0 ? void 0 : sourceNaturalText.offsetWidth) || 0) > TEXT_MAX_WIDTH,
6757
+ transient: this.Transient || Boolean(hover) || cursorAmount > 0,
6758
+ extendedGeometry: visual instanceof Cesium.Entity && Boolean(visual.polygon || visual.polyline || visual.corridor),
6759
+ viewportWidth: this.viewer.canvas.clientWidth,
6760
+ 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,
6761
+ entityId: (_t = this.rego) === null || _t === void 0 ? void 0 : _t.entityId, group: this.groupId, x, y, width, height, anchorX, anchorY, depth, leaderLength, distance,
6762
+ // Priority shows the hovered member individually without dragging its cluster along.
6763
+ opacity, priority: hover || cursorAmount > 0 || (state === null || state === void 0 ? void 0 : state.highlighted) ? 1 : 0, options: this.options, text: originalText, clusterBounds
6764
+ };
6765
+ };
6766
+ let displacement = { x: 0, y: 0 };
6767
+ let displayedSummaryMore = false;
6768
+ let nudge = { x: 0, y: 0, fromX: 0, fromY: 0, targetX: 0, targetY: 0, start: 0 };
6769
+ const apply = (placement) => {
6770
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
6771
+ const show = (placement === null || placement === void 0 ? void 0 : placement.visible) && placement.opacity > 0;
6772
+ clickEntityIds = show ? placement.entityIds : [];
6773
+ ele.style.visibility = show ? "visible" : "hidden";
6774
+ lineEle.style.display = show && leader ? "block" : "none";
6775
+ this.anchor.style.display = show && leader ? "block" : "none";
6776
+ if (!show) {
6777
+ displacement = { x: 0, y: 0 };
6778
+ nudge = { x: 0, y: 0, fromX: 0, fromY: 0, targetX: 0, targetY: 0, start: 0 };
6779
+ if (!this.Transient) {
6780
+ this.ringAnimation = undefined;
5860
6781
  }
5861
- // Offset end by the pixel offset.
5862
- lineEndPos2d.x += pixelOffset.x;
5863
- lineEndPos2d.y += pixelOffset.y;
5864
- // If out of the screen then we can hide it.
5865
- const startOutOfScreen = (lineStartPos2d.x < 0 || lineStartPos2d.x > this.viewer.canvas.clientWidth ||
5866
- lineStartPos2d.y < 0 || lineStartPos2d.y > this.viewer.canvas.clientHeight);
5867
- const endOutOfScreen = (lineEndPos2d.x < 0 || lineEndPos2d.x > this.viewer.canvas.clientWidth ||
5868
- lineEndPos2d.y < 0 || lineEndPos2d.y > this.viewer.canvas.clientHeight);
5869
- if (startOutOfScreen && endOutOfScreen) {
5870
- ele.style.transform = "";
5871
- ele.style.opacity = "0";
5872
- lineEle.style.display = "none";
5873
- return;
6782
+ return;
6783
+ }
6784
+ const displayText = (_a = placement.text) !== null && _a !== void 0 ? _a : originalText;
6785
+ const summaryMore = Boolean(placement.summaryMore);
6786
+ if (textElement && (textElement.textContent !== displayText + (summaryMore ? "\nโ€ฆ" : "") || displayedSummaryMore !== summaryMore)) {
6787
+ textElement.textContent = displayText;
6788
+ if (summaryMore) {
6789
+ const more = document.createElement("span");
6790
+ more.className = "entity-label-more";
6791
+ more.style.opacity = "0.65";
6792
+ more.textContent = "\nโ€ฆ";
6793
+ textElement.appendChild(more);
6794
+ }
6795
+ displayedSummaryMore = summaryMore;
6796
+ // Measure the summary's footprint next frame without replacing each member's source text.
6797
+ this.viewer.scene.requestRender();
6798
+ }
6799
+ const frame = placement.frame;
6800
+ const coordinates = layer.coordinates;
6801
+ const opacity = placement.opacity * this.HoverOpacity();
6802
+ ele.style.opacity = String(opacity);
6803
+ ele.style.zIndex = String(placement.zIndex);
6804
+ ele.dataset.clusterCount = String(placement.members.length || 1);
6805
+ countElement.style.display = placement.members.length ? "block" : "none";
6806
+ countElement.style.order = placement.textSide === "left" ? "-1" : "0";
6807
+ const count = placement.members.length ? String(placement.members.length) : "";
6808
+ if (countElement.textContent !== count) {
6809
+ countElement.textContent = count;
6810
+ // The badge participates in layout, so its new width needs another measurement.
6811
+ this.viewer.scene.requestRender();
6812
+ }
6813
+ if (iconContainer) {
6814
+ const badgeWidth = countElement.offsetWidth;
6815
+ const iconWidth = iconElement.offsetWidth;
6816
+ const narrow = iconWidth < iconElement.offsetHeight * 0.6;
6817
+ const clearance = badgeWidth ? (narrow ? badgeWidth / 2 + 2 : Math.max(0, badgeWidth / 2 - iconWidth / 4)) : 0;
6818
+ countElement.style.left = `calc(100% + ${clearance}px)`;
6819
+ // Reserve the badge overhang so adjacent text and viewport edges remain clear.
6820
+ iconContainer.style.marginTop = `${countElement.offsetHeight / 2}px`;
6821
+ iconContainer.style.marginRight = `${badgeWidth / 2 + clearance}px`;
6822
+ }
6823
+ // Summary and badge changes must fit on their first visible frame, including after resize.
6824
+ const ring = this.options.anchorRing;
6825
+ const pointer = (layer.pointer || (cursorMotion.target ? cursorMotion.position : undefined) || (this.Transient && !this.Persistent ?
6826
+ (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));
6827
+ 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);
6828
+ 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));
6829
+ const targetRadius = normalRadius + (15.4 - normalRadius) * cursorMotion.amount;
6830
+ const now = Date.now();
6831
+ const duration = (_j = ring === null || ring === void 0 ? void 0 : ring.transitionDuration) !== null && _j !== void 0 ? _j : 200;
6832
+ const sharedRing = this.Transient && !this.Persistent && this.hoverFade.to === 1 ? this.hoverRingState : undefined;
6833
+ let ringAnimation = sharedRing ? sharedRing.animation : this.ringAnimation;
6834
+ if (!ringAnimation) {
6835
+ ringAnimation = { target: 0, start: now, outer: 2.5, inner: 0, fromOuter: 2.5, fromInner: 0 };
6836
+ }
6837
+ const animateRing = () => {
6838
+ const progress = duration > 0 ? Math.min(1, Math.max(0, (now - ringAnimation.start) / duration)) : 1;
6839
+ const eased = progress * progress * (3 - 2 * progress);
6840
+ ringAnimation.outer = ringAnimation.fromOuter + ((ringAnimation.target || 2.5) - ringAnimation.fromOuter) * eased;
6841
+ ringAnimation.inner = ringAnimation.fromInner + (Math.max(0, ringAnimation.target - 2) - ringAnimation.fromInner) * eased;
6842
+ return progress;
6843
+ };
6844
+ animateRing();
6845
+ if (ringAnimation.target !== targetRadius) {
6846
+ ringAnimation = {
6847
+ ...ringAnimation,
6848
+ target: targetRadius,
6849
+ start: now,
6850
+ fromOuter: ringAnimation.outer,
6851
+ fromInner: ringAnimation.inner
6852
+ };
6853
+ }
6854
+ // Animate the hole and stem together; no CSS transition may lag behind a moving world anchor.
6855
+ if (animateRing() < 1 && (ringAnimation.fromOuter !== (targetRadius || 2.5) || ringAnimation.fromInner !== Math.max(0, targetRadius - 2))) {
6856
+ this.viewer.scene.requestRender();
6857
+ }
6858
+ this.ringAnimation = ringAnimation;
6859
+ if (sharedRing) {
6860
+ sharedRing.animation = ringAnimation;
6861
+ }
6862
+ const radius = ringAnimation.inner > 0 ? ringAnimation.outer : 0;
6863
+ 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);
6864
+ const position = { x: (stem || frame).x, y: (stem || frame).y };
6865
+ const width = ele.offsetWidth / coordinates.scaleX;
6866
+ const height = ele.offsetHeight / coordinates.scaleY;
6867
+ if (leader && placement.textSide === "left") {
6868
+ position.x = Math.max(0, Math.min(frame.anchorX - width, this.viewer.canvas.clientWidth - width));
6869
+ }
6870
+ const gap = (_l = (_k = this.options.pointerAvoidance) === null || _k === void 0 ? void 0 : _k.gap) !== null && _l !== void 0 ? _l : 12;
6871
+ const containsPointer = (x, y, margin) => {
6872
+ return pointer && pointer.x >= x - margin && pointer.x <= x + width + margin && pointer.y >= y - margin && pointer.y <= y + height + margin;
6873
+ };
6874
+ const passive = !selectOnClick || !this.visualRegister;
6875
+ if (passive && ((_m = this.options.pointerAvoidance) === null || _m === void 0 ? void 0 : _m.enabled) !== false && pointer &&
6876
+ (containsPointer(position.x, position.y, gap) || containsPointer(position.x + displacement.x, position.y + displacement.y, gap))) {
6877
+ if (containsPointer(position.x + displacement.x, position.y + displacement.y, 0)) {
6878
+ // Choose against the original box and retain the offset until the pointer leaves both boxes.
6879
+ const candidates = [
6880
+ { x: 0, y: pointer.y - gap - height - position.y },
6881
+ { x: 0, y: pointer.y + gap - position.y },
6882
+ { x: pointer.x - gap - width - position.x, y: 0 },
6883
+ { x: pointer.x + gap - position.x, y: 0 }
6884
+ ].filter(offset => position.x + offset.x >= 0 && position.y + offset.y >= 0 &&
6885
+ position.x + offset.x + width <= this.viewer.canvas.clientWidth && position.y + offset.y + height <= this.viewer.canvas.clientHeight)
6886
+ .sort((a, b) => Math.hypot(a.x, a.y) - Math.hypot(b.x, b.y));
6887
+ displacement = candidates[0] || { x: 0, y: 0 };
5874
6888
  }
5875
- // Position the label above the line end point.
5876
- this.label.style.left = `${lineEndPos2d.x}px`;
5877
- this.label.style.top = `${lineEndPos2d.y}px`;
5878
- // Update line position and size.
5879
- const dx = lineEndPos2d.x - lineStartPos2d.x;
5880
- const dy = lineEndPos2d.y - lineStartPos2d.y;
5881
- const length = Math.sqrt(dx * dx + dy * dy);
5882
- const angle = Math.atan2(dy, dx) - Math.PI / 2;
5883
- this.line.style.left = `${lineStartPos2d.x}px`;
5884
- this.line.style.top = `${lineStartPos2d.y}px`;
5885
- this.line.style.height = `${length}px`;
5886
- this.line.style.transform = `rotate(${angle}rad)`;
5887
- this.line.style.transformOrigin = 'top center';
5888
6889
  }
5889
6890
  else {
5890
- ele.style.transform = "";
5891
- ele.style.opacity = "0";
5892
- lineEle.style.display = "none";
6891
+ displacement = { x: 0, y: 0 };
6892
+ }
6893
+ const nudgeDuration = (_p = (_o = this.options.pointerAvoidance) === null || _o === void 0 ? void 0 : _o.transitionDuration) !== null && _p !== void 0 ? _p : 200;
6894
+ const animateNudge = () => {
6895
+ const progress = now < nudge.start ? 0 : nudgeDuration > 0 ? Math.min(1, (now - nudge.start) / nudgeDuration) : 1;
6896
+ const eased = progress * progress * (3 - 2 * progress);
6897
+ nudge.x = nudge.fromX + (nudge.targetX - nudge.fromX) * eased;
6898
+ nudge.y = nudge.fromY + (nudge.targetY - nudge.fromY) * eased;
6899
+ return progress;
6900
+ };
6901
+ animateNudge();
6902
+ if (nudge.targetX !== displacement.x || nudge.targetY !== displacement.y) {
6903
+ nudge = {
6904
+ ...nudge,
6905
+ fromX: nudge.x,
6906
+ fromY: nudge.y,
6907
+ targetX: displacement.x,
6908
+ targetY: displacement.y,
6909
+ start: now + ((_r = (_q = this.options.pointerAvoidance) === null || _q === void 0 ? void 0 : _q.delay) !== null && _r !== void 0 ? _r : 180)
6910
+ };
6911
+ }
6912
+ if (animateNudge() < 1 && (nudge.fromX !== nudge.targetX || nudge.fromY !== nudge.targetY)) {
6913
+ this.viewer.scene.requestRender();
6914
+ }
6915
+ position.x += nudge.x;
6916
+ position.y += nudge.y;
6917
+ ele.style.left = `${coordinates.x + position.x * coordinates.scaleX}px`;
6918
+ ele.style.top = `${coordinates.y + position.y * coordinates.scaleY}px`;
6919
+ const clickTarget = content.querySelector('[role="button"]');
6920
+ if (clickTarget) {
6921
+ 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}`);
6922
+ }
6923
+ if (leader) {
6924
+ const nodeRadius = ringAnimation.outer;
6925
+ const anchor = this.anchor;
6926
+ anchor.style.left = `${coordinates.x + (frame.anchorX - nodeRadius) * coordinates.scaleX}px`;
6927
+ anchor.style.top = `${coordinates.y + (frame.anchorY - nodeRadius) * coordinates.scaleY}px`;
6928
+ anchor.style.width = `${nodeRadius * 2 * coordinates.scaleX}px`;
6929
+ anchor.style.height = `${nodeRadius * 2 * coordinates.scaleY}px`;
6930
+ anchor.style.boxSizing = "border-box";
6931
+ anchor.style.background = "transparent";
6932
+ const svg = anchor.firstElementChild;
6933
+ svg.setAttribute("viewBox", `0 0 ${nodeRadius * 2} ${nodeRadius * 2}`);
6934
+ const circle = svg.firstElementChild;
6935
+ circle.setAttribute("cx", String(nodeRadius));
6936
+ circle.setAttribute("cy", String(nodeRadius));
6937
+ circle.setAttribute("r", String((nodeRadius + ringAnimation.inner) / 2));
6938
+ circle.setAttribute("stroke", this.options.leaderColor || "rgba(255, 255, 255, 0.8)");
6939
+ circle.setAttribute("stroke-width", String(nodeRadius - ringAnimation.inner));
6940
+ anchor.style.boxShadow = radius ? "none" : "0 0 5px rgba(0, 0, 0, 0.3)";
6941
+ anchor.style.opacity = String(opacity * (normalLeader ? 1 : cursorMotion.amount));
6942
+ anchor.style.zIndex = String(placement.zIndex);
6943
+ // The stem starts at the rim, never across the transparent centre, even after passive displacement.
6944
+ const endX = Math.max(position.x, Math.min(frame.anchorX, position.x + width));
6945
+ const endY = frame.anchorY < position.y ? position.y : frame.anchorY > position.y + height ? position.y + height : frame.anchorY;
6946
+ const dx = endX - frame.anchorX, dy = endY - frame.anchorY;
6947
+ const length = Math.hypot(dx, dy);
6948
+ const leaderAngle = Math.atan2(dy, dx);
6949
+ const pointerDx = pointer ? pointer.x - frame.anchorX : 0;
6950
+ const pointerDy = pointer ? pointer.y - frame.anchorY : 0;
6951
+ const desiredAngle = Math.hypot(pointerDx, pointerDy) > 0.5 ? Math.atan2(pointerDy, pointerDx) + Math.PI - leaderAngle : 0;
6952
+ const wrappedAngle = Math.atan2(Math.sin(desiredAngle), Math.cos(desiredAngle));
6953
+ // Keep the stem inside the 126-degree arc, away from antialiased endpoints.
6954
+ const arcFraction = 0.35;
6955
+ const rotationLimit = Math.PI * arcFraction - Math.PI / 30;
6956
+ const targetOffset = hovered ? Math.max(-rotationLimit, Math.min(rotationLimit, wrappedAngle)) : ((_v = ringAnimation.arcOffset) !== null && _v !== void 0 ? _v : 0);
6957
+ const targetOpen = cursorMotion.target || (hovered && (ring === null || ring === void 0 ? void 0 : ring.enabled) !== false && targetRadius > 0) ? 1 : 0;
6958
+ const elapsed = Math.min(32, Math.max(0, now - ((_w = ringAnimation.arcTime) !== null && _w !== void 0 ? _w : now)));
6959
+ const blend = duration > 0 ? 1 - Math.exp(-elapsed / (duration / 3)) : 1;
6960
+ const approach = (value, target) => Math.abs(value - target) < 0.001 ? target : value + (target - value) * blend;
6961
+ ringAnimation.arcOffset = approach((_x = ringAnimation.arcOffset) !== null && _x !== void 0 ? _x : 0, targetOffset);
6962
+ ringAnimation.arcOpen = approach((_y = ringAnimation.arcOpen) !== null && _y !== void 0 ? _y : 0, targetOpen);
6963
+ ringAnimation.arcTime = now;
6964
+ if (ringAnimation.arcOffset !== targetOffset || ringAnimation.arcOpen !== targetOpen) {
6965
+ this.viewer.scene.requestRender();
6966
+ }
6967
+ const circumference = Math.PI * (nodeRadius + ringAnimation.inner);
6968
+ const filledFraction = 1 - ringAnimation.arcOpen * (1 - arcFraction);
6969
+ circle.setAttribute("stroke-dasharray", `${circumference * filledFraction} ${circumference * (1 - filledFraction)}`);
6970
+ circle.setAttribute("transform", `rotate(${(leaderAngle + ringAnimation.arcOffset - Math.PI * filledFraction) * 180 / Math.PI} ${nodeRadius} ${nodeRadius})`);
6971
+ const cut = Math.min(radius, length);
6972
+ const startX = frame.anchorX + (length ? dx / length * cut : 0);
6973
+ const startY = frame.anchorY + (length ? dy / length * cut : 0);
6974
+ const screenDx = (endX - startX) * coordinates.scaleX;
6975
+ const screenDy = (endY - startY) * coordinates.scaleY;
6976
+ lineEle.style.width = `${2 * coordinates.scaleX}px`;
6977
+ lineEle.style.left = `${coordinates.x + (startX - 1) * coordinates.scaleX}px`;
6978
+ lineEle.style.top = `${coordinates.y + startY * coordinates.scaleY}px`;
6979
+ lineEle.style.height = `${Math.hypot(screenDx, screenDy)}px`;
6980
+ lineEle.style.transform = `rotate(${Math.atan2(screenDy, screenDx) - Math.PI / 2}rad)`;
6981
+ lineEle.style.boxShadow = radius ? "none" : "0 0 5px rgba(0, 0, 0, 0.3)";
6982
+ lineEle.style.transformOrigin = "top center";
6983
+ lineEle.style.opacity = String(opacity * (normalLeader ? 1 : cursorMotion.amount));
6984
+ lineEle.style.zIndex = String(placement.zIndex);
5893
6985
  }
5894
6986
  };
5895
- this.renderRemoval = this.viewer.scene.postUpdate.addEventListener(updateLabel);
5896
- updateLabel();
6987
+ ele.style.display = "flex";
6988
+ ele.style.visibility = "hidden";
6989
+ this.renderRemoval = layer.Add({ project, apply });
5897
6990
  // If terrain changes then we'll re-call createLabel as the position may have changed.
5898
6991
  let terrTimeout = null;
5899
6992
  this.terrChangedRemoval = this.viewer.scene.terrainProviderChanged.addEventListener(() => {
@@ -5913,6 +7006,9 @@
5913
7006
  * Removes label from the viewer and unsubscribes from events.
5914
7007
  */
5915
7008
  removeLabel() {
7009
+ var _a;
7010
+ (_a = this.anchor) === null || _a === void 0 ? void 0 : _a.remove();
7011
+ this.anchor = null;
5916
7012
  if (this.label &&
5917
7013
  this.label.parentElement) {
5918
7014
  this.label.parentElement.removeChild(this.label);
@@ -5938,16 +7034,44 @@
5938
7034
  * If one exists then the action is ignored.
5939
7035
  */
5940
7036
  function Attach(params) {
5941
- const { rego, viewer, pos3d, visualRegister, api } = params;
5942
- if (!rego || !viewer || rego[EntityLabel.ATTACH_KEY]) {
7037
+ var _a, _b, _c;
7038
+ const { rego, viewer, pos3d, visualRegister, api, options, groupId } = params;
7039
+ if (!rego || !viewer) {
5943
7040
  return;
5944
7041
  }
7042
+ if (params.persistent == null && visualRegister) {
7043
+ const group = visualRegister.GetLabelGroupFor(rego.entityId, rego.menuItemId);
7044
+ if (group || visualRegister.GetIsLabelled({ entityId: rego.entityId, menuItemId: rego.menuItemId })) {
7045
+ return Attach({
7046
+ ...params,
7047
+ pos3d: undefined,
7048
+ options: (_a = group === null || group === void 0 ? void 0 : group.options) !== null && _a !== void 0 ? _a : options,
7049
+ groupId: (_b = group === null || group === void 0 ? void 0 : group.groupId) !== null && _b !== void 0 ? _b : groupId,
7050
+ persistent: true
7051
+ });
7052
+ }
7053
+ }
7054
+ if (rego[EntityLabel.ATTACH_KEY]) {
7055
+ // Legacy hover callers omit ownership/options and must not reconfigure a register-owned label.
7056
+ if (rego[EntityLabel.ATTACH_KEY].Persistent && params.persistent !== true && !groupId) {
7057
+ return rego[EntityLabel.ATTACH_KEY];
7058
+ }
7059
+ rego[EntityLabel.ATTACH_KEY].Persistent = (_c = params.persistent) !== null && _c !== void 0 ? _c : Boolean(groupId);
7060
+ rego[EntityLabel.ATTACH_KEY].Configure(options, groupId);
7061
+ return rego[EntityLabel.ATTACH_KEY];
7062
+ }
5945
7063
  rego[EntityLabel.ATTACH_KEY] = new Label({
5946
7064
  rego,
5947
7065
  viewer,
5948
7066
  pos3d,
5949
7067
  visualRegister,
5950
- api
7068
+ api,
7069
+ options,
7070
+ groupId,
7071
+ persistent: params.persistent,
7072
+ transient: params.transient,
7073
+ hoverRingState: params.hoverRingState,
7074
+ visible: params.visible
5951
7075
  });
5952
7076
  return rego[EntityLabel.ATTACH_KEY];
5953
7077
  }
@@ -5962,12 +7086,19 @@
5962
7086
  if (!rego && label) {
5963
7087
  rego = label.Rego;
5964
7088
  }
7089
+ if (label && label !== (rego === null || rego === void 0 ? void 0 : rego[EntityLabel.ATTACH_KEY])) {
7090
+ if (!label.Persistent || params.force)
7091
+ label.Dispose();
7092
+ return;
7093
+ }
5965
7094
  if (!rego || !rego[EntityLabel.ATTACH_KEY]) {
5966
7095
  if (label) {
5967
7096
  (_a = label.Dispose) === null || _a === void 0 ? void 0 : _a.call(label);
5968
7097
  }
5969
7098
  return;
5970
7099
  }
7100
+ if (rego[EntityLabel.ATTACH_KEY].Persistent && !params.force)
7101
+ return;
5971
7102
  (_c = (_b = rego[EntityLabel.ATTACH_KEY]).Dispose) === null || _c === void 0 ? void 0 : _c.call(_b);
5972
7103
  rego[EntityLabel.ATTACH_KEY] = null;
5973
7104
  }
@@ -5987,6 +7118,481 @@
5987
7118
  EntityLabel.GetLabel = GetLabel;
5988
7119
  })(exports.EntityLabel || (exports.EntityLabel = {}));
5989
7120
 
7121
+ (function (EntityLabelGroup) {
7122
+ /*
7123
+ * Keeps the query response useful even when the Entity has no loaded visual yet.
7124
+ */
7125
+ function FromEntity(entity) {
7126
+ var _a, _b, _c, _d;
7127
+ const location = (_a = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _a === void 0 ? void 0 : _a.Location;
7128
+ const pivot = (_b = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _b === void 0 ? void 0 : _b.GeometryWorldPivot;
7129
+ let cartographic;
7130
+ if ((pivot === null || pivot === void 0 ? void 0 : pivot.length) >= 3 && pivot.every(Number.isFinite)) {
7131
+ cartographic = Cesium.Cartographic.fromCartesian(Cesium.Cartesian3.fromArray(pivot));
7132
+ }
7133
+ return {
7134
+ entityId: (_c = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _c === void 0 ? void 0 : _c.ID,
7135
+ text: (_d = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _d === void 0 ? void 0 : _d.Name,
7136
+ longitude: cartographic ? Cesium.Math.toDegrees(cartographic.longitude) : location === null || location === void 0 ? void 0 : location.longitude,
7137
+ latitude: cartographic ? Cesium.Math.toDegrees(cartographic.latitude) : location === null || location === void 0 ? void 0 : location.latitude,
7138
+ altitude: cartographic ? cartographic.height : location === null || location === void 0 ? void 0 : location.altitude,
7139
+ entity
7140
+ };
7141
+ }
7142
+ EntityLabelGroup.FromEntity = FromEntity;
7143
+ /*
7144
+ * Uses the existing DataLab grammar, discarding actions and projections that cannot return Entities.
7145
+ */
7146
+ async function Loader(source, api, signal) {
7147
+ var _a;
7148
+ var _b;
7149
+ if (source.type === "provider") {
7150
+ return (offset, limit, signal) => source.loadPage({ offset, limit, signal });
7151
+ }
7152
+ if (source.type === "entityIds" || source.type === "json") {
7153
+ const items = (source.type === "entityIds" ? source.entityIds.map(entityId => ({ entityId })) :
7154
+ typeof source.items === "string" ? JSON.parse(source.items) : source.items);
7155
+ if (!Array.isArray(items)) {
7156
+ throw new Error("Label JSON must be an array of label items");
7157
+ }
7158
+ return async (offset, limit) => ({ items: items.slice(offset, offset + limit), hasMore: offset + limit < items.length });
7159
+ }
7160
+ if (!api) {
7161
+ throw new Error("DataLab label sources require API getters on the visual register");
7162
+ }
7163
+ let query;
7164
+ if (source.type === "queryId") {
7165
+ const saved = (await BModels.DataLab.Get({ api, queryId: source.queryId, req: { noCache: true, abortSignal: signal } })).query;
7166
+ 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;
7167
+ if (!selection) {
7168
+ throw new Error("Saved label query has no selection criteria");
7169
+ }
7170
+ query = { PrimarySelection: selection };
7171
+ }
7172
+ else {
7173
+ query = typeof source.query === "string" ? JSON.parse(source.query) : source.query;
7174
+ }
7175
+ if (!(query === null || query === void 0 ? void 0 : query.PrimarySelection)) {
7176
+ throw new Error("Label query requires PrimarySelection");
7177
+ }
7178
+ query = JSON.parse(JSON.stringify(query));
7179
+ delete query.Action;
7180
+ query.PrimarySelection.Select = { Type: BModels.DataLab.ESelectType.Entity };
7181
+ (_b = query.PrimarySelection).OrderBy || (_b.OrderBy = "Bruce/ID");
7182
+ query.Expand = query.Expand ? `${query.Expand},location` : "location";
7183
+ return async (offset, limit, signal) => {
7184
+ const response = await BModels.DataLab.Run({ api, query, skip: offset, load: limit, migrated: true, req: { noCache: true, abortSignal: signal } });
7185
+ const entities = response.entities || response.items || [];
7186
+ return {
7187
+ items: entities.map(FromEntity),
7188
+ hasMore: response.totalCount != null ? offset + entities.length < response.totalCount : entities.length === limit
7189
+ };
7190
+ };
7191
+ }
7192
+ EntityLabelGroup.Loader = Loader;
7193
+ /*
7194
+ * Snapshot settings without dropping Calculator's optional runtime function values.
7195
+ */
7196
+ function copyOptions(value) {
7197
+ if (Array.isArray(value)) {
7198
+ return value.map(copyOptions);
7199
+ }
7200
+ if (value && typeof value === "object") {
7201
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, copyOptions(item)]));
7202
+ }
7203
+ return value;
7204
+ }
7205
+ /*
7206
+ * Evaluate source attributes during register refresh, never in the camera-frame layout loop.
7207
+ */
7208
+ function resolveIcon(value, entity) {
7209
+ if (!Array.isArray(value))
7210
+ return value;
7211
+ try {
7212
+ return BModels.Calculator.GetString(value, entity || {});
7213
+ }
7214
+ catch {
7215
+ // An invalid expression on one item must not prevent the remaining labels from rendering.
7216
+ return null;
7217
+ }
7218
+ }
7219
+ /*
7220
+ * Owns membership independently of manual labelled states, with atomic replacement and stale-load cancellation.
7221
+ */
7222
+ class Manager {
7223
+ constructor(register, changed = () => { }) {
7224
+ this.register = register;
7225
+ this.changed = changed;
7226
+ this.active = new Map();
7227
+ this.pending = new Map();
7228
+ this.standalone = new Map();
7229
+ this.standaloneItems = new Map();
7230
+ this.members = new Map();
7231
+ this.definitions = new Map();
7232
+ this.visibility = new Map();
7233
+ this.errors = new Map();
7234
+ }
7235
+ notify() {
7236
+ try {
7237
+ this.changed();
7238
+ }
7239
+ catch (error) {
7240
+ console.error("Label group observer failed", error);
7241
+ }
7242
+ }
7243
+ Refresh(id) {
7244
+ const definition = this.definitions.get(id);
7245
+ if (!definition)
7246
+ return Promise.reject(new Error(`Unknown label group: ${id}`));
7247
+ return this.Set({ ...definition, visible: this.visibility.get(id) });
7248
+ }
7249
+ SetVisible(id, visible) {
7250
+ var _a;
7251
+ if (!this.definitions.has(id))
7252
+ return false;
7253
+ if (this.visibility.get(id) === visible)
7254
+ return true;
7255
+ this.visibility.set(id, visible);
7256
+ this.refresh(new Set(((_a = this.active.get(id)) === null || _a === void 0 ? void 0 : _a.items.keys()) || []));
7257
+ this.notify();
7258
+ return true;
7259
+ }
7260
+ async Set(definition) {
7261
+ var _a, _b, _c, _d, _e, _f, _g, _h;
7262
+ if (!(definition === null || definition === void 0 ? void 0 : definition.id) || !definition.source) {
7263
+ throw new Error("Label group requires id and source");
7264
+ }
7265
+ definition = { ...definition, options: copyOptions(definition.options) };
7266
+ const pageSize = (_a = definition.pageSize) !== null && _a !== void 0 ? _a : 500;
7267
+ const maximum = (_b = definition.maxItems) !== null && _b !== void 0 ? _b : 50000;
7268
+ if (!Number.isInteger(pageSize) || pageSize < 1 || !Number.isInteger(maximum) || maximum < 1) {
7269
+ throw new Error("Label pageSize and maxItems must be positive integers");
7270
+ }
7271
+ ValidateOptions(definition.options);
7272
+ (_c = this.pending.get(definition.id)) === null || _c === void 0 ? void 0 : _c.abort();
7273
+ const controller = new AbortController();
7274
+ this.pending.set(definition.id, controller);
7275
+ this.definitions.set(definition.id, definition);
7276
+ const wasVisible = this.visibility.get(definition.id);
7277
+ 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);
7278
+ if (wasVisible != null && wasVisible !== this.visibility.get(definition.id)) {
7279
+ this.refresh(new Set(((_f = this.active.get(definition.id)) === null || _f === void 0 ? void 0 : _f.items.keys()) || []));
7280
+ }
7281
+ this.errors.delete(definition.id);
7282
+ this.notify();
7283
+ const cancelled = () => ({ id: definition.id, entityIds: [], truncated: false, cancelled: true });
7284
+ const items = new Map();
7285
+ let truncated = false;
7286
+ try {
7287
+ const load = await Loader(definition.source, (_g = this.register.ApiGetters) === null || _g === void 0 ? void 0 : _g.GetBruceApi(), controller.signal);
7288
+ let offset = 0;
7289
+ for (let pageNumber = 0;; pageNumber++) {
7290
+ if (controller.signal.aborted || this.register.Disposed) {
7291
+ return cancelled();
7292
+ }
7293
+ if (pageNumber >= 10000) {
7294
+ throw new Error("Label source exceeded 10000 pages");
7295
+ }
7296
+ const page = await load(offset, Math.min(pageSize, maximum - items.size), controller.signal);
7297
+ if (controller.signal.aborted || this.register.Disposed)
7298
+ return cancelled();
7299
+ if (!Array.isArray(page === null || page === void 0 ? void 0 : page.items)) {
7300
+ throw new Error("Label source returned an invalid page");
7301
+ }
7302
+ for (const item of page.items) {
7303
+ if (!item || typeof item.entityId !== "string" || !item.entityId) {
7304
+ throw new Error("Label item requires entityId");
7305
+ }
7306
+ if (!items.has(item.entityId) && items.size >= maximum) {
7307
+ truncated = true;
7308
+ break;
7309
+ }
7310
+ items.set(item.entityId, item);
7311
+ }
7312
+ if (!page.hasMore) {
7313
+ break;
7314
+ }
7315
+ if (items.size >= maximum) {
7316
+ truncated = true;
7317
+ break;
7318
+ }
7319
+ const next = (_h = page.nextOffset) !== null && _h !== void 0 ? _h : offset + page.items.length;
7320
+ if (!Number.isSafeInteger(next) || next <= offset) {
7321
+ throw new Error("Label source pagination did not advance");
7322
+ }
7323
+ offset = next;
7324
+ }
7325
+ const previous = this.active.get(definition.id);
7326
+ const affected = new Set([...((previous === null || previous === void 0 ? void 0 : previous.items.keys()) || []), ...items.keys()]);
7327
+ this.unindex(definition.id);
7328
+ // Reinsertion makes the most recently committed matching group the presentation owner.
7329
+ this.active.delete(definition.id);
7330
+ this.active.set(definition.id, { definition, items, truncated });
7331
+ for (const id of items.keys()) {
7332
+ if (!this.members.has(id)) {
7333
+ this.members.set(id, new Set());
7334
+ }
7335
+ this.members.get(id).add(definition.id);
7336
+ }
7337
+ this.refresh(affected);
7338
+ return { id: definition.id, entityIds: Array.from(items.keys()), truncated, cancelled: false };
7339
+ }
7340
+ catch (error) {
7341
+ if (controller.signal.aborted || this.register.Disposed) {
7342
+ return cancelled();
7343
+ }
7344
+ this.errors.set(definition.id, error instanceof Error ? error.message : String(error));
7345
+ throw error;
7346
+ }
7347
+ finally {
7348
+ if (this.pending.get(definition.id) === controller) {
7349
+ this.pending.delete(definition.id);
7350
+ this.notify();
7351
+ }
7352
+ }
7353
+ }
7354
+ Get() {
7355
+ return Array.from(this.definitions.keys(), id => {
7356
+ var _a;
7357
+ const group = this.active.get(id);
7358
+ return {
7359
+ id,
7360
+ entityIds: Array.from((group === null || group === void 0 ? void 0 : group.items.keys()) || []),
7361
+ truncated: (_a = group === null || group === void 0 ? void 0 : group.truncated) !== null && _a !== void 0 ? _a : false,
7362
+ cancelled: false,
7363
+ visible: this.visibility.get(id) !== false,
7364
+ loading: this.pending.has(id),
7365
+ error: this.errors.get(id)
7366
+ };
7367
+ });
7368
+ }
7369
+ Has(entityId, menuItemId) {
7370
+ for (const id of this.members.get(entityId) || []) {
7371
+ const group = this.active.get(id);
7372
+ if (group && this.visibility.get(id) !== false && (!group.definition.menuItemId || group.definition.menuItemId === menuItemId)) {
7373
+ return true;
7374
+ }
7375
+ }
7376
+ return false;
7377
+ }
7378
+ Resolve(entityId, menuItemId) {
7379
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
7380
+ const ids = Array.from(this.members.get(entityId) || []).reverse();
7381
+ for (const id of ids) {
7382
+ const group = this.active.get(id);
7383
+ if (this.visibility.get(id) === false) {
7384
+ continue;
7385
+ }
7386
+ if (group.definition.menuItemId && group.definition.menuItemId !== menuItemId) {
7387
+ continue;
7388
+ }
7389
+ const item = group.items.get(entityId);
7390
+ const entity = (_a = item.entity) !== null && _a !== void 0 ? _a : { Bruce: { ID: entityId, Name: item.text } };
7391
+ let iconPriority = (_b = group.definition.options) === null || _b === void 0 ? void 0 : _b.iconPriority;
7392
+ if (Array.isArray(iconPriority)) {
7393
+ try {
7394
+ iconPriority = BModels.Calculator.GetNumber(iconPriority, entity);
7395
+ }
7396
+ catch {
7397
+ iconPriority = null;
7398
+ }
7399
+ }
7400
+ let iconScale = (_c = group.definition.options) === null || _c === void 0 ? void 0 : _c.iconScale;
7401
+ if (Array.isArray(iconScale)) {
7402
+ try {
7403
+ iconScale = BModels.Calculator.GetNumber(iconScale, entity);
7404
+ }
7405
+ catch {
7406
+ iconScale = null;
7407
+ }
7408
+ if (!Number.isFinite(iconScale)) {
7409
+ iconScale = 1;
7410
+ }
7411
+ }
7412
+ if (iconScale != null) {
7413
+ iconScale = Math.max(0, iconScale);
7414
+ }
7415
+ const textSetting = (_d = group.definition.options) === null || _d === void 0 ? void 0 : _d.text;
7416
+ let resolvedText = resolveIcon(textSetting, entity);
7417
+ const expression = Array.isArray(textSetting);
7418
+ if (expression && !(resolvedText === null || resolvedText === void 0 ? void 0 : resolvedText.trim())) {
7419
+ resolvedText = undefined;
7420
+ }
7421
+ 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]
7422
+ .find(text => typeof text === "string" && text.trim().length > 0);
7423
+ const options = {
7424
+ ...group.definition.options,
7425
+ text: resolvedText !== null && resolvedText !== void 0 ? resolvedText : fallbackText,
7426
+ iconScale: iconScale,
7427
+ icon: resolveIcon((_g = group.definition.options) === null || _g === void 0 ? void 0 : _g.icon, entity),
7428
+ iconUrl: resolveIcon((_h = group.definition.options) === null || _h === void 0 ? void 0 : _h.iconUrl, entity),
7429
+ iconClass: resolveIcon((_j = group.definition.options) === null || _j === void 0 ? void 0 : _j.iconClass, entity),
7430
+ iconColor: resolveIcon((_k = group.definition.options) === null || _k === void 0 ? void 0 : _k.iconColor, entity),
7431
+ iconBackground: (_l = group.definition.options) === null || _l === void 0 ? void 0 : _l.iconBackground,
7432
+ iconPriority: Number.isFinite(iconPriority) ? iconPriority : item.iconPriority,
7433
+ iconStatus: (_o = resolveIcon((_m = group.definition.options) === null || _m === void 0 ? void 0 : _m.iconStatus, entity)) !== null && _o !== void 0 ? _o : item.iconStatus
7434
+ };
7435
+ if (options.attributes && item.entity && !(expression && resolvedText != null)) {
7436
+ options.text = [
7437
+ (_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,
7438
+ ...options.attributes.map(path => BModels.Entity.GetValue({ entity: item.entity, path })).filter(value => value != null).map(String)
7439
+ ].join("\n");
7440
+ }
7441
+ return { groupId: id, options, item };
7442
+ }
7443
+ return null;
7444
+ }
7445
+ RefreshEntity(entityId) {
7446
+ var _a, _b, _c, _d, _e, _f, _g;
7447
+ const match = this.Resolve(entityId);
7448
+ const registered = this.register.GetRego({ entityId });
7449
+ if (!registered && match && this.standalone.has(entityId) && this.standaloneItems.get(entityId) === match.item) {
7450
+ 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);
7451
+ return;
7452
+ }
7453
+ (_c = this.standalone.get(entityId)) === null || _c === void 0 ? void 0 : _c.Dispose();
7454
+ this.standalone.delete(entityId);
7455
+ this.standaloneItems.delete(entityId);
7456
+ if (this.register.Disposed || registered) {
7457
+ return;
7458
+ }
7459
+ if (!match) {
7460
+ return;
7461
+ }
7462
+ const item = match.item;
7463
+ if (![item.longitude, item.latitude, (_d = item.altitude) !== null && _d !== void 0 ? _d : 0].every(Number.isFinite) || Math.abs(item.latitude) > 90) {
7464
+ return;
7465
+ }
7466
+ const visual = new Cesium.Entity({
7467
+ position: Cesium.Cartesian3.fromDegrees(item.longitude, item.latitude, item.altitude || 0),
7468
+ point: { heightReference: Cesium.HeightReference.NONE, pixelSize: 0 }
7469
+ });
7470
+ this.standalone.set(entityId, new exports.EntityLabel.Label({
7471
+ viewer: this.register.Viewer,
7472
+ visualRegister: this.register,
7473
+ api: (_e = this.register.ApiGetters) === null || _e === void 0 ? void 0 : _e.GetBruceApi(),
7474
+ rego: { entityId, visual, name: item.text || entityId, priority: 0 },
7475
+ options: { ...match.options, text: (_g = (_f = match.options.text) !== null && _f !== void 0 ? _f : item.text) !== null && _g !== void 0 ? _g : entityId },
7476
+ groupId: match.groupId,
7477
+ visible: () => {
7478
+ const state = this.register.GetState({ entityId });
7479
+ return !state.hidden && (!this.register.GetIsIsolatedAny() || state.isolated || this.register.GetIsolationOpacity() != null);
7480
+ }
7481
+ }));
7482
+ this.standaloneItems.set(entityId, item);
7483
+ }
7484
+ Remove(id) {
7485
+ var _a;
7486
+ const known = this.definitions.delete(id);
7487
+ this.visibility.delete(id);
7488
+ this.errors.delete(id);
7489
+ (_a = this.pending.get(id)) === null || _a === void 0 ? void 0 : _a.abort();
7490
+ this.pending.delete(id);
7491
+ const group = this.active.get(id);
7492
+ if (!group) {
7493
+ if (known) {
7494
+ this.notify();
7495
+ return;
7496
+ }
7497
+ }
7498
+ this.unindex(id);
7499
+ this.active.delete(id);
7500
+ this.refresh(new Set(group.items.keys()));
7501
+ this.notify();
7502
+ }
7503
+ Clear() {
7504
+ for (const controller of this.pending.values()) {
7505
+ controller.abort();
7506
+ }
7507
+ this.pending.clear();
7508
+ const affected = new Set(this.members.keys());
7509
+ this.active.clear();
7510
+ this.members.clear();
7511
+ this.definitions.clear();
7512
+ this.visibility.clear();
7513
+ this.errors.clear();
7514
+ this.refresh(affected);
7515
+ this.notify();
7516
+ }
7517
+ Dispose() {
7518
+ for (const controller of this.pending.values()) {
7519
+ controller.abort();
7520
+ }
7521
+ for (const label of this.standalone.values()) {
7522
+ label.Dispose();
7523
+ }
7524
+ this.pending.clear();
7525
+ this.standalone.clear();
7526
+ this.standaloneItems.clear();
7527
+ this.active.clear();
7528
+ this.members.clear();
7529
+ this.definitions.clear();
7530
+ this.visibility.clear();
7531
+ this.errors.clear();
7532
+ }
7533
+ unindex(groupId) {
7534
+ var _a;
7535
+ for (const id of ((_a = this.active.get(groupId)) === null || _a === void 0 ? void 0 : _a.items.keys()) || []) {
7536
+ const groups = this.members.get(id);
7537
+ groups === null || groups === void 0 ? void 0 : groups.delete(groupId);
7538
+ if (!(groups === null || groups === void 0 ? void 0 : groups.size)) {
7539
+ this.members.delete(id);
7540
+ }
7541
+ }
7542
+ }
7543
+ refresh(ids) {
7544
+ var _a;
7545
+ // Creation drains through the register's bounded update queue, including source-only labels.
7546
+ for (const id of ids) {
7547
+ (_a = this.standalone.get(id)) === null || _a === void 0 ? void 0 : _a.Dispose();
7548
+ this.standalone.delete(id);
7549
+ this.standaloneItems.delete(id);
7550
+ }
7551
+ this.register.ForceUpdate({ entityIds: Array.from(ids) });
7552
+ this.register.Viewer.scene.requestRender();
7553
+ }
7554
+ }
7555
+ EntityLabelGroup.Manager = Manager;
7556
+ /*
7557
+ * Rejects invalid geometry settings before they can create unbounded spatial-index work.
7558
+ */
7559
+ function ValidateOptions(options) {
7560
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
7561
+ if (!options) {
7562
+ return;
7563
+ }
7564
+ if (options.iconBackground != null && typeof options.iconBackground !== "boolean") {
7565
+ throw new Error("Label iconBackground must be a boolean");
7566
+ }
7567
+ for (const value of [options.icon, options.iconUrl, options.iconClass, options.iconColor, options.text]) {
7568
+ if (value != null && typeof value !== "string" && !Array.isArray(value)) {
7569
+ throw new Error("Label icons must be strings or Calculator field arrays");
7570
+ }
7571
+ }
7572
+ if (options.iconScale != null && !Array.isArray(options.iconScale) && !Number.isFinite(options.iconScale)) {
7573
+ throw new Error("Label iconScale must be finite or a Calculator field array");
7574
+ }
7575
+ if (options.iconPriority != null && !Array.isArray(options.iconPriority) && !Number.isFinite(options.iconPriority)) {
7576
+ throw new Error("Label iconPriority must be finite or a Calculator field array");
7577
+ }
7578
+ 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]) {
7579
+ if (value != null && (!Number.isFinite(value) || value < 0)) {
7580
+ throw new Error("Label dimensions and distances must be finite and nonnegative");
7581
+ }
7582
+ }
7583
+ if (((_k = options.near) !== null && _k !== void 0 ? _k : 0) > ((_l = options.far) !== null && _l !== void 0 ? _l : Infinity)) {
7584
+ throw new Error("Label near must not exceed far");
7585
+ }
7586
+ if (options.overlapOpacity != null && (!Number.isFinite(options.overlapOpacity) || options.overlapOpacity < 0 || options.overlapOpacity > 1)) {
7587
+ throw new Error("Label overlapOpacity must be between zero and one");
7588
+ }
7589
+ if (((_m = options.cluster) === null || _m === void 0 ? void 0 : _m.minSize) != null && (!Number.isInteger(options.cluster.minSize) || options.cluster.minSize < 2)) {
7590
+ throw new Error("Label cluster minSize must be an integer of at least two");
7591
+ }
7592
+ }
7593
+ EntityLabelGroup.ValidateOptions = ValidateOptions;
7594
+ })(exports.EntityLabelGroup || (exports.EntityLabelGroup = {}));
7595
+
5990
7596
  /**
5991
7597
  * Utility for rendering parabolas between two points on the map.
5992
7598
  */
@@ -7480,7 +9086,7 @@
7480
9086
  }
7481
9087
  }
7482
9088
  function updateEntity(viewer, entityId, register, refresh, cache = {}) {
7483
- var _a;
9089
+ var _a, _b, _c;
7484
9090
  if (!entityId) {
7485
9091
  // What are we doing?
7486
9092
  return;
@@ -7593,20 +9199,25 @@
7593
9199
  }
7594
9200
  }
7595
9201
  }
7596
- if (state.labelled && visible) {
9202
+ const labelGroup = register.GetLabelGroupFor(rego.entityId, rego.menuItemId);
9203
+ if ((state.labelled || labelGroup) && visible) {
7597
9204
  exports.EntityLabel.Attach({
7598
9205
  rego,
7599
9206
  viewer,
7600
- api: register.ApiGetters.GetBruceApi(),
7601
- visualRegister: register
9207
+ api: (_b = register.ApiGetters) === null || _b === void 0 ? void 0 : _b.GetBruceApi(),
9208
+ visualRegister: register,
9209
+ options: labelGroup === null || labelGroup === void 0 ? void 0 : labelGroup.options,
9210
+ groupId: labelGroup === null || labelGroup === void 0 ? void 0 : labelGroup.groupId,
9211
+ persistent: true
7602
9212
  });
7603
9213
  }
7604
- else {
9214
+ else if (!visible || ((_c = exports.EntityLabel.GetLabel({ rego })) === null || _c === void 0 ? void 0 : _c.Persistent)) {
7605
9215
  exports.EntityLabel.Detatch({
7606
- rego
9216
+ rego, force: true
7607
9217
  });
7608
9218
  }
7609
9219
  }
9220
+ register.RefreshLabelGroupEntity(entityId);
7610
9221
  }
7611
9222
  function markEntity(register, rego, visual, ignoreParent) {
7612
9223
  const vis = visual;
@@ -7717,6 +9328,7 @@
7717
9328
  }
7718
9329
  return this.onUpdate;
7719
9330
  }
9331
+ get OnLabelGroupsChanged() { return this.labelGroupsChanged; }
7720
9332
  constructor(params) {
7721
9333
  this.id = BModels.ObjectUtils.UId();
7722
9334
  this.disposed = false;
@@ -7755,10 +9367,35 @@
7755
9367
  this.menuItemSources = new Map();
7756
9368
  // Set when the data source order no longer matches the zIndexes seen, so it is re-applied once.
7757
9369
  this.sourceOrderDirty = false;
9370
+ this.labelGroupsChanged = new BModels.BruceEvent();
9371
+ this.hoverRingState = {};
9372
+ this.retiringHoverLabels = new Map();
7758
9373
  /*
7759
9374
  * Tilesets in the scene, keyed by the tileset itself.
7760
9375
  */
7761
9376
  this.tilesets = new Map();
9377
+ this.ClearLabelHover = () => {
9378
+ if (!this.labelHover && !this.hoverLabel) {
9379
+ return;
9380
+ }
9381
+ this.ReleaseHoverLabel();
9382
+ this.labelHover = undefined;
9383
+ clearTimeout(this.hoverRingReset);
9384
+ this.hoverRingReset = setTimeout(() => { this.hoverRingState = {}; }, 400);
9385
+ const doc = this.viewer.container.ownerDocument;
9386
+ doc.removeEventListener("pointermove", this.CheckLabelHoverBounds, true);
9387
+ this.viewer.container.removeEventListener("pointerleave", this.CheckLabelHoverBounds);
9388
+ doc.defaultView.removeEventListener("blur", this.ClearLabelHover);
9389
+ if (!this.viewer.isDestroyed()) {
9390
+ this.viewer.scene.requestRender();
9391
+ }
9392
+ };
9393
+ this.CheckLabelHoverBounds = (event) => {
9394
+ const bounds = this.viewer.canvas.getBoundingClientRect();
9395
+ if (event.clientX < bounds.left || event.clientX >= bounds.right || event.clientY < bounds.top || event.clientY >= bounds.bottom) {
9396
+ this.ClearLabelHover();
9397
+ }
9398
+ };
7762
9399
  this.viewer = params.viewer;
7763
9400
  this.apiGetters = params.apiGetters;
7764
9401
  this.cameraCullerDispose = exports.VisualRegisterCuller.Monitor({
@@ -7954,6 +9591,7 @@
7954
9591
  const BATCH_SIZE = 300;
7955
9592
  if (this.updateQueue.length) {
7956
9593
  this.updateQueueInterval = setInterval(() => {
9594
+ var _a;
7957
9595
  if (this.Disposed || (!this.viewer || !this.viewer.scene || this.viewer.isDestroyed())) {
7958
9596
  clearInterval(this.updateQueueInterval);
7959
9597
  this.updateQueueInterval = null;
@@ -7966,6 +9604,8 @@
7966
9604
  this.updateQueueIds.delete(entityId);
7967
9605
  // Check if still registered.
7968
9606
  if (entityId && (!this.rego[entityId] || !this.rego[entityId].length)) {
9607
+ (_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.RefreshEntity(entityId);
9608
+ delete this.updateQueueSettings[entityId];
7969
9609
  continue;
7970
9610
  }
7971
9611
  updateEntity(this.viewer, entityId, this, this.updateQueueSettings[entityId] == null ? false : this.updateQueueSettings[entityId], cache);
@@ -8623,8 +10263,23 @@
8623
10263
  markEntity(this, rego, rego.visual, false);
8624
10264
  }
8625
10265
  Dispose() {
8626
- var _a, _b, _c, _d;
8627
- (_a = this.cameraCullerDispose) === null || _a === void 0 ? void 0 : _a.call(this, {
10266
+ var _a, _b, _c, _d, _e;
10267
+ this.ClearLabelHover();
10268
+ clearTimeout(this.hoverRingReset);
10269
+ this.hoverRingState = {};
10270
+ for (const { label, timer } of this.retiringHoverLabels.values()) {
10271
+ clearTimeout(timer);
10272
+ exports.EntityLabel.Detatch({ label });
10273
+ }
10274
+ this.retiringHoverLabels.clear();
10275
+ (_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.Dispose();
10276
+ this.labelGroupsChanged.Clear();
10277
+ for (const regos of Object.values(this.rego)) {
10278
+ for (const rego of regos)
10279
+ exports.EntityLabel.Detatch({ rego, force: true });
10280
+ }
10281
+ exports.EntityLabel.SetContainer(this.viewer);
10282
+ (_b = this.cameraCullerDispose) === null || _b === void 0 ? void 0 : _b.call(this, {
8628
10283
  uncull: false
8629
10284
  });
8630
10285
  this.cameraCullerDispose = null;
@@ -8640,7 +10295,7 @@
8640
10295
  this.disposed = true;
8641
10296
  for (const record of Array.from(this.menuItemSources.values())) {
8642
10297
  try {
8643
- (_c = (_b = this.viewer) === null || _b === void 0 ? void 0 : _b.dataSources) === null || _c === void 0 ? void 0 : _c.remove(record.source, true);
10298
+ (_d = (_c = this.viewer) === null || _c === void 0 ? void 0 : _c.dataSources) === null || _d === void 0 ? void 0 : _d.remove(record.source, true);
8644
10299
  }
8645
10300
  catch (e) {
8646
10301
  console.error(e);
@@ -8649,7 +10304,7 @@
8649
10304
  this.menuItemSources.clear();
8650
10305
  // Hand every tileset's show flag back to its owner, holding whatever the owner last asked for.
8651
10306
  for (const rego of Array.from(this.tilesets.values())) {
8652
- (_d = rego.gate) === null || _d === void 0 ? void 0 : _d.Release();
10307
+ (_e = rego.gate) === null || _e === void 0 ? void 0 : _e.Release();
8653
10308
  }
8654
10309
  this.tilesets.clear();
8655
10310
  }
@@ -9601,7 +11256,7 @@
9601
11256
  * @returns
9602
11257
  */
9603
11258
  RemoveRegos(params) {
9604
- var _a, _b, _c;
11259
+ var _a, _b, _c, _d, _e;
9605
11260
  let { entityId, menuItemId, doUpdate, relation, retainTagIds, requestRender, doRemove, source, lazy, onComplete } = params;
9606
11261
  if (doUpdate == null || doUpdate === undefined) {
9607
11262
  doUpdate = true;
@@ -9622,7 +11277,7 @@
9622
11277
  }
9623
11278
  }
9624
11279
  const removeOne = (entityId) => {
9625
- var _a, _b;
11280
+ var _a, _b, _c;
9626
11281
  const entityRegos = this.rego[entityId];
9627
11282
  if (!entityRegos) {
9628
11283
  return;
@@ -9637,7 +11292,7 @@
9637
11292
  return;
9638
11293
  }
9639
11294
  exports.EntityLabel.Detatch({
9640
- rego
11295
+ rego, force: true
9641
11296
  });
9642
11297
  // Never tear down the graphic the caller is registering: the renderers hand back
9643
11298
  // the same object when they reuse it, so removing it here destroys what replaces it.
@@ -9662,15 +11317,14 @@
9662
11317
  (_a = this.onUpdate) === null || _a === void 0 ? void 0 : _a.Trigger(update);
9663
11318
  }
9664
11319
  // Only worth refreshing while the Entity is still drawn by something else, since
9665
- // losing this rego can change which of the survivors wins. With none left there is
9666
- // nothing to restyle, and queueing one per removed Entity is what turns disabling a
9667
- // large menu item into minutes of scheduled work.
11320
+ // losing this rego can change which of the survivors wins.
9668
11321
  if (doesInclude && ((_b = this.rego[entityId]) === null || _b === void 0 ? void 0 : _b.length)) {
9669
11322
  this.queueUpdate({
9670
11323
  entityId: entityId,
9671
11324
  refresh: false
9672
11325
  });
9673
11326
  }
11327
+ (_c = this.labelGroups) === null || _c === void 0 ? void 0 : _c.RefreshEntity(entityId);
9674
11328
  };
9675
11329
  const finish = () => {
9676
11330
  this.releaseMenuItemCollection(menuItemId);
@@ -9701,7 +11355,7 @@
9701
11355
  return;
9702
11356
  }
9703
11357
  exports.EntityLabel.Detatch({
9704
- rego
11358
+ rego, force: true
9705
11359
  });
9706
11360
  // Never tear down the graphic the caller is registering: the renderers hand back
9707
11361
  // the same object when they reuse it, so removing it here destroys what replaces it.
@@ -9720,7 +11374,7 @@
9720
11374
  if (source !== EUpdateSource.TREE_CASCADE) {
9721
11375
  (_b = this.onUpdate) === null || _b === void 0 ? void 0 : _b.Trigger(update);
9722
11376
  }
9723
- this.rego[entityId] = entityRegos.filter(r => r.menuItemId !== menuItemId);
11377
+ this.rego[entityId] = this.rego[entityId].filter(r => r.menuItemId !== menuItemId);
9724
11378
  this.unindexMenuItemEntityIfAbsent(menuItemId, entityId);
9725
11379
  if (doUpdate && menuItemId) {
9726
11380
  this.queueUpdate({
@@ -9729,6 +11383,7 @@
9729
11383
  });
9730
11384
  }
9731
11385
  }
11386
+ (_c = this.labelGroups) === null || _c === void 0 ? void 0 : _c.RefreshEntity(entityId);
9732
11387
  }
9733
11388
  else if (relation) {
9734
11389
  const relationKey = RelationRenderEngine.GetRenderGroupId(relation);
@@ -9740,7 +11395,7 @@
9740
11395
  continue;
9741
11396
  }
9742
11397
  exports.EntityLabel.Detatch({
9743
- rego
11398
+ rego, force: true
9744
11399
  });
9745
11400
  removeEntity(this.viewer, rego.visual);
9746
11401
  rego.visual = null;
@@ -9749,6 +11404,7 @@
9749
11404
  delete this.rego[entityId];
9750
11405
  }
9751
11406
  this.unindexMenuItemEntityIfAbsent(rego.menuItemId, entityId);
11407
+ (_d = this.labelGroups) === null || _d === void 0 ? void 0 : _d.RefreshEntity(entityId);
9752
11408
  const update = {
9753
11409
  type: EVisualUpdateType.Remove,
9754
11410
  entityId: rego.entityId,
@@ -9758,7 +11414,7 @@
9758
11414
  update.source = source;
9759
11415
  }
9760
11416
  if (source !== EUpdateSource.TREE_CASCADE) {
9761
- (_c = this.onUpdate) === null || _c === void 0 ? void 0 : _c.Trigger(update);
11417
+ (_e = this.onUpdate) === null || _e === void 0 ? void 0 : _e.Trigger(update);
9762
11418
  }
9763
11419
  if (doUpdate) {
9764
11420
  this.queueUpdate({
@@ -9777,7 +11433,7 @@
9777
11433
  * @param params
9778
11434
  */
9779
11435
  RemoveRegosByVisuals(params) {
9780
- var _a, _b;
11436
+ var _a, _b, _c;
9781
11437
  const { doRemove = true, requestRender = true, source, doUpdate, menuItemId } = params;
9782
11438
  const removedEntityIds = new Set();
9783
11439
  for (const visual of params.visuals) {
@@ -9792,9 +11448,8 @@
9792
11448
  continue;
9793
11449
  }
9794
11450
  if (rego.visual === visual) {
9795
- // The primary graphic for this rego - remove the rego entirely.
9796
11451
  exports.EntityLabel.Detatch({
9797
- rego
11452
+ rego, force: true
9798
11453
  });
9799
11454
  if (doRemove != false) {
9800
11455
  removeEntity(this.viewer, rego.visual);
@@ -9823,9 +11478,10 @@
9823
11478
  });
9824
11479
  }
9825
11480
  removedEntityIds.add(entityId);
11481
+ (_b = this.labelGroups) === null || _b === void 0 ? void 0 : _b.RefreshEntity(entityId);
9826
11482
  }
9827
11483
  else {
9828
- const siblings = (_b = rego.visual) === null || _b === void 0 ? void 0 : _b["_siblingGraphics"];
11484
+ const siblings = (_c = rego.visual) === null || _c === void 0 ? void 0 : _c["_siblingGraphics"];
9829
11485
  if (siblings === null || siblings === void 0 ? void 0 : siblings.length) {
9830
11486
  if (doRemove != false && siblings.includes(visual)) {
9831
11487
  removeEntity(this.viewer, visual);
@@ -10092,6 +11748,204 @@
10092
11748
  }
10093
11749
  return map;
10094
11750
  }
11751
+ ReleaseHoverLabel(immediate = false) {
11752
+ const label = this.hoverLabel;
11753
+ this.hoverLabel = undefined;
11754
+ if (!label || label.Disposed || label.Persistent) {
11755
+ return;
11756
+ }
11757
+ if (immediate) {
11758
+ exports.EntityLabel.Detatch({ label });
11759
+ return;
11760
+ }
11761
+ label.SetHoverVisible(false);
11762
+ const key = JSON.stringify([label.Rego.entityId, label.Rego.menuItemId || ""]);
11763
+ const previous = this.retiringHoverLabels.get(key);
11764
+ if (previous) {
11765
+ clearTimeout(previous.timer);
11766
+ exports.EntityLabel.Detatch({ label: previous.label });
11767
+ }
11768
+ // Retain briefly beyond the fade to bridge debounced same-target hover updates.
11769
+ const timer = setTimeout(() => {
11770
+ this.retiringHoverLabels.delete(key);
11771
+ exports.EntityLabel.Detatch({ label });
11772
+ }, 240);
11773
+ this.retiringHoverLabels.set(key, { label, timer });
11774
+ }
11775
+ /*
11776
+ * Owns temporary positioning separately from persistent label membership and world anchors.
11777
+ */
11778
+ SetLabelHover(params) {
11779
+ var _a, _b;
11780
+ if (this.disposed) {
11781
+ return;
11782
+ }
11783
+ if (!(params === null || params === void 0 ? void 0 : params.entityId)) {
11784
+ this.ClearLabelHover();
11785
+ return;
11786
+ }
11787
+ if (params.position && (!Number.isFinite(params.position.x) || !Number.isFinite(params.position.y) ||
11788
+ params.position.x < 0 || params.position.y < 0 ||
11789
+ params.position.x >= this.viewer.canvas.clientWidth || params.position.y >= this.viewer.canvas.clientHeight)) {
11790
+ this.ClearLabelHover();
11791
+ return;
11792
+ }
11793
+ 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;
11794
+ if (changed) {
11795
+ this.ClearLabelHover();
11796
+ }
11797
+ clearTimeout(this.hoverRingReset);
11798
+ this.labelHover = {
11799
+ entityId: params.entityId,
11800
+ menuItemId: params.menuItemId,
11801
+ position: params.position ? { ...params.position } : undefined,
11802
+ createIfMissing: params.createIfMissing !== false
11803
+ };
11804
+ const doc = this.viewer.container.ownerDocument;
11805
+ doc.addEventListener("pointermove", this.CheckLabelHoverBounds, true);
11806
+ this.viewer.container.addEventListener("pointerleave", this.CheckLabelHoverBounds);
11807
+ doc.defaultView.addEventListener("blur", this.ClearLabelHover);
11808
+ this.SyncLabelHover();
11809
+ this.viewer.scene.requestRender();
11810
+ }
11811
+ GetLabelHoverPosition(entityId, menuItemId) {
11812
+ const hover = this.labelHover;
11813
+ return (hover === null || hover === void 0 ? void 0 : hover.entityId) === entityId && (hover.menuItemId == null || hover.menuItemId === menuItemId) ? hover.position : undefined;
11814
+ }
11815
+ SyncLabelHover() {
11816
+ var _a, _b, _c, _d;
11817
+ const hover = this.labelHover;
11818
+ if (!hover || this.disposed) {
11819
+ return;
11820
+ }
11821
+ if ((_a = this.hoverLabel) === null || _a === void 0 ? void 0 : _a.Disposed) {
11822
+ this.hoverLabel = undefined;
11823
+ }
11824
+ if (!hover.createIfMissing) {
11825
+ this.ReleaseHoverLabel(true);
11826
+ return;
11827
+ }
11828
+ const group = this.GetLabelGroupFor(hover.entityId, hover.menuItemId);
11829
+ let rego = this.GetRego(hover);
11830
+ const key = JSON.stringify([hover.entityId, hover.menuItemId || ""]);
11831
+ const retiring = this.retiringHoverLabels.get(key);
11832
+ if (retiring) {
11833
+ clearTimeout(retiring.timer);
11834
+ this.retiringHoverLabels.delete(key);
11835
+ if (!retiring.label.Disposed) {
11836
+ this.hoverLabel = retiring.label;
11837
+ }
11838
+ }
11839
+ const existing = rego && exports.EntityLabel.GetLabel({ rego });
11840
+ const state = this.GetState(hover);
11841
+ if ((!rego && !group) || state.hidden || ((_b = rego === null || rego === void 0 ? void 0 : rego.visual) === null || _b === void 0 ? void 0 : _b.show) === false ||
11842
+ (this.GetIsIsolatedAny() && !state.isolated && this.GetIsolationOpacity() == null) ||
11843
+ (existing && existing !== this.hoverLabel && existing.HasText) ||
11844
+ (group && group.options.presentation !== "icon")) {
11845
+ this.ReleaseHoverLabel(true);
11846
+ return;
11847
+ }
11848
+ if (this.hoverLabel && !this.hoverLabel.Disposed) {
11849
+ this.hoverLabel.SetHoverVisible(true);
11850
+ return;
11851
+ }
11852
+ // Query-only icon groups have coordinates but no registered scene visual to attach to.
11853
+ if (!rego && group && [group.item.longitude, group.item.latitude, (_c = group.item.altitude) !== null && _c !== void 0 ? _c : 0].every(Number.isFinite)) {
11854
+ rego = {
11855
+ entityId: hover.entityId,
11856
+ menuItemId: hover.menuItemId,
11857
+ name: group.options.text,
11858
+ priority: 0,
11859
+ visual: new Cesium.Entity({
11860
+ position: Cesium.Cartesian3.fromDegrees(group.item.longitude, group.item.latitude, group.item.altitude || 0),
11861
+ point: {
11862
+ pixelSize: 0,
11863
+ heightReference: Cesium.HeightReference.NONE
11864
+ }
11865
+ })
11866
+ };
11867
+ }
11868
+ if (!rego) {
11869
+ return;
11870
+ }
11871
+ const params = {
11872
+ rego,
11873
+ viewer: this.viewer,
11874
+ visualRegister: this,
11875
+ api: (_d = this.ApiGetters) === null || _d === void 0 ? void 0 : _d.GetBruceApi(),
11876
+ persistent: false,
11877
+ transient: true,
11878
+ hoverRingState: this.hoverRingState,
11879
+ options: {
11880
+ presentation: "text",
11881
+ text: group === null || group === void 0 ? void 0 : group.options.text,
11882
+ selectOnClick: false
11883
+ },
11884
+ visible: () => {
11885
+ var _a, _b;
11886
+ const current = this.GetState(hover);
11887
+ return (!this.disposed && !current.hidden && ((_a = rego.visual) === null || _a === void 0 ? void 0 : _a.show) !== false &&
11888
+ Boolean(this.GetRego(hover) || ((_b = this.labelGroups) === null || _b === void 0 ? void 0 : _b.Has(hover.entityId, hover.menuItemId))) &&
11889
+ (!this.GetIsIsolatedAny() || current.isolated || this.GetIsolationOpacity() != null));
11890
+ }
11891
+ };
11892
+ this.hoverLabel = existing || group ? new exports.EntityLabel.Label(params) : exports.EntityLabel.Attach(params);
11893
+ }
11894
+ SetLabelContainer(container) {
11895
+ if (this.disposed) {
11896
+ return;
11897
+ }
11898
+ exports.EntityLabel.SetContainer(this.viewer, container);
11899
+ }
11900
+ ClearLabelContainer() {
11901
+ this.SetLabelContainer();
11902
+ }
11903
+ /*
11904
+ * Adds source-driven membership without changing manual labelled state or legacy bookmark snapshots.
11905
+ */
11906
+ SetLabelGroup(group) {
11907
+ if (this.disposed) {
11908
+ return Promise.reject(new Error("Visual register is disposed"));
11909
+ }
11910
+ if (!this.labelGroups) {
11911
+ this.labelGroups = new exports.EntityLabelGroup.Manager(this, () => this.labelGroupsChanged.Trigger(undefined));
11912
+ }
11913
+ return this.labelGroups.Set(group);
11914
+ }
11915
+ RemoveLabelGroup(id) {
11916
+ var _a;
11917
+ (_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.Remove(id);
11918
+ }
11919
+ RefreshLabelGroup(id) {
11920
+ var _a, _b;
11921
+ if (this.disposed) {
11922
+ return Promise.reject(new Error("Visual register is disposed"));
11923
+ }
11924
+ 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}`));
11925
+ }
11926
+ SetLabelGroupVisible(id, visible) {
11927
+ var _a, _b;
11928
+ return (_b = (_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.SetVisible(id, visible)) !== null && _b !== void 0 ? _b : false;
11929
+ }
11930
+ ClearLabelGroups() {
11931
+ var _a;
11932
+ (_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.Clear();
11933
+ }
11934
+ GetLabelGroups() {
11935
+ var _a;
11936
+ return ((_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.Get()) || [];
11937
+ }
11938
+ GetLabelGroupFor(entityId, menuItemId) {
11939
+ var _a;
11940
+ return (_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.Resolve(entityId, menuItemId);
11941
+ }
11942
+ RefreshLabelGroupEntity(entityId) {
11943
+ var _a, _b;
11944
+ (_a = this.labelGroups) === null || _a === void 0 ? void 0 : _a.RefreshEntity(entityId);
11945
+ if (((_b = this.labelHover) === null || _b === void 0 ? void 0 : _b.entityId) === entityId) {
11946
+ this.SyncLabelHover();
11947
+ }
11948
+ }
10095
11949
  }
10096
11950
  VisualsRegister.Register = Register;
10097
11951
  })(exports.VisualsRegister || (exports.VisualsRegister = {}));
@@ -10218,6 +12072,8 @@
10218
12072
  }
10219
12073
  class Point {
10220
12074
  constructor(lon, lat, id) {
12075
+ this.radius = 0;
12076
+ this.minDistance = 0;
10221
12077
  this.lon = lon;
10222
12078
  this.lat = lat;
10223
12079
  this.id = id;
@@ -10235,8 +12091,8 @@
10235
12091
  let xDist = Math.abs(range.x - this.x);
10236
12092
  let yDist = Math.abs(range.y - this.y);
10237
12093
  let r = this.radius;
10238
- let w = range.width;
10239
- let h = range.height;
12094
+ let w = range.width / 2;
12095
+ let h = range.height / 2;
10240
12096
  if (xDist > (w + r) || yDist > (h + r)) {
10241
12097
  return false;
10242
12098
  }
@@ -10250,8 +12106,7 @@
10250
12106
  Contains(point) {
10251
12107
  const dx = this.x - point.lon;
10252
12108
  const dy = this.y - point.lat;
10253
- const distance = Math.sqrt(dx * dx + dy * dy);
10254
- return distance <= this.radius;
12109
+ return dx * dx + dy * dy <= this.radius * this.radius;
10255
12110
  }
10256
12111
  }
10257
12112
  class Rectangle {
@@ -10288,13 +12143,13 @@
10288
12143
  let y = this.boundary.y;
10289
12144
  let w = this.boundary.w / 2;
10290
12145
  let h = this.boundary.h / 2;
10291
- let ne = new Rectangle(x + w / 2, y - h / 2, w, h);
12146
+ let ne = new Rectangle(x + w, y - h, w, h);
10292
12147
  this.northeast = new Quad(ne, this.capacity);
10293
- let nw = new Rectangle(x - w / 2, y - h / 2, w, h);
12148
+ let nw = new Rectangle(x - w, y - h, w, h);
10294
12149
  this.northwest = new Quad(nw, this.capacity);
10295
- let se = new Rectangle(x + w / 2, y + h / 2, w, h);
12150
+ let se = new Rectangle(x + w, y + h, w, h);
10296
12151
  this.southeast = new Quad(se, this.capacity);
10297
- let sw = new Rectangle(x - w / 2, y + h / 2, w, h);
12152
+ let sw = new Rectangle(x - w, y + h, w, h);
10298
12153
  this.southwest = new Quad(sw, this.capacity);
10299
12154
  this.divided = true;
10300
12155
  }
@@ -10302,7 +12157,8 @@
10302
12157
  if (!this.boundary.Contains(point)) {
10303
12158
  return false;
10304
12159
  }
10305
- if (this.points.length < this.capacity) {
12160
+ // Coincident points must terminate in a bucket rather than subdividing indefinitely.
12161
+ if (this.points.length < this.capacity || this.boundary.w < 1e-10) {
10306
12162
  this.points.push(point);
10307
12163
  return true;
10308
12164
  }
@@ -10532,505 +12388,640 @@
10532
12388
  }
10533
12389
  return null;
10534
12390
  }
10535
- const FORCE_UPDATE_BATCH_SIZE = 1000;
10536
- const FORCE_UPDATE_BATCH_DELAY = 100;
10537
- class PointClustering {
10538
- constructor(register, menuItemId, settings) {
10539
- this.minDistance = 5000;
10540
- this.minTotalCount = 0;
10541
- this.disposed = false;
10542
- this.registeredEntityIds = new Set();
10543
- // Queue to force update entities.
10544
- this.updateEntityQueue = [];
10545
- this.register = register;
10546
- this.viewer = register.Viewer;
10547
- this.menuItemId = menuItemId;
10548
- let boundary = new Rectangle(0, 0, 360, 180);
10549
- this.quadTree = new Quad(boundary, 30);
10550
- this.prevClusteredEntities = new Set();
10551
- this.currClusteredEntities = new Set();
10552
- this.clusterEntities = new Map();
10553
- this.updateQueue = new BModels.DelayQueue(() => {
10554
- this.doUpdate();
10555
- }, 1000);
10556
- this.listenCamera();
10557
- this.listenSelection();
10558
- if (settings) {
10559
- if (!isNaN(+settings.minDistance)) {
10560
- this.minDistance = +settings.minDistance;
10561
- }
10562
- if (!isNaN(+settings.minTotalCount)) {
10563
- this.minTotalCount = +settings.minTotalCount;
10564
- }
10565
- }
10566
- }
10567
- Update() {
10568
- this.updateQueue.Call();
12391
+ const clusterPickers = new WeakMap();
12392
+ /*
12393
+ * One capture listener set per viewer prevents host canvas picking from undoing cluster selection.
12394
+ */
12395
+ function getClusterPicker(viewer) {
12396
+ let picker = clusterPickers.get(viewer);
12397
+ if (picker) {
12398
+ return picker;
10569
12399
  }
10570
- queueForceUpdate(entityIds) {
10571
- for (let i = 0; i < entityIds.length; i++) {
10572
- if (this.updateEntityQueue.includes(entityIds[i])) {
10573
- continue;
12400
+ const canvas = viewer.canvas;
12401
+ picker = {
12402
+ owners: new Set(),
12403
+ targets: new Map(),
12404
+ remove: () => { }
12405
+ };
12406
+ let gesture;
12407
+ let released;
12408
+ const handler = (event) => {
12409
+ if (viewer.isDestroyed())
12410
+ return;
12411
+ const stop = () => { event.preventDefault(); event.stopImmediatePropagation(); };
12412
+ if (event.type === "pointermove") {
12413
+ if (gesture && Math.hypot(event.clientX - gesture.x, event.clientY - gesture.y) > 6) {
12414
+ gesture.moved = true;
10574
12415
  }
10575
- this.updateEntityQueue.push(entityIds[i]);
12416
+ return;
10576
12417
  }
10577
- this.runForceUpdateQueue();
10578
- }
10579
- runForceUpdateQueue() {
10580
- if (!this.updateEntityQueue.length) {
12418
+ if (event.type === "pointercancel") {
12419
+ if (gesture) {
12420
+ released = { ...gesture, moved: true };
12421
+ gesture = null;
12422
+ if (released.target) {
12423
+ stop();
12424
+ }
12425
+ }
10581
12426
  return;
10582
12427
  }
10583
- if (this.queueInterval) {
12428
+ if (event.button !== 0) {
10584
12429
  return;
10585
12430
  }
10586
- this.queueInterval = setInterval(() => {
10587
- if (this.disposed) {
10588
- clearInterval(this.queueInterval);
10589
- this.queueInterval = null;
12431
+ if (event.type === "pointerup" || event.type === "mouseup") {
12432
+ // Only consume releases whose press we owned, even if the cursor left the billboard.
12433
+ if (!gesture) {
10590
12434
  return;
10591
12435
  }
10592
- const ids = this.updateEntityQueue.splice(0, FORCE_UPDATE_BATCH_SIZE);
10593
- this.register.ForceUpdate({
10594
- entityIds: ids
10595
- });
10596
- if (!this.updateEntityQueue.length) {
10597
- clearInterval(this.queueInterval);
10598
- this.queueInterval = null;
12436
+ gesture.moved || (gesture.moved = Math.hypot(event.clientX - gesture.x, event.clientY - gesture.y) > 6);
12437
+ released = gesture;
12438
+ gesture = null;
12439
+ if (released.target) {
12440
+ stop();
10599
12441
  }
10600
- }, FORCE_UPDATE_BATCH_DELAY);
10601
- }
10602
- /**
10603
- * Starts listening to camera changes.
10604
- * This will trigger the clustering update whenever camera is moved.
10605
- */
10606
- listenCamera() {
10607
- const viewer = this.viewer;
10608
- let lastPos3d;
10609
- const hasMoved = () => {
10610
- var _a;
10611
- const cameraPos = (_a = viewer === null || viewer === void 0 ? void 0 : viewer.camera) === null || _a === void 0 ? void 0 : _a.positionCartographic;
10612
- if (!cameraPos) {
10613
- return false;
12442
+ return;
12443
+ }
12444
+ const rect = canvas.getBoundingClientRect();
12445
+ const picked = viewer.scene.pick(new Cesium.Cartesian2(event.clientX - rect.left, event.clientY - rect.top));
12446
+ const select = picker.targets.get(picked === null || picked === void 0 ? void 0 : picked.id);
12447
+ if (event.type === "pointerdown" || event.type === "mousedown") {
12448
+ released = null;
12449
+ gesture = { target: select ? picked.id : null, x: event.clientX, y: event.clientY, moved: false };
12450
+ if (gesture.target) {
12451
+ stop();
10614
12452
  }
10615
- const pos3d = Cesium.Cartographic.toCartesian(cameraPos);
10616
- if (!lastPos3d) {
10617
- lastPos3d = pos3d;
10618
- return false;
12453
+ return;
12454
+ }
12455
+ if (released) {
12456
+ const owned = !!released.target;
12457
+ const invalid = released.moved || released.target !== (picked === null || picked === void 0 ? void 0 : picked.id);
12458
+ released = null;
12459
+ if (!owned) {
12460
+ return;
10619
12461
  }
10620
- const distance = Cesium.Cartesian3.distance(pos3d, lastPos3d);
10621
- lastPos3d = pos3d;
10622
- // 50 meter movement at least.
10623
- return distance > 50;
10624
- };
10625
- this.listenCameraRemoval = viewer.scene.camera.changed.addEventListener(() => {
10626
- if (hasMoved()) {
10627
- this.updateQueue.Call();
12462
+ if (invalid) {
12463
+ stop();
12464
+ return;
10628
12465
  }
10629
- });
10630
- }
10631
- /**
10632
- * Stops listening to camera changes.
10633
- */
10634
- unlistenCamera() {
10635
- if (this.listenCameraRemoval) {
10636
- this.listenCameraRemoval();
10637
- this.listenCameraRemoval = null;
10638
12466
  }
12467
+ if (!select) {
12468
+ return;
12469
+ }
12470
+ stop();
12471
+ if (event.type === "click") {
12472
+ select();
12473
+ }
12474
+ };
12475
+ const types = ["pointerdown", "pointermove", "pointerup", "pointercancel", "mousedown", "mouseup", "click", "dblclick"];
12476
+ if (canvas === null || canvas === void 0 ? void 0 : canvas.addEventListener) {
12477
+ for (const type of types) {
12478
+ canvas.addEventListener(type, handler, true);
12479
+ }
12480
+ picker.remove = () => {
12481
+ for (const type of types) {
12482
+ canvas.removeEventListener(type, handler, true);
12483
+ }
12484
+ };
10639
12485
  }
10640
- /**
10641
- * Starts listening to entity selection changes.
10642
- * Selected entities are not clustered.
10643
- */
10644
- listenSelection() {
10645
- this.listenUpdateRemoval = this.register.OnUpdate.Subscribe((data) => {
10646
- if (data.type == exports.VisualsRegister.EVisualUpdateType.DeselectAll) {
10647
- this.updateQueue.Call();
12486
+ clusterPickers.set(viewer, picker);
12487
+ return picker;
12488
+ }
12489
+ /*
12490
+ * Count tiers are bounded so large clusters do not cover the scene with enormous billboards.
12491
+ */
12492
+ function PointClusterSize(count) {
12493
+ return count >= 10000 ? 140 : count >= 1000 ? 120 : count >= 100 ? 90 : count >= 10 ? 80 : 70;
12494
+ }
12495
+ class PointClustering {
12496
+ constructor(register, menuItemId, settings = {}) {
12497
+ this.register = register;
12498
+ this.menuItemId = menuItemId;
12499
+ this.settings = settings;
12500
+ this.minDistance = 5000;
12501
+ this.minTotalCount = 0;
12502
+ this.currClusteredEntities = new Set();
12503
+ this.clusterEntities = new Map();
12504
+ this.clusterViews = new Map();
12505
+ this.registeredEntityIds = new Set();
12506
+ this.records = new Map();
12507
+ this.disposed = false;
12508
+ this.removals = [];
12509
+ this.selectionQueued = false;
12510
+ this.viewer = register.Viewer;
12511
+ this.settings = settings = settings || {};
12512
+ if (Number.isFinite(+settings.minDistance)) {
12513
+ this.minDistance = Math.max(0, +settings.minDistance);
12514
+ }
12515
+ if (Number.isFinite(+settings.minTotalCount)) {
12516
+ this.minTotalCount = Math.max(0, +settings.minTotalCount);
12517
+ }
12518
+ this.quadTree = new Quad(new Rectangle(0, 0, 360, 180), 30);
12519
+ this.updateQueue = new BModels.DelayQueue(() => this.doUpdate(), 1000);
12520
+ this.picker = getClusterPicker(this.viewer);
12521
+ this.picker.owners.add(this);
12522
+ this.removals.push(this.viewer.scene.camera.changed.addEventListener(() => this.Update()));
12523
+ this.removals.push(this.viewer.scene.terrainProviderChanged.addEventListener(() => this.Update()));
12524
+ this.removals.push(register.OnUpdate.Subscribe(data => {
12525
+ if (data.type === exports.VisualsRegister.EVisualUpdateType.DeselectAll ||
12526
+ (data.entityId && this.registeredEntityIds.has(data.entityId) && data.selected != null)) {
12527
+ if (!this.selectionQueued) {
12528
+ this.selectionQueued = true;
12529
+ Promise.resolve().then(() => {
12530
+ this.selectionQueued = false;
12531
+ if (!this.disposed)
12532
+ this.refreshSelection();
12533
+ });
12534
+ }
10648
12535
  }
10649
- else if (data.entityId &&
10650
- data.selected != null &&
10651
- data.type == exports.VisualsRegister.EVisualUpdateType.Update &&
10652
- this.registeredEntityIds.has(data.entityId)) {
10653
- this.updateQueue.Call();
12536
+ if ("isolated" in data || (data.entityId && this.registeredEntityIds.has(data.entityId) && "hidden" in data)) {
12537
+ this.Update();
10654
12538
  }
10655
- });
12539
+ }));
10656
12540
  }
10657
- unlistenSelection() {
10658
- if (this.listenUpdateRemoval) {
10659
- this.listenUpdateRemoval();
10660
- this.listenUpdateRemoval = null;
12541
+ Update() {
12542
+ if (!this.disposed) {
12543
+ this.updateQueue.Call();
10661
12544
  }
10662
12545
  }
10663
- /**
10664
- * Removes all clusters and updates entities to no longer be suppressed.
10665
- * @returns
10666
- */
12546
+ GetClusters() {
12547
+ return Array.from(this.clusterViews.values(), view => ({ ...view, entityIds: [...view.entityIds] }));
12548
+ }
10667
12549
  Dispose() {
10668
12550
  if (this.disposed) {
10669
12551
  return;
10670
12552
  }
10671
- for (let [, entity] of Array.from(this.clusterEntities)) {
10672
- this.viewer.entities.remove(entity);
10673
- }
10674
- this.clusterEntities.clear();
10675
- this.unlistenCamera();
10676
- this.unlistenSelection();
10677
12553
  this.disposed = true;
10678
- this.updateEntityQueue = [];
10679
- // Restore entities.
10680
- const toUpdateIds = [];
10681
- for (let id of Array.from(this.registeredEntityIds)) {
10682
- const rego = this.register.GetRego({
10683
- entityId: id,
10684
- menuItemId: this.menuItemId
10685
- });
10686
- if (rego && rego.overrideShow) {
12554
+ this.updateQueue.Dispose();
12555
+ for (const remove of this.removals) {
12556
+ remove();
12557
+ }
12558
+ for (const id of this.clusterEntities.keys()) {
12559
+ this.removeClusterEntity(id);
12560
+ }
12561
+ this.picker.owners.delete(this);
12562
+ if (!this.picker.owners.size) {
12563
+ this.picker.remove();
12564
+ clusterPickers.delete(this.viewer);
12565
+ }
12566
+ const restore = [];
12567
+ for (const id of this.registeredEntityIds) {
12568
+ const rego = this.register.GetRego({ entityId: id, menuItemId: this.menuItemId });
12569
+ if ((rego === null || rego === void 0 ? void 0 : rego.overrideShow) === false) {
10687
12570
  rego.overrideShow = null;
10688
- toUpdateIds.push(id);
12571
+ restore.push(id);
10689
12572
  }
10690
12573
  }
10691
- if (toUpdateIds.length) {
10692
- this.register.ForceUpdate({
10693
- entityIds: toUpdateIds,
10694
- });
12574
+ this.records.clear();
12575
+ this.registeredEntityIds.clear();
12576
+ this.currClusteredEntities.clear();
12577
+ if (restore.length) {
12578
+ this.register.ForceUpdate({ entityIds: restore });
10695
12579
  }
10696
12580
  }
10697
- /**
10698
- * Calculates center of given points.
10699
- * @param points
10700
- * @returns
10701
- */
10702
- calculateCentroid(points) {
10703
- let lonSum = 0;
10704
- let latSum = 0;
10705
- for (let point of points) {
10706
- lonSum += point.lon;
10707
- latSum += point.lat;
12581
+ refreshSelection() {
12582
+ for (const view of this.clusterViews.values()) {
12583
+ const selected = view.entityIds.length > 0 && view.entityIds.every(entityId => this.register.GetIsSelected({ entityId, menuItemId: this.menuItemId }));
12584
+ if (selected === view.selected)
12585
+ continue;
12586
+ view.selected = selected;
12587
+ if (selected)
12588
+ exports.CesiumEntityStyler.Select({ viewer: this.viewer, entity: view.entity, requestRender: false });
12589
+ else
12590
+ exports.CesiumEntityStyler.Deselect({ viewer: this.viewer, entity: view.entity, requestRender: false });
10708
12591
  }
10709
- const lonAvg = lonSum / points.length;
10710
- const latAvg = latSum / points.length;
10711
- return new Point(lonAvg, latAvg, "");
12592
+ this.viewer.scene.requestRender();
10712
12593
  }
10713
- /**
10714
- * Gathers current clusters and renders them.
10715
- * @returns
10716
- */
10717
- doUpdate() {
10718
- if (this.disposed) {
12594
+ selectCluster(clusterId) {
12595
+ const view = this.clusterViews.get(clusterId);
12596
+ if (this.disposed || !view)
10719
12597
  return;
12598
+ this.register.ClearSelected({ requestRender: false });
12599
+ this.register.SetSelected({ entityIds: view.entityIds, selected: true });
12600
+ this.refreshSelection();
12601
+ }
12602
+ resolveGeometry(visual, seen = new Set()) {
12603
+ var _a, _b, _c;
12604
+ if (!visual || seen.has(visual)) {
12605
+ return null;
10720
12606
  }
10721
- // 1: Update precision.
10722
- // This defines how far apart these clusters can be.
10723
- const cameraHeight = this.viewer.camera.positionCartographic.height;
10724
- this.getClusterSpacing(cameraHeight);
10725
- // 2: Get clusters.
10726
- const { clusters, noLongerClustered } = this.getClusters();
10727
- const entitiesToUpdate = [];
10728
- // 3: Remove all cesium cluster entities that are no longer clustered.
10729
- for (let id of Array.from(noLongerClustered)) {
10730
- this.removeClusterEntity(id);
12607
+ seen.add(visual);
12608
+ const value = (property) => GetValue(this.viewer, property);
12609
+ const valid = (p) => p && [p.x, p.y, p.z].every(Number.isFinite) && Cesium.Cartesian3.magnitudeSquared(p) > 0;
12610
+ const kind = visual.model || visual.box || visual.cylinder || visual.ellipsoid ? "model" :
12611
+ visual.polygon || visual.rectangle || visual.ellipse ? "polygon" :
12612
+ visual.polyline || visual.corridor || visual.wall || visual.polylineVolume ? "polyline" :
12613
+ visual.point || visual.billboard || visual.label ? "point" : "other";
12614
+ const configured = (_a = this.settings.minDistanceByType) === null || _a === void 0 ? void 0 : _a[kind];
12615
+ let minDistance = Number.isFinite(configured) ? Math.max(0, configured) : this.minDistance * (kind === "point" || kind === "other" ? 1 : 2);
12616
+ const graphic = (visual.model || visual.polygon || visual.rectangle || visual.ellipse || visual.polyline ||
12617
+ visual.corridor || visual.wall || visual.polylineVolume || visual.billboard || visual.point || visual.label || visual.box || visual.cylinder || visual.ellipsoid);
12618
+ let heightRef = (_b = value(graphic === null || graphic === void 0 ? void 0 : graphic.heightReference)) !== null && _b !== void 0 ? _b : Cesium.HeightReference.NONE;
12619
+ if (visual.polyline) {
12620
+ heightRef = value(visual.polyline.clampToGround) ? Cesium.HeightReference.CLAMP_TO_GROUND : Cesium.HeightReference.NONE;
12621
+ }
12622
+ let positions;
12623
+ let height;
12624
+ if (visual.polygon) {
12625
+ const hierarchy = value(visual.polygon.hierarchy);
12626
+ positions = hierarchy === null || hierarchy === void 0 ? void 0 : hierarchy.positions;
12627
+ if (!value(visual.polygon.perPositionHeight)) {
12628
+ height = value(visual.polygon.height);
12629
+ if (height == null && value(visual.polygon.extrudedHeight) == null) {
12630
+ heightRef = Cesium.HeightReference.CLAMP_TO_GROUND;
12631
+ }
12632
+ height !== null && height !== void 0 ? height : (height = 0);
12633
+ }
12634
+ }
12635
+ else if (visual.rectangle) {
12636
+ const rectangle = value(visual.rectangle.coordinates);
12637
+ if (rectangle) {
12638
+ positions = [Cesium.Rectangle.northwest(rectangle), Cesium.Rectangle.northeast(rectangle),
12639
+ Cesium.Rectangle.southwest(rectangle), Cesium.Rectangle.southeast(rectangle)].map(c => Cesium.Cartographic.toCartesian(c));
12640
+ }
12641
+ height = value(visual.rectangle.height);
12642
+ if (height == null && value(visual.rectangle.extrudedHeight) == null) {
12643
+ heightRef = Cesium.HeightReference.CLAMP_TO_GROUND;
12644
+ }
12645
+ height !== null && height !== void 0 ? height : (height = 0);
12646
+ }
12647
+ else if (visual.polyline || visual.corridor || visual.wall || visual.polylineVolume) {
12648
+ positions = value(graphic.positions);
12649
+ if (visual.wall && positions) {
12650
+ const minimum = value(visual.wall.minimumHeights);
12651
+ const maximum = value(visual.wall.maximumHeights);
12652
+ positions = positions.flatMap((position, index) => {
12653
+ var _a, _b;
12654
+ if (!valid(position)) {
12655
+ return [];
12656
+ }
12657
+ const carto = Cesium.Cartographic.fromCartesian(position);
12658
+ if (!carto) {
12659
+ return [];
12660
+ }
12661
+ 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]
12662
+ .filter(Number.isFinite).map(height => Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, height)));
12663
+ });
12664
+ }
10731
12665
  }
10732
- // 4: Stop hiding entities that are no longer clustered.
10733
- const previousEntityIds = new Set(this.registeredEntityIds);
10734
- for (let id of Array.from(previousEntityIds)) {
10735
- if (!this.currClusteredEntities.has(id)) {
10736
- const rego = this.register.GetRego({
10737
- entityId: id,
10738
- menuItemId: this.menuItemId
12666
+ if (visual.corridor || visual.ellipse) {
12667
+ height = value(graphic.height);
12668
+ if (height == null && value(graphic.extrudedHeight) == null)
12669
+ heightRef = Cesium.HeightReference.CLAMP_TO_GROUND;
12670
+ height !== null && height !== void 0 ? height : (height = 0);
12671
+ }
12672
+ if (!(positions === null || positions === void 0 ? void 0 : positions.length)) {
12673
+ const position = value(visual.position);
12674
+ if (valid(position)) {
12675
+ positions = [position];
12676
+ }
12677
+ }
12678
+ const children = visual._siblingGraphics || [];
12679
+ // Container entities often copy their first child's position; don't count that anchor twice.
12680
+ if (!graphic && children.length) {
12681
+ positions = [];
12682
+ minDistance = 0;
12683
+ }
12684
+ const resolved = [];
12685
+ for (const position of positions || []) {
12686
+ if (!valid(position))
12687
+ continue;
12688
+ let source = position;
12689
+ if (height != null) {
12690
+ const carto = Cesium.Cartographic.fromCartesian(position);
12691
+ if (!carto)
12692
+ continue;
12693
+ source = Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, height);
12694
+ }
12695
+ const actual = exports.DrawingUtils.EnsurePosHeight({
12696
+ viewer: this.viewer,
12697
+ pos3d: source,
12698
+ desiredHeightRef: Cesium.HeightReference.NONE,
12699
+ heightRef
12700
+ });
12701
+ if (valid(actual)) {
12702
+ resolved.push(actual);
12703
+ }
12704
+ }
12705
+ const extrudedHeight = value(graphic === null || graphic === void 0 ? void 0 : graphic.extrudedHeight);
12706
+ if (Number.isFinite(extrudedHeight)) {
12707
+ const reference = (_c = value(graphic.extrudedHeightReference)) !== null && _c !== void 0 ? _c : Cesium.HeightReference.NONE;
12708
+ for (const position of positions || []) {
12709
+ if (!valid(position)) {
12710
+ continue;
12711
+ }
12712
+ const carto = Cesium.Cartographic.fromCartesian(position);
12713
+ if (!carto) {
12714
+ continue;
12715
+ }
12716
+ const actual = exports.DrawingUtils.EnsurePosHeight({
12717
+ viewer: this.viewer,
12718
+ pos3d: Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, extrudedHeight),
12719
+ desiredHeightRef: Cesium.HeightReference.NONE, heightRef: reference
10739
12720
  });
10740
- if (rego && rego.overrideShow != null) {
10741
- rego.overrideShow = null;
10742
- entitiesToUpdate.push(id);
12721
+ if (valid(actual)) {
12722
+ resolved.push(actual);
10743
12723
  }
10744
12724
  }
10745
12725
  }
10746
- const getScale = (count) => {
10747
- // const MIN_SCALE = 15;
10748
- // const MAX_SCALE = 80;
10749
- // const SCALE_PER_POINT = 1.01;
10750
- // let scale = MIN_SCALE + (count * SCALE_PER_POINT);
10751
- // scale = Math.min(scale, MAX_SCALE);
10752
- // return scale;
10753
- if (count >= 10000) {
10754
- return 140;
12726
+ const spheres = [];
12727
+ const anchors = [];
12728
+ if (resolved.length) {
12729
+ const sphere = Cesium.BoundingSphere.fromPoints(resolved);
12730
+ // A chord midpoint can lie underground. Preserve the average rendered vertex altitude.
12731
+ const center = Cesium.Cartographic.fromCartesian(sphere.center);
12732
+ if (center) {
12733
+ const altitude = resolved.reduce((sum, p) => sum + Cesium.Cartographic.fromCartesian(p).height, 0) / resolved.length;
12734
+ const position = Cesium.Cartesian3.fromRadians(center.longitude, center.latitude, altitude);
12735
+ sphere.radius += Cesium.Cartesian3.distance(position, sphere.center);
12736
+ sphere.center = position;
12737
+ }
12738
+ if (visual.ellipse) {
12739
+ sphere.radius = Math.max(sphere.radius, value(visual.ellipse.semiMajorAxis) || 0, value(visual.ellipse.semiMinorAxis) || 0);
12740
+ }
12741
+ if (visual.box) {
12742
+ sphere.radius = Math.max(sphere.radius, Cesium.Cartesian3.magnitude(value(visual.box.dimensions) || Cesium.Cartesian3.ZERO) / 2);
10755
12743
  }
10756
- if (count >= 1000) {
10757
- return 120;
12744
+ if (visual.ellipsoid) {
12745
+ sphere.radius = Math.max(sphere.radius, Cesium.Cartesian3.maximumComponent(value(visual.ellipsoid.radii) || Cesium.Cartesian3.ZERO));
10758
12746
  }
10759
- if (count >= 100) {
10760
- return 90;
12747
+ if (visual.cylinder) {
12748
+ 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)));
10761
12749
  }
10762
- if (count >= 10) {
10763
- return 80;
12750
+ if (visual.corridor) {
12751
+ sphere.radius += (value(visual.corridor.width) || 0) / 2;
10764
12752
  }
10765
- return 70;
12753
+ if (visual.polylineVolume) {
12754
+ const shape = value(visual.polylineVolume.shape) || [];
12755
+ sphere.radius += shape.reduce((radius, p) => Math.max(radius, Math.hypot(p.x, p.y)), 0);
12756
+ }
12757
+ spheres.push(sphere);
12758
+ anchors.push(sphere.center);
12759
+ }
12760
+ for (const child of children) {
12761
+ const result = this.resolveGeometry(child, seen);
12762
+ if (!result) {
12763
+ continue;
12764
+ }
12765
+ spheres.push(new Cesium.BoundingSphere(result.position, result.radius));
12766
+ anchors.push(result.position);
12767
+ minDistance = Math.max(minDistance, result.minDistance);
12768
+ }
12769
+ if (!spheres.length) {
12770
+ return null;
12771
+ }
12772
+ const sphere = spheres.length === 1 ? spheres[0] : Cesium.BoundingSphere.fromBoundingSpheres(spheres);
12773
+ if (!valid(sphere.center) || !Number.isFinite(sphere.radius)) {
12774
+ return null;
12775
+ }
12776
+ if (spheres.length > 1) {
12777
+ const carto = Cesium.Cartographic.fromCartesian(sphere.center);
12778
+ if (!carto) {
12779
+ return null;
12780
+ }
12781
+ const height = anchors.reduce((sum, p) => sum + Cesium.Cartographic.fromCartesian(p).height, 0) / anchors.length;
12782
+ const position = Cesium.Cartesian3.fromRadians(carto.longitude, carto.latitude, height);
12783
+ sphere.radius += Cesium.Cartesian3.distance(position, sphere.center);
12784
+ sphere.center = position;
12785
+ }
12786
+ return { position: sphere.center, radius: sphere.radius, minDistance };
12787
+ }
12788
+ appearance(id) {
12789
+ var _a, _b, _c, _d, _e, _f;
12790
+ const { visual, metadata } = this.records.get(id);
12791
+ const data = metadata.entity || { Bruce: { ID: id } };
12792
+ let priority;
12793
+ let status;
12794
+ try {
12795
+ priority = Array.isArray(this.settings.iconPriority) ? BModels.Calculator.GetNumber(this.settings.iconPriority, data) : this.settings.iconPriority;
12796
+ status = Array.isArray(this.settings.iconStatus) ? BModels.Calculator.GetString(this.settings.iconStatus, data) : this.settings.iconStatus;
12797
+ }
12798
+ catch {
12799
+ priority = undefined;
12800
+ status = undefined;
12801
+ }
12802
+ const rawImage = GetValue(this.viewer, (_a = visual.billboard) === null || _a === void 0 ? void 0 : _a.image);
12803
+ const imageKey = (_b = visual.billboard) === null || _b === void 0 ? void 0 : _b._billboardImgKey;
12804
+ const iconUrl = (_c = metadata.iconUrl) !== null && _c !== void 0 ? _c : (typeof rawImage === "string" ? rawImage : undefined);
12805
+ // Original URLs retain status tokens after the point renderer converts an image into a data URI.
12806
+ const inferenceUrl = (_e = (_d = metadata.iconUrl) !== null && _d !== void 0 ? _d : imageKey) !== null && _e !== void 0 ? _e : iconUrl;
12807
+ const colorGraphic = visual.point || visual.polygon || visual.polyline || visual.corridor || visual.ellipse || visual.model;
12808
+ const color = colorGraphic ? exports.CesiumEntityStyler.GetDefaultColor(this.viewer, colorGraphic) : null;
12809
+ let colorStatus;
12810
+ if (color && color.alpha > 0 && color.red > 0.5 && color.blue < 0.4) {
12811
+ colorStatus = color.green > 0.4 ? "warning" : "red";
12812
+ }
12813
+ const bgColor = color === null || color === void 0 ? void 0 : color.toCssColorString();
12814
+ return {
12815
+ priority: GetClusterIconPriority({
12816
+ iconPriority: Number.isFinite(priority) ? priority : metadata.iconPriority,
12817
+ iconStatus: (_f = status !== null && status !== void 0 ? status : metadata.iconStatus) !== null && _f !== void 0 ? _f : colorStatus, icon: metadata.icon, iconUrl: inferenceUrl
12818
+ }),
12819
+ iconUrl, bgColor,
12820
+ txtColor: color && color.red * 0.299 + color.green * 0.587 + color.blue * 0.114 > 0.5 ? "black" : "white"
10766
12821
  };
10767
- /**
10768
- * Generate circle with label in center.
10769
- * @param count
10770
- * @returns
10771
- */
10772
- const getCanvas = (count) => {
12822
+ }
12823
+ doUpdate() {
12824
+ var _a;
12825
+ if (this.disposed) {
12826
+ return;
12827
+ }
12828
+ const previous = new Set(this.currClusteredEntities);
12829
+ const { clusters } = this.getClusters();
12830
+ const live = new Set(clusters.map(cluster => cluster.center.id));
12831
+ for (const id of this.clusterEntities.keys()) {
12832
+ if (!live.has(id)) {
12833
+ this.removeClusterEntity(id);
12834
+ }
12835
+ }
12836
+ const changed = new Set();
12837
+ for (const id of new Set([...previous, ...this.registeredEntityIds])) {
12838
+ if (this.currClusteredEntities.has(id)) {
12839
+ continue;
12840
+ }
12841
+ const rego = this.register.GetRego({ entityId: id, menuItemId: this.menuItemId });
12842
+ if ((rego === null || rego === void 0 ? void 0 : rego.overrideShow) === false) {
12843
+ rego.overrideShow = null;
12844
+ changed.add(id);
12845
+ }
12846
+ }
12847
+ for (const cluster of clusters) {
12848
+ const clusterId = cluster.center.id;
12849
+ let representative = cluster.center;
12850
+ let appearance = this.appearance(representative.id);
12851
+ if (this.settings.representative !== "nearest") {
12852
+ for (const point of cluster.points) {
12853
+ const candidate = this.appearance(point.id);
12854
+ const distance = Cesium.Cartesian3.distanceSquared(point.position, this.viewer.camera.positionWC || this.viewer.camera.position);
12855
+ const representativeDistance = Cesium.Cartesian3.distanceSquared(representative.position, this.viewer.camera.positionWC || this.viewer.camera.position);
12856
+ if (candidate.priority > appearance.priority || (candidate.priority === appearance.priority &&
12857
+ (distance < representativeDistance || (distance === representativeDistance &&
12858
+ point.id.localeCompare(representative.id) < 0)))) {
12859
+ representative = point;
12860
+ appearance = candidate;
12861
+ }
12862
+ }
12863
+ }
12864
+ const position = new Cesium.Cartesian3();
12865
+ for (const point of cluster.points) {
12866
+ Cesium.Cartesian3.add(position, point.position, position);
12867
+ }
12868
+ Cesium.Cartesian3.divideByScalar(position, cluster.points.length, position);
12869
+ const count = cluster.points.length;
10773
12870
  const params = {
10774
- size: getScale(count),
10775
- txtColor: this.pointColorTxt,
10776
- bgColor: this.pointColorBg,
12871
+ size: PointClusterSize(count),
10777
12872
  text: String(count),
10778
- iconUrl: this.iconUrl
12873
+ iconUrl: appearance.iconUrl,
12874
+ bgColor: appearance.bgColor,
12875
+ txtColor: appearance.txtColor
10779
12876
  };
10780
- return getClusterImage(params);
10781
- };
10782
- // 5: iterate over clusters and add/update them as Cesium entities.
10783
- for (let cluster of clusters) {
10784
- const clusterId = cluster.center.id;
10785
- let clusterEntity = this.clusterEntities.get(clusterId);
10786
- const centroid = this.calculateCentroid(cluster.points);
10787
- const count = cluster.points.length;
10788
- if (clusterEntity) {
10789
- clusterEntity.position = new Cesium.ConstantPositionProperty(Cesium.Cartesian3.fromDegrees(centroid.lon, centroid.lat, 150));
10790
- clusterEntity.billboard.image = new Cesium.CallbackProperty(() => {
10791
- return getCanvas(count);
10792
- }, false),
10793
- clusterEntity.billboard.width = new Cesium.ConstantProperty(getScale(cluster.points.length));
10794
- clusterEntity.billboard.height = new Cesium.ConstantProperty(getScale(cluster.points.length));
10795
- }
10796
- else {
10797
- clusterEntity = this.viewer.entities.add({
10798
- position: Cesium.Cartesian3.fromDegrees(centroid.lon, centroid.lat, 150),
12877
+ const image = new Cesium.CallbackProperty(() => getClusterImage(params), false);
12878
+ let entity = this.clusterEntities.get(clusterId);
12879
+ if (!entity) {
12880
+ entity = this.viewer.entities.add({
12881
+ position,
10799
12882
  billboard: {
12883
+ image,
12884
+ color: Cesium.Color.WHITE,
12885
+ width: params.size,
12886
+ height: params.size,
10800
12887
  heightReference: Cesium.HeightReference.NONE,
10801
- image: new Cesium.CallbackProperty(() => {
10802
- return getCanvas(count);
10803
- }, false),
10804
- width: getScale(count),
10805
- height: getScale(count),
10806
12888
  verticalOrigin: Cesium.VerticalOrigin.CENTER,
10807
12889
  horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
10808
- disableDepthTestDistance: Number.POSITIVE_INFINITY,
10809
- scaleByDistance: new Cesium.NearFarScalar(1.5e2, 0.5, 3.0e7, 0.1),
12890
+ disableDepthTestDistance: Infinity,
12891
+ scaleByDistance: new Cesium.NearFarScalar(1.5e2, 0.5, 3.0e7, 0.1)
10810
12892
  }
10811
12893
  });
10812
- this.clusterEntities.set(clusterId, clusterEntity);
12894
+ this.clusterEntities.set(clusterId, entity);
12895
+ if (this.settings.selectOnClick !== false)
12896
+ this.picker.targets.set(entity, () => this.selectCluster(clusterId));
10813
12897
  }
10814
- for (let i = 0; i < cluster.points.length; i++) {
10815
- const entityId = cluster.points[i].id;
12898
+ else {
12899
+ entity.position = new Cesium.ConstantPositionProperty(position);
12900
+ entity.billboard.image = image;
12901
+ entity.billboard.width = new Cesium.ConstantProperty(params.size);
12902
+ entity.billboard.height = new Cesium.ConstantProperty(params.size);
12903
+ }
12904
+ const old = this.clusterViews.get(clusterId);
12905
+ this.clusterViews.set(clusterId, {
12906
+ entity,
12907
+ entityIds: cluster.points.map(point => point.id),
12908
+ representativeId: representative.id,
12909
+ selected: (_a = old === null || old === void 0 ? void 0 : old.selected) !== null && _a !== void 0 ? _a : false
12910
+ });
12911
+ _loadClusterImage(params).then(() => {
12912
+ if (!this.disposed && !this.viewer.isDestroyed()) {
12913
+ this.viewer.scene.requestRender();
12914
+ }
12915
+ });
12916
+ for (const point of cluster.points) {
10816
12917
  const rego = this.register.GetRego({
10817
- entityId: entityId,
12918
+ entityId: point.id,
10818
12919
  menuItemId: this.menuItemId
10819
12920
  });
10820
- if (rego && rego.overrideShow != false) {
12921
+ if (rego && rego.overrideShow !== false) {
10821
12922
  rego.overrideShow = false;
10822
- entitiesToUpdate.push(entityId);
12923
+ changed.add(point.id);
10823
12924
  }
10824
12925
  }
10825
12926
  }
10826
- this.queueForceUpdate(entitiesToUpdate);
10827
- // 6: Iterate over existing cluster entities and remove those that are no longer clustered.
10828
- for (let [clusterId, clusterEntity] of Array.from(this.clusterEntities)) {
10829
- if (!clusters.find(x => x.center.id == clusterId)) {
10830
- this.viewer.entities.remove(clusterEntity);
10831
- this.clusterEntities.delete(clusterId);
10832
- }
12927
+ if (changed.size) {
12928
+ this.register.ForceUpdate({ entityIds: Array.from(changed) });
10833
12929
  }
10834
- this.viewer.scene.requestRender();
12930
+ this.refreshSelection();
10835
12931
  }
10836
- getClusterSpacing(distanceFromCluster) {
10837
- // Distance thresholds in meters from the cluster.
10838
- const distanceThresholds = [3000, 4000, 5300, 6000, 7000];
10839
- // Distance increments in degrees.
10840
- const distanceIncrements = [0.001, 0.002, 0.01, 0.03, 0.09];
10841
- let index = 0;
10842
- if (distanceFromCluster) {
10843
- for (let i = 0; i < distanceThresholds.length; i++) {
10844
- if (distanceFromCluster > distanceThresholds[i]) {
10845
- index = i;
10846
- }
10847
- }
10848
- }
10849
- return distanceIncrements[index];
12932
+ getClusterSpacing(distance) {
12933
+ return distance > 7000 ? 0.09 : distance > 6000 ? 0.03 : distance > 5300 ? 0.01 : distance > 4000 ? 0.002 : 0.001;
10850
12934
  }
10851
12935
  getClusters() {
12936
+ const previous = new Set(this.currClusteredEntities);
10852
12937
  this.currClusteredEntities.clear();
10853
- let clusters = [];
10854
- let processedPoints = new Set();
10855
- const cameraPosition = this.viewer.camera.position;
10856
- // We won't cluster selected entities.
10857
- const selectedIds = this.register.GetSelected();
10858
- // Only cluster if total registered points exceeds minTotalCount.
10859
- let validClusters = [];
10860
- if (this.registeredEntityIds.size >= this.minTotalCount) {
10861
- const MIN_CAMERA_DISTANCE = this.minDistance;
10862
- const processQuad = (quad) => {
10863
- // Distance to quad.
10864
- // TODO: Needs improvement.
10865
- const distanceToQuad = quad.GetDistanceToQuad(cameraPosition);
10866
- // Skip quads that are too close.
10867
- if (distanceToQuad >= MIN_CAMERA_DISTANCE) {
10868
- for (let point of quad.points) {
10869
- // Skip points already processed in previous clusters.
10870
- if (selectedIds.includes(point.id) || processedPoints.has(point.id)) {
10871
- continue;
10872
- }
10873
- // Skip points closer than MIN_CAMERA_DISTANCE meters to the camera.
10874
- const cartesian3 = Cesium.Cartesian3.fromDegrees(point.lon, point.lat);
10875
- const distanceFromCluster = Cesium.Cartesian3.distance(cartesian3, cameraPosition);
10876
- if (distanceFromCluster <= MIN_CAMERA_DISTANCE) {
10877
- continue;
10878
- }
10879
- let found = [];
10880
- let nearbyPoints = quad.Query(new Circle(point.lon, point.lat, this.getClusterSpacing(distanceFromCluster)), found);
10881
- nearbyPoints = nearbyPoints.filter(x => !processedPoints.has(x.id) && !selectedIds.includes(x.id));
10882
- if (nearbyPoints.length > 1) {
10883
- let cluster = { center: point, points: [] };
10884
- for (let nearby of nearbyPoints) {
10885
- if (!cluster.points.includes(nearby)) {
10886
- cluster.points.push(nearby);
10887
- processedPoints.add(nearby.id);
10888
- this.currClusteredEntities.add(nearby.id);
10889
- }
10890
- }
10891
- clusters.push(cluster);
10892
- }
10893
- }
10894
- }
10895
- if (quad.divided) {
10896
- processQuad(quad.northwest);
10897
- processQuad(quad.northeast);
10898
- processQuad(quad.southwest);
10899
- processQuad(quad.southeast);
10900
- }
10901
- };
10902
- processQuad(this.quadTree);
10903
- // Filter out clusters with only one point.
10904
- validClusters = clusters.filter((cluster) => cluster.points.length > 1);
12938
+ this.quadTree = new Quad(new Rectangle(0, 0, 360, 180), 30);
12939
+ const points = [];
12940
+ const distances = new Map();
12941
+ const camera = this.viewer.camera.positionWC || this.viewer.camera.position;
12942
+ const isolated = this.register.GetIsIsolatedAny();
12943
+ for (const [id, record] of this.records) {
12944
+ const state = this.register.GetState({ entityId: id, menuItemId: this.menuItemId });
12945
+ if (state.hidden || (isolated && !state.isolated)) {
12946
+ continue;
12947
+ }
12948
+ const geometry = this.resolveGeometry(record.visual);
12949
+ if (!geometry) {
12950
+ continue;
12951
+ }
12952
+ const { position, radius, minDistance } = geometry;
12953
+ const carto = Cesium.Cartographic.fromCartesian(position);
12954
+ if (!carto) {
12955
+ continue;
12956
+ }
12957
+ const point = new Point(Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude), id);
12958
+ point.position = position;
12959
+ point.radius = radius;
12960
+ point.minDistance = minDistance;
12961
+ if (!this.quadTree.Insert(point)) {
12962
+ continue;
12963
+ }
12964
+ points.push(point);
12965
+ distances.set(id, Cesium.Cartesian3.distance(position, camera));
10905
12966
  }
10906
- // Get the entity IDs that are no longer clustered
10907
- const noLongerClustered = new Set(Array.from(this.prevClusteredEntities).filter((id) => !this.currClusteredEntities.has(id)));
10908
- // Update the previous clustered entities ref.
10909
- this.prevClusteredEntities = new Set(this.currClusteredEntities);
10910
- return { clusters: validClusters, noLongerClustered };
10911
- }
10912
- /**
10913
- * Removes Cesium cluster entity.
10914
- * @param clusterId
10915
- */
10916
- removeClusterEntity(clusterId) {
10917
- const clusterEntity = this.clusterEntities.get(clusterId);
10918
- if (clusterEntity) {
10919
- this.viewer.entities.remove(clusterEntity);
10920
- this.clusterEntities.delete(clusterId);
12967
+ const clusters = points.length < this.minTotalCount ? [] :
12968
+ ClusterItems({
12969
+ items: points.sort((a, b) => distances.get(a.id) - distances.get(b.id) || a.id.localeCompare(b.id)),
12970
+ id: point => point.id,
12971
+ eligible: point => Math.max(0, distances.get(point.id) - point.radius) > point.minDistance,
12972
+ neighbours: point => this.quadTree.Query(new Circle(point.lon, point.lat, this.getClusterSpacing(distances.get(point.id))), [])
12973
+ }).map(cluster => ({ center: cluster.representative, points: cluster.members }));
12974
+ for (const cluster of clusters) {
12975
+ for (const point of cluster.points) {
12976
+ this.currClusteredEntities.add(point.id);
12977
+ }
10921
12978
  }
12979
+ return {
12980
+ clusters,
12981
+ noLongerClustered: new Set(Array.from(previous).filter(id => !this.currClusteredEntities.has(id)))
12982
+ };
10922
12983
  }
10923
- convertCartesianToCartographic(cartesian3) {
10924
- const carto = Cesium.Cartographic.fromCartesian(cartesian3);
10925
- const lon = Cesium.Math.toDegrees(carto.longitude);
10926
- const lat = Cesium.Math.toDegrees(carto.latitude);
10927
- const id = lon + ',' + lat;
10928
- return new Point(lon, lat, id);
10929
- }
10930
- /**
10931
- * Adds new entity to the clustering logic.
10932
- * @param id Bruce entity ID.
10933
- * @param cartesian3 entity's position.
10934
- */
10935
- addPoint(id, cartesian3) {
10936
- let point = this.convertCartesianToCartographic(cartesian3);
10937
- point.id = id;
10938
- return this.quadTree.Insert(point);
12984
+ removeClusterEntity(id) {
12985
+ const entity = this.clusterEntities.get(id);
12986
+ if (!entity)
12987
+ return;
12988
+ this.picker.targets.delete(entity);
12989
+ this.viewer.entities.remove(entity);
12990
+ this.clusterEntities.delete(id);
12991
+ this.clusterViews.delete(id);
10939
12992
  }
10940
- /**
10941
- * Adds entity to clustering logic.
10942
- * Will return false if entity could not be clustered and therefor should not be hidden.
10943
- * @param id
10944
- * @param entity
10945
- * @param queueUpdate
10946
- * @returns
12993
+ /*
12994
+ * Metadata is additive; existing three-argument calls keep their original signature.
10947
12995
  */
10948
- AddEntity(id, entity, queueUpdate = true) {
10949
- if (this.disposed) {
10950
- return false;
10951
- }
10952
- if (!entity) {
10953
- return false;
10954
- }
10955
- if (entity.polygon || entity.polyline) {
12996
+ AddEntity(id, visual, queueUpdate = true, metadata = {}) {
12997
+ if (this.disposed || !visual) {
10956
12998
  return false;
10957
12999
  }
10958
- const pos3d = GetValue(this.viewer, entity.position);
10959
- if (!(pos3d === null || pos3d === void 0 ? void 0 : pos3d.x)) {
10960
- return false;
10961
- }
10962
- if (!this.pointColorBg && entity.point) {
10963
- const pointColorBg = GetValue(this.viewer, entity.point.color);
10964
- if (pointColorBg) {
10965
- let cColor = null;
10966
- if (pointColorBg instanceof Object) {
10967
- cColor = new Cesium.Color(pointColorBg.red, pointColorBg.green, pointColorBg.blue, pointColorBg.alpha);
10968
- }
10969
- else if (typeof pointColorBg === "string") {
10970
- cColor = Cesium.Color.fromCssColorString(pointColorBg);
10971
- }
10972
- // Determine if text color should instead be black based on background.
10973
- // cColor contains r,g,b,a values where r,g,b are in the range [0,1].
10974
- if (cColor) {
10975
- const brightness = (cColor.red * 299 + cColor.green * 587 + cColor.blue * 114) / 1000;
10976
- if (brightness > 0.5) {
10977
- this.pointColorTxt = "black";
10978
- }
10979
- else {
10980
- this.pointColorTxt = "white";
10981
- }
10982
- this.pointColorBg = cColor.toCssColorString();
10983
- }
10984
- }
10985
- }
10986
- if (!this.iconUrl && entity.billboard) {
10987
- const iconUrl = GetValue(this.viewer, entity.billboard.image);
10988
- if (typeof iconUrl == "string") {
10989
- this.iconUrl = iconUrl;
10990
- }
10991
- }
10992
- if (this.registeredEntityIds.has(id)) {
10993
- this.RemoveEntity(id, false);
10994
- }
10995
- const added = this.addPoint(id, pos3d);
10996
- if (!added) {
13000
+ if (!this.resolveGeometry(visual)) {
13001
+ this.RemoveEntity(id, queueUpdate);
10997
13002
  return false;
10998
13003
  }
13004
+ this.records.set(id, { visual, metadata });
10999
13005
  this.registeredEntityIds.add(id);
11000
13006
  if (queueUpdate) {
11001
- this.updateQueue.Call();
13007
+ this.Update();
11002
13008
  }
11003
13009
  return true;
11004
13010
  }
11005
- /**
11006
- * Removes entity from clustering logic.
11007
- * Warning: This will not reveal the entity, suppressShow will remain true.
11008
- * This is made with the assumption that the entity is being removed from viewer.
11009
- * @param id
11010
- * @param queueUpdate
11011
- * @returns
11012
- */
11013
13011
  RemoveEntity(id, queueUpdate = false) {
11014
13012
  if (this.disposed) {
11015
13013
  return;
11016
13014
  }
11017
- let pointToRemove = this.quadTree.Find(id);
11018
- if (pointToRemove) {
11019
- this.quadTree.Remove(pointToRemove);
11020
- if (queueUpdate) {
11021
- this.updateQueue.Call();
11022
- }
11023
- }
13015
+ this.records.delete(id);
11024
13016
  this.registeredEntityIds.delete(id);
13017
+ if (queueUpdate) {
13018
+ this.Update();
13019
+ }
11025
13020
  }
11026
13021
  }
11027
13022
 
11028
13023
  /**
11029
13024
  * How often a historic refresh is worth running, and which Entities it should ask about first.
11030
- *
11031
- * A handful of tracked vehicles can afford to re-read on almost every clock tick. Thousands of
11032
- * parcels cannot, and refreshing them that often was never buying anything since the answer barely
11033
- * changes between ticks.
11034
13025
  */
11035
13026
  var HistoricFetchOrder;
11036
13027
  (function (HistoricFetchOrder) {
@@ -11075,6 +13066,13 @@
11075
13066
  HistoricFetchOrder.IntervalForClock = IntervalForClock;
11076
13067
  // Upper bounds in metres for the distance bands, nearest first.
11077
13068
  HistoricFetchOrder.DEFAULT_BANDS = [500, 2000, 10000, 50000];
13069
+ // How far outside the visible rectangle something still counts as worth re-reading, share of the view itself.
13070
+ HistoricFetchOrder.REFRESH_MARGIN_FRACTION = 0.5;
13071
+ // A close view still gets a usable buffer, and a distant one does not reach half the account.
13072
+ HistoricFetchOrder.MIN_REFRESH_MARGIN_METRES = 250;
13073
+ HistoricFetchOrder.MAX_REFRESH_MARGIN_METRES = 2000;
13074
+ const METRES_PER_DEGREE_LAT = 111320;
13075
+ const MIN_LAT_COSINE = 0.01;
11078
13076
  /**
11079
13077
  * Resolves what to order by from the view monitor, falling back to the camera itself.
11080
13078
  */
@@ -11155,6 +13153,55 @@
11155
13153
  return grouped.filter(group => group.length > 0);
11156
13154
  }
11157
13155
  HistoricFetchOrder.ByCameraDistanceOf = ByCameraDistanceOf;
13156
+ /**
13157
+ * Returns how far past a visible rectangle a refresh should still reach, in metres.
13158
+ */
13159
+ function RefreshMarginFor(bounds) {
13160
+ if (!bounds) {
13161
+ return HistoricFetchOrder.MIN_REFRESH_MARGIN_METRES;
13162
+ }
13163
+ const latSpan = Math.abs(bounds.north - bounds.south) * METRES_PER_DEGREE_LAT;
13164
+ const cosine = Math.max(MIN_LAT_COSINE, Math.cos(Cesium.Math.toRadians((bounds.north + bounds.south) / 2)));
13165
+ const lonDegrees = bounds.west > bounds.east
13166
+ ? (bounds.east + 360) - bounds.west
13167
+ : bounds.east - bounds.west;
13168
+ const lonSpan = Math.abs(lonDegrees) * METRES_PER_DEGREE_LAT * cosine;
13169
+ const span = Math.min(latSpan, lonSpan);
13170
+ if (!(span > 0)) {
13171
+ return HistoricFetchOrder.MIN_REFRESH_MARGIN_METRES;
13172
+ }
13173
+ return Math.min(HistoricFetchOrder.MAX_REFRESH_MARGIN_METRES, Math.max(HistoricFetchOrder.MIN_REFRESH_MARGIN_METRES, span * HistoricFetchOrder.REFRESH_MARGIN_FRACTION));
13174
+ }
13175
+ HistoricFetchOrder.RefreshMarginFor = RefreshMarginFor;
13176
+ /**
13177
+ * Returns the rectangle grown outwards by a margin in metres.
13178
+ */
13179
+ function GrowBounds(bounds, metres) {
13180
+ if (!bounds || !(metres > 0)) {
13181
+ return bounds;
13182
+ }
13183
+ const dLat = metres / METRES_PER_DEGREE_LAT;
13184
+ const north = Math.min(90, bounds.north + dLat);
13185
+ const south = Math.max(-90, bounds.south - dLat);
13186
+ const cosine = Math.cos(Cesium.Math.toRadians((bounds.north + bounds.south) / 2));
13187
+ if (cosine < MIN_LAT_COSINE) {
13188
+ return { north, south, west: -180, east: 180 };
13189
+ }
13190
+ const dLon = metres / (METRES_PER_DEGREE_LAT * cosine);
13191
+ const straddles = bounds.west > bounds.east;
13192
+ const gap = straddles ? bounds.west - bounds.east : 360 - (bounds.east - bounds.west);
13193
+ if (gap <= dLon * 2) {
13194
+ return { north, south, west: -180, east: 180 };
13195
+ }
13196
+ return { north, south, west: wrapLongitude(bounds.west - dLon), east: wrapLongitude(bounds.east + dLon) };
13197
+ }
13198
+ HistoricFetchOrder.GrowBounds = GrowBounds;
13199
+ /*
13200
+ * Returns a longitude folded back into the range the bounds are expressed in.
13201
+ */
13202
+ function wrapLongitude(lon) {
13203
+ return (((lon + 180) % 360) + 360) % 360 - 180;
13204
+ }
11158
13205
  /*
11159
13206
  * Returns whether a world position sits inside a ground rectangle given in degrees.
11160
13207
  */
@@ -11163,17 +13210,22 @@
11163
13210
  if (!degrees) {
11164
13211
  return false;
11165
13212
  }
11166
- const { lat, lon } = degrees;
13213
+ return InDegreeBounds(degrees.lat, degrees.lon, bounds);
13214
+ }
13215
+ HistoricFetchOrder.InBounds = InBounds;
13216
+ /*
13217
+ * The same test for a caller that already holds the degrees and should not pay to resolve them from a world position again.
13218
+ */
13219
+ function InDegreeBounds(lat, lon, bounds) {
11167
13220
  if (lat < bounds.south || lat > bounds.north) {
11168
13221
  return false;
11169
13222
  }
11170
- // A view straddling the antimeridian reports west east of east, so the span is the outside.
11171
13223
  if (bounds.west > bounds.east) {
11172
13224
  return lon >= bounds.west || lon <= bounds.east;
11173
13225
  }
11174
13226
  return lon >= bounds.west && lon <= bounds.east;
11175
13227
  }
11176
- HistoricFetchOrder.InBounds = InBounds;
13228
+ HistoricFetchOrder.InDegreeBounds = InDegreeBounds;
11177
13229
  /*
11178
13230
  * Returns an Entity's world position, or null when it has none to place it by.
11179
13231
  */
@@ -11664,7 +13716,7 @@
11664
13716
  });
11665
13717
  this.useGeojson = item.renderAsGeojson == true;
11666
13718
  if (item.enableClustering) {
11667
- this.clustering = new PointClustering(this.visualsManager, this.item.id);
13719
+ this.clustering = new PointClustering(this.visualsManager, this.item.id, this.item.clustering);
11668
13720
  }
11669
13721
  }
11670
13722
  Init(params) {
@@ -12700,8 +14752,8 @@
12700
14752
  // The baseline data source must be editable.
12701
14753
  const canEdit = !((_b = entity.Bruce.Outline) === null || _b === void 0 ? void 0 : _b.find(x => x.Baseline && !x.Editable));
12702
14754
  const visual = rego === null || rego === void 0 ? void 0 : rego.visual;
14755
+ const wasClustered = this.clustering ? this.clustering.AddEntity(id, cEntity, false, { entity }) : false;
12703
14756
  if (!visual || visual != cEntity) {
12704
- const wasClustered = this.clustering ? this.clustering.AddEntity(id, cEntity, false) : false;
12705
14757
  const tagIds = entity.Bruce["Layer.ID"];
12706
14758
  const rego = {
12707
14759
  canEdit: canEdit,
@@ -14136,8 +16188,8 @@
14136
16188
  menuItemId: this.item.id
14137
16189
  });
14138
16190
  const visual = rego === null || rego === void 0 ? void 0 : rego.visual;
16191
+ const clustered = this.clustering ? this.clustering.AddEntity(id, cEntity, false, { entity }) : false;
14139
16192
  if (!visual || visual != cEntity) {
14140
- const clustered = this.clustering ? this.clustering.AddEntity(id, cEntity, false) : false;
14141
16193
  this.visualsManager.AddRego({
14142
16194
  rego: {
14143
16195
  canEdit: true,
@@ -14559,6 +16611,19 @@
14559
16611
  // All IDs that have ever been queued.
14560
16612
  // When nothing is in queue, we'll re-request the existing IDs based on timeline changes.
14561
16613
  this.allEIds = new Set();
16614
+ // IDs a historic read has actually answered with a record, rather than falling back to a stub.
16615
+ // A refresh serves these before the ones that have never produced anything.
16616
+ this.everFound = new Set();
16617
+ // Every known Entity bucketed by the ground cell its position falls in, so a refresh can ask
16618
+ // what the view reaches instead of walking the whole set. Built as IDs arrive.
16619
+ this.cells = new Map();
16620
+ // IDs the position source could not place yet. They are always refreshed.
16621
+ this.unplaced = new Set();
16622
+ // The IDs the last decision found inside the view, so a camera move can queue the difference
16623
+ // rather than re-requesting everything it already asked about.
16624
+ this.lastInView = new Set();
16625
+ // Unsubscribes the view monitor listener that queues what the camera has moved onto.
16626
+ this.monitorRemoval = null;
14562
16627
  // Indicates a tick is active.
14563
16628
  // When true, we won't bother calling onTick again.
14564
16629
  this.runningTick = false;
@@ -14609,6 +16674,16 @@
14609
16674
  }
14610
16675
  this.historic = historic;
14611
16676
  this.expandSources = expandSources;
16677
+ if (this.historic) {
16678
+ // Anything already known was taken in before the index was being built, so it is filed
16679
+ // a bounded number at a time from here rather than in one pass now.
16680
+ for (const entityId of this.allEIds) {
16681
+ this.unplaced.add(entityId);
16682
+ }
16683
+ }
16684
+ else {
16685
+ this.clearIndex();
16686
+ }
14612
16687
  if (this.historic) {
14613
16688
  // 4 ticks per second.
14614
16689
  // The fastest data we are working with is 5 times per second.
@@ -14660,6 +16735,8 @@
14660
16735
  this.lastOrderFocus = null;
14661
16736
  this.allEIds.clear();
14662
16737
  this.lastFoundDateTimes.clear();
16738
+ this.everFound.clear();
16739
+ this.clearIndex();
14663
16740
  this.lastDateTime = null;
14664
16741
  }
14665
16742
  ReQueueAll() {
@@ -14670,22 +16747,193 @@
14670
16747
  this.eIdQueued = new Set(this.eIdQueue);
14671
16748
  this.priorityCount = 0;
14672
16749
  this.lastOrderFocus = null;
16750
+ this.clearIndex();
14673
16751
  }
14674
16752
  /**
14675
- * Supplies a lookup from Entity ID to world position, letting the queue resolve what the camera
14676
- * is nearest first. Sources that cannot answer should not call this, and are served in order.
16753
+ * Supplies a lookup from Entity ID to world position, letting the queue resolve what the camera is nearest first.
16754
+ * Sources that cannot answer should not call this, and are served in order.
14677
16755
  */
14678
16756
  SetPositionSource(positionOf) {
14679
16757
  this.positionOf = positionOf;
14680
16758
  this.lastOrderFocus = null;
16759
+ this.cells.clear();
16760
+ this.lastInView.clear();
16761
+ for (const entityId of this.allEIds) {
16762
+ this.unplaced.add(entityId);
16763
+ }
14681
16764
  }
14682
16765
  /**
14683
- * Supplies the view monitor, so the order follows the ground the camera is aimed at and prefers
14684
- * what is on screen. Without one the ordering falls back to distance from the camera itself.
16766
+ * Supplies the view monitor, so the order follows the ground the camera is aimed at and prefers what is on screen.
16767
+ * Without one the ordering falls back to distance from the camera itself.
14685
16768
  */
14686
16769
  SetViewMonitor(monitor) {
16770
+ var _a;
16771
+ (_a = this.monitorRemoval) === null || _a === void 0 ? void 0 : _a.call(this);
16772
+ this.monitorRemoval = null;
14687
16773
  this.monitor = monitor;
14688
16774
  this.lastOrderFocus = null;
16775
+ if (!monitor) {
16776
+ return;
16777
+ }
16778
+ this.monitorRemoval = monitor.Updated().Subscribe(() => {
16779
+ this.admitNewlyInView();
16780
+ });
16781
+ }
16782
+ /*
16783
+ * Queues whatever the camera has moved onto since the last decision.
16784
+ */
16785
+ admitNewlyInView() {
16786
+ if (this.disposed || !this.historic) {
16787
+ return;
16788
+ }
16789
+ const inView = this.decideInView();
16790
+ if (!inView) {
16791
+ return;
16792
+ }
16793
+ const admit = [];
16794
+ for (const entityId of inView) {
16795
+ if (!this.lastInView.has(entityId)) {
16796
+ admit.push(entityId);
16797
+ }
16798
+ }
16799
+ this.lastInView = new Set(inView);
16800
+ if (admit.length) {
16801
+ this.Queue(admit);
16802
+ }
16803
+ }
16804
+ /*
16805
+ * Returns the IDs a refresh should ask about, or null when the view cannot say and the answer has to be everything.
16806
+ */
16807
+ decideInView() {
16808
+ if (!this.historic || !this.positionOf || !this.monitor) {
16809
+ return null;
16810
+ }
16811
+ const area = this.refreshArea();
16812
+ if (!area) {
16813
+ return null;
16814
+ }
16815
+ const inView = [];
16816
+ for (const key of this.candidateCellKeys(area)) {
16817
+ const cell = this.cells.get(key);
16818
+ if (!cell) {
16819
+ continue;
16820
+ }
16821
+ for (let i = 0; i < cell.ids.length; i++) {
16822
+ if (HistoricFetchOrder.InDegreeBounds(cell.lats[i], cell.lons[i], area)) {
16823
+ inView.push(cell.ids[i]);
16824
+ }
16825
+ }
16826
+ }
16827
+ // Anything the source cannot place yet is asked about rather than quietly dropped.
16828
+ for (const entityId of this.unplaced) {
16829
+ inView.push(entityId);
16830
+ }
16831
+ return inView;
16832
+ }
16833
+ /*
16834
+ * Returns the cell keys a rectangle can reach.
16835
+ */
16836
+ candidateCellKeys(area) {
16837
+ const size = EntityGatherer.CELL_DEGREES;
16838
+ const southCell = Math.floor(area.south / size);
16839
+ const northCell = Math.floor(area.north / size);
16840
+ const spans = (area.west > area.east ? [
16841
+ {
16842
+ from: Math.floor(area.west / size),
16843
+ to: Math.floor(180 / size)
16844
+ },
16845
+ {
16846
+ from: Math.floor(-180 / size),
16847
+ to: Math.floor(area.east / size)
16848
+ }
16849
+ ] :
16850
+ [{
16851
+ from: Math.floor(area.west / size),
16852
+ to: Math.floor(area.east / size)
16853
+ }]);
16854
+ let width = 0;
16855
+ for (const span of spans) {
16856
+ width += (span.to - span.from) + 1;
16857
+ }
16858
+ const wanted = ((northCell - southCell) + 1) * width;
16859
+ if (!(wanted > 0) || wanted > this.cells.size) {
16860
+ return Array.from(this.cells.keys());
16861
+ }
16862
+ const keys = [];
16863
+ for (let lat = southCell; lat <= northCell; lat++) {
16864
+ for (const span of spans) {
16865
+ for (let lon = span.from; lon <= span.to; lon++) {
16866
+ keys.push(lat + ":" + lon);
16867
+ }
16868
+ }
16869
+ }
16870
+ return keys;
16871
+ }
16872
+ /*
16873
+ * Files an Entity in the cell its position falls in.
16874
+ */
16875
+ place(entityId) {
16876
+ const degrees = this.degreesOf(entityId);
16877
+ if (!degrees) {
16878
+ this.unplaced.add(entityId);
16879
+ return;
16880
+ }
16881
+ const size = EntityGatherer.CELL_DEGREES;
16882
+ const key = Math.floor(degrees.lat / size) + ":" + Math.floor(degrees.lon / size);
16883
+ let cell = this.cells.get(key);
16884
+ if (!cell) {
16885
+ cell = { ids: [], lats: [], lons: [] };
16886
+ this.cells.set(key, cell);
16887
+ }
16888
+ cell.ids.push(entityId);
16889
+ cell.lats.push(degrees.lat);
16890
+ cell.lons.push(degrees.lon);
16891
+ this.unplaced.delete(entityId);
16892
+ }
16893
+ /*
16894
+ * Retries a bounded number of the Entities the source could not place before, so a late baked
16895
+ * position joins the index without any one tick paying for all of them.
16896
+ */
16897
+ placeStragglers() {
16898
+ if (!this.unplaced.size || !this.positionOf) {
16899
+ return;
16900
+ }
16901
+ let budget = EntityGatherer.PLACE_BATCH;
16902
+ for (const entityId of this.unplaced) {
16903
+ if (budget-- <= 0) {
16904
+ return;
16905
+ }
16906
+ this.place(entityId);
16907
+ }
16908
+ }
16909
+ clearIndex() {
16910
+ this.cells.clear();
16911
+ this.unplaced.clear();
16912
+ this.lastInView.clear();
16913
+ }
16914
+ /*
16915
+ * Returns where an Entity sits in degrees, or null when the source cannot place it.
16916
+ */
16917
+ degreesOf(entityId) {
16918
+ let pos = null;
16919
+ try {
16920
+ pos = this.positionOf ? this.positionOf(entityId) : null;
16921
+ }
16922
+ catch {
16923
+ pos = null;
16924
+ }
16925
+ return pos ? CartesianToDegrees(pos) : null;
16926
+ }
16927
+ /*
16928
+ * Returns the visible rectangle plus the margin a refresh still reaches into,
16929
+ * or null when the view cannot say, which is what leaves every Entity in.
16930
+ */
16931
+ refreshArea() {
16932
+ const focus = HistoricFetchOrder.FocusFrom({ viewer: this.viewer, monitor: this.monitor });
16933
+ if (!(focus === null || focus === void 0 ? void 0 : focus.bounds)) {
16934
+ return null;
16935
+ }
16936
+ return HistoricFetchOrder.GrowBounds(focus.bounds, HistoricFetchOrder.RefreshMarginFor(focus.bounds));
14689
16937
  }
14690
16938
  /**
14691
16939
  * Re-bands the pending queue so the nearest Entities are requested first.
@@ -14698,8 +16946,6 @@
14698
16946
  if (!focus) {
14699
16947
  return;
14700
16948
  }
14701
- // Re-banding on every batch would cost a full pass per request for no change in the answer.
14702
- // Measured on the focus, so panning across the ground counts even when the eye barely moves.
14703
16949
  if (this.lastOrderFocus && !EntityGatherer.boundsChanged(this.lastOrderFocus, focus) &&
14704
16950
  Cesium.Cartesian3.distance(this.lastOrderFocus.origin, focus.origin) < EntityGatherer.REORDER_MOVE_METRES) {
14705
16951
  return;
@@ -14746,26 +16992,47 @@
14746
16992
  });
14747
16993
  const ordered = [];
14748
16994
  for (const band of banded) {
16995
+ // Within a band, the ones a read has answered for go first.
16996
+ // A building that has never produced a record is the one least likely to start now, so it waits.
16997
+ const withData = [];
16998
+ const withoutData = [];
14749
16999
  for (const entityId of band) {
17000
+ (this.everFound.has(entityId) ? withData : withoutData).push(entityId);
17001
+ }
17002
+ for (const entityId of withData) {
17003
+ ordered.push(entityId);
17004
+ }
17005
+ for (const entityId of withoutData) {
14750
17006
  ordered.push(entityId);
14751
17007
  }
14752
17008
  }
14753
17009
  return ordered;
14754
17010
  }
17011
+ /*
17012
+ * Takes an ID into the known set, filing it in the index the first time it is seen.
17013
+ */
17014
+ remember(entityId) {
17015
+ if (this.allEIds.has(entityId)) {
17016
+ return;
17017
+ }
17018
+ this.allEIds.add(entityId);
17019
+ // Only a historic refresh reads the index, so nothing else pays to build it.
17020
+ if (this.historic) {
17021
+ this.place(entityId);
17022
+ }
17023
+ }
14755
17024
  Queue(entityIds, topOfQueue = false) {
14756
17025
  if (this.disposed || !(entityIds === null || entityIds === void 0 ? void 0 : entityIds.length)) {
14757
17026
  return;
14758
17027
  }
14759
17028
  let changes = 0;
14760
17029
  if (topOfQueue) {
14761
- // Walked back to front so a repeated ID keeps the position of its last occurrence, then
14762
- // laid down in that order, which is where inserting one at a time leaves them.
14763
17030
  const toFront = [];
14764
17031
  const promoting = new Set();
14765
17032
  let anyAlreadyQueued = false;
14766
17033
  for (let i = entityIds.length - 1; i >= 0; i--) {
14767
17034
  const eId = entityIds[i];
14768
- this.allEIds.add(eId);
17035
+ this.remember(eId);
14769
17036
  if (promoting.has(eId)) {
14770
17037
  continue;
14771
17038
  }
@@ -14801,7 +17068,7 @@
14801
17068
  else {
14802
17069
  for (let i = 0; i < entityIds.length; i++) {
14803
17070
  const eId = entityIds[i];
14804
- this.allEIds.add(eId);
17071
+ this.remember(eId);
14805
17072
  if (this.eIdQueued.has(eId)) {
14806
17073
  continue;
14807
17074
  }
@@ -14816,19 +17083,22 @@
14816
17083
  this.onTick(false);
14817
17084
  }
14818
17085
  Dispose() {
17086
+ var _a;
14819
17087
  if (this.disposed) {
14820
17088
  return;
14821
17089
  }
14822
17090
  this.disposed = true;
17091
+ (_a = this.monitorRemoval) === null || _a === void 0 ? void 0 : _a.call(this);
17092
+ this.monitorRemoval = null;
14823
17093
  this.hDisposals.forEach((d) => d());
14824
17094
  this.hDisposals = [];
14825
- // A tick still awaiting a response resumes after this, and anything left here would have it
14826
- // ask for the whole set again on the way out.
14827
17095
  this.eIdQueue = [];
14828
17096
  this.eIdQueued.clear();
14829
17097
  this.priorityCount = 0;
14830
17098
  this.allEIds.clear();
14831
17099
  this.lastFoundDateTimes.clear();
17100
+ this.everFound.clear();
17101
+ this.clearIndex();
14832
17102
  this.entityDataOverrides.clear();
14833
17103
  }
14834
17104
  checkRange(force) {
@@ -14852,6 +17122,7 @@
14852
17122
  this.lastDateTime = rTime;
14853
17123
  (async () => {
14854
17124
  this.runningTick = true;
17125
+ this.placeStragglers();
14855
17126
  let rerun = false;
14856
17127
  try {
14857
17128
  const handleResponse = async (requestedBatch, entities) => {
@@ -14864,6 +17135,13 @@
14864
17135
  foundById.set(entityId, entity);
14865
17136
  }
14866
17137
  }
17138
+ if (this.historic) {
17139
+ for (const entityId of requestedBatch || []) {
17140
+ if (foundById.has(entityId)) {
17141
+ this.everFound.add(entityId);
17142
+ }
17143
+ }
17144
+ }
14867
17145
  for (const entityId of requestedBatch || []) {
14868
17146
  const override = this.entityDataOverrides.get(entityId);
14869
17147
  if (override) {
@@ -14992,15 +17270,16 @@
14992
17270
  this._onQueueProgress.Trigger(100);
14993
17271
  }
14994
17272
  }
14995
- // Now run through all IDs that we've seen and request them.
17273
+ // Now run through the IDs we've seen that the view still reaches, and request them.
14996
17274
  // We'll skip those that we just requested.
14997
- let allEIds = Array.from(this.allEIds);
17275
+ const inView = this.decideInView();
17276
+ this.lastInView = inView ? new Set(inView) : new Set();
17277
+ let allEIds = inView || Array.from(this.allEIds);
14998
17278
  if (requestedIds.size) {
14999
17279
  allEIds = allEIds.filter((eId) => !requestedIds.has(eId));
15000
17280
  }
15001
17281
  if (allEIds.length) {
15002
- // A historic tick re-reads the whole set, so the near ones are worth resolving
15003
- // first here even more than in the queue above.
17282
+ // The near ones are resolved first out of whatever the view still reaches.
15004
17283
  allEIds = this.bandIds(allEIds);
15005
17284
  while (allEIds.length && !this.disposed && !this.eIdQueue.length) {
15006
17285
  if (this.historic && this.lastDateTime !== rTime) {
@@ -15039,6 +17318,11 @@
15039
17318
  }
15040
17319
  // Below this the whole queue clears in one or two requests, so ordering it buys nothing.
15041
17320
  EntityGatherer.MIN_ORDER_QUEUE = 500;
17321
+ // Side of the ground cells Entities are bucketed into, in degrees.
17322
+ // About a kilometre, so a view covers a handful of them rather than hundreds.
17323
+ EntityGatherer.CELL_DEGREES = 0.01;
17324
+ // How many Entities one tick will try to place in the index.
17325
+ EntityGatherer.PLACE_BATCH = 20000;
15042
17326
  // How far the point being looked at has to move before the queue is worth re-banding, in metres.
15043
17327
  // Under the nearest band's width, so a move that could change what is nearest still triggers one.
15044
17328
  EntityGatherer.REORDER_MOVE_METRES = 250;
@@ -47784,7 +50068,7 @@ void main() {
47784
50068
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
47785
50069
  })(exports.StyleUtils || (exports.StyleUtils = {}));
47786
50070
 
47787
- const VERSION = "7.3.8";
50071
+ const VERSION = "7.4.0";
47788
50072
  /**
47789
50073
  * Updates the environment instance used by bruce-cesium to one specified.
47790
50074
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.
@@ -47804,6 +50088,9 @@ void main() {
47804
50088
  exports.VERSION = VERSION;
47805
50089
  exports.setENVIRONMENT = setENVIRONMENT;
47806
50090
  exports.getENVIRONMENT = getENVIRONMENT;
50091
+ exports.ClusterItems = ClusterItems;
50092
+ exports.PointClustering = PointClustering;
50093
+ exports.PointClusterSize = PointClusterSize;
47807
50094
  exports.isStyleChanged = isStyleChanged;
47808
50095
  exports.isOutlineChanged = isOutlineChanged$1;
47809
50096
  exports.CesiumParabola = CesiumParabola;