bruce-cesium 7.2.4 → 7.2.6

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 (31) hide show
  1. package/dist/bruce-cesium.es5.js +386 -52
  2. package/dist/bruce-cesium.es5.js.map +1 -1
  3. package/dist/bruce-cesium.umd.js +384 -50
  4. package/dist/bruce-cesium.umd.js.map +1 -1
  5. package/dist/lib/bruce-cesium.js +1 -1
  6. package/dist/lib/internal/cesium-utils.js +32 -1
  7. package/dist/lib/internal/cesium-utils.js.map +1 -1
  8. package/dist/lib/rendering/entity-gatherer.js +170 -17
  9. package/dist/lib/rendering/entity-gatherer.js.map +1 -1
  10. package/dist/lib/rendering/menu-item-manager.js +1 -0
  11. package/dist/lib/rendering/menu-item-manager.js.map +1 -1
  12. package/dist/lib/rendering/render-managers/common/historic-utils.js +91 -24
  13. package/dist/lib/rendering/render-managers/common/historic-utils.js.map +1 -1
  14. package/dist/lib/rendering/render-managers/entities/entities-ids-render-manager.js +1 -0
  15. package/dist/lib/rendering/render-managers/entities/entities-ids-render-manager.js.map +1 -1
  16. package/dist/lib/rendering/render-managers/entities/entities-render-manager.js +1 -0
  17. package/dist/lib/rendering/render-managers/entities/entities-render-manager.js.map +1 -1
  18. package/dist/lib/rendering/render-managers/tilesets/tileset-entities-render-manager.js +61 -3
  19. package/dist/lib/rendering/render-managers/tilesets/tileset-entities-render-manager.js.map +1 -1
  20. package/dist/lib/rendering/texture-frame-series-animator.js +20 -3
  21. package/dist/lib/rendering/texture-frame-series-animator.js.map +1 -1
  22. package/dist/lib/rendering/tileset-styler.js +10 -1
  23. package/dist/lib/rendering/tileset-styler.js.map +1 -1
  24. package/dist/types/bruce-cesium.d.ts +1 -1
  25. package/dist/types/internal/cesium-utils.d.ts +9 -0
  26. package/dist/types/rendering/entity-gatherer.d.ts +24 -0
  27. package/dist/types/rendering/render-managers/common/historic-utils.d.ts +30 -5
  28. package/dist/types/rendering/render-managers/tilesets/tileset-entities-render-manager.d.ts +5 -0
  29. package/dist/types/rendering/texture-frame-series-animator.d.ts +1 -0
  30. package/dist/types/rendering/tileset-styler.d.ts +3 -0
  31. package/package.json +1 -1
@@ -2883,6 +2883,36 @@
2883
2883
  function ColorToCColor(color) {
2884
2884
  return new Cesium.Color(color.red ? color.red / 255 : 0, color.green ? color.green / 255 : 0, color.blue ? color.blue / 255 : 0, color.alpha);
2885
2885
  }
2886
+ const degreesCache = new WeakMap();
2887
+ /**
2888
+ * Returns a world position's latitude and longitude in degrees, or null when it has none.
2889
+ * @param pos
2890
+ */
2891
+ function CartesianToDegrees(pos) {
2892
+ if (!pos) {
2893
+ return null;
2894
+ }
2895
+ const cached = degreesCache.get(pos);
2896
+ if (cached) {
2897
+ return cached;
2898
+ }
2899
+ let carto;
2900
+ try {
2901
+ carto = Cesium.Cartographic.fromCartesian(pos);
2902
+ }
2903
+ catch {
2904
+ return null;
2905
+ }
2906
+ if (!carto) {
2907
+ return null;
2908
+ }
2909
+ const degrees = {
2910
+ lat: Cesium.Math.toDegrees(carto.latitude),
2911
+ lon: Cesium.Math.toDegrees(carto.longitude)
2912
+ };
2913
+ degreesCache.set(pos, degrees);
2914
+ return degrees;
2915
+ }
2886
2916
  /**
2887
2917
  * Removes duplicate points in a row and returns a new array.
2888
2918
  * Passed array is not mutated.
@@ -10737,41 +10767,107 @@
10737
10767
  return Math.round(minMs + ((maxMs - minMs) * progress));
10738
10768
  }
10739
10769
  HistoricFetchOrder.IntervalFor = IntervalFor;
10770
+ // Upper bounds in metres for the distance bands, nearest first.
10771
+ HistoricFetchOrder.DEFAULT_BANDS = [500, 2000, 10000, 50000];
10740
10772
  /**
10741
- * Returns the Entities grouped into bands by distance from the camera, nearest band first.
10742
- *
10743
- * Bands rather than a true sort, so a small camera move does not reshuffle the whole queue and
10744
- * restart it. What the user is looking at resolves first either way.
10773
+ * Resolves what to order by from the view monitor, falling back to the camera itself.
10745
10774
  */
10746
- function ByCameraDistance(params) {
10775
+ function FocusFrom(params) {
10747
10776
  var _a;
10748
- const { viewer, entities } = params;
10749
- const bands = params.bands || [500, 2000, 10000, 50000];
10750
- if (!(entities === null || entities === void 0 ? void 0 : entities.length)) {
10777
+ const { viewer, monitor } = params;
10778
+ let origin = null;
10779
+ let bounds = null;
10780
+ try {
10781
+ // Null whenever the view has no ground intersection, eg. a camera aimed at the sky.
10782
+ const target = monitor === null || monitor === void 0 ? void 0 : monitor.GetTarget();
10783
+ if (target && isFinite(target.latitude) && isFinite(target.longitude)) {
10784
+ origin = Cesium.Cartesian3.fromDegrees(target.longitude, target.latitude);
10785
+ }
10786
+ bounds = (monitor === null || monitor === void 0 ? void 0 : monitor.GetBounds()) || null;
10787
+ }
10788
+ catch {
10789
+ origin = null;
10790
+ bounds = null;
10791
+ }
10792
+ if (!origin) {
10793
+ origin = (_a = viewer === null || viewer === void 0 ? void 0 : viewer.camera) === null || _a === void 0 ? void 0 : _a.position;
10794
+ }
10795
+ if (!origin) {
10796
+ return null;
10797
+ }
10798
+ return bounds ? { origin, bounds } : { origin };
10799
+ }
10800
+ HistoricFetchOrder.FocusFrom = FocusFrom;
10801
+ /**
10802
+ * Returns the Entities grouped nearest first, on screen ahead of off screen.
10803
+ */
10804
+ function ByCameraDistance(params) {
10805
+ const { viewer, entities, monitor } = params;
10806
+ return ByCameraDistanceOf({
10807
+ focus: FocusFrom({ viewer, monitor }),
10808
+ items: entities,
10809
+ posOf: (entity) => SafePos(entity, viewer),
10810
+ bands: params.bands
10811
+ });
10812
+ }
10813
+ HistoricFetchOrder.ByCameraDistance = ByCameraDistance;
10814
+ /**
10815
+ * Bands anything that can name its own world position, on the same bounds as the Entity form.
10816
+ */
10817
+ function ByCameraDistanceOf(params) {
10818
+ const { focus, items, posOf } = params;
10819
+ const bands = params.bands || HistoricFetchOrder.DEFAULT_BANDS;
10820
+ if (!(items === null || items === void 0 ? void 0 : items.length)) {
10751
10821
  return [];
10752
10822
  }
10753
- const cameraPos = (_a = viewer === null || viewer === void 0 ? void 0 : viewer.camera) === null || _a === void 0 ? void 0 : _a.position;
10754
- if (!cameraPos) {
10755
- return [entities];
10823
+ if (!(focus === null || focus === void 0 ? void 0 : focus.origin)) {
10824
+ return [items];
10825
+ }
10826
+ // One band past the named bounds, for everything beyond the last of them. Then the same
10827
+ // again for what is off screen, so nothing visible waits behind something that is not.
10828
+ const perTier = bands.length + 1;
10829
+ const offScreenTier = focus.bounds ? perTier : 0;
10830
+ const placeless = offScreenTier + perTier;
10831
+ const grouped = [];
10832
+ for (let i = 0; i <= placeless; i++) {
10833
+ grouped.push([]);
10756
10834
  }
10757
- // One band past the named bounds, for everything beyond the last of them.
10758
- const banded = bands.map(() => []).concat([[]]);
10759
- for (const entity of entities) {
10760
- const pos = SafePos(entity, viewer);
10835
+ for (const item of items) {
10836
+ // An item with no position falls to the last group rather than blocking the near ones.
10837
+ const pos = posOf(item);
10761
10838
  if (!pos) {
10762
- banded[banded.length - 1].push(entity);
10839
+ grouped[placeless].push(item);
10763
10840
  continue;
10764
10841
  }
10765
- const distance = Cesium.Cartesian3.distance(cameraPos, pos);
10766
- let index = bands.findIndex(bound => distance <= bound);
10767
- if (index < 0) {
10768
- index = banded.length - 1;
10842
+ const distance = Cesium.Cartesian3.distance(focus.origin, pos);
10843
+ let band = bands.findIndex(bound => distance <= bound);
10844
+ if (band < 0) {
10845
+ band = bands.length;
10769
10846
  }
10770
- banded[index].push(entity);
10847
+ grouped[(focus.bounds && !InBounds(pos, focus.bounds)) ? offScreenTier + band : band].push(item);
10771
10848
  }
10772
- return banded.filter(band => band.length > 0);
10849
+ return grouped.filter(group => group.length > 0);
10773
10850
  }
10774
- HistoricFetchOrder.ByCameraDistance = ByCameraDistance;
10851
+ HistoricFetchOrder.ByCameraDistanceOf = ByCameraDistanceOf;
10852
+ /*
10853
+ * Returns whether a world position sits inside a ground rectangle given in degrees.
10854
+ */
10855
+ function InBounds(pos, bounds) {
10856
+ const degrees = CartesianToDegrees(pos);
10857
+ if (!degrees) {
10858
+ return false;
10859
+ }
10860
+ const { lat, lon } = degrees;
10861
+ if (lat < bounds.south || lat > bounds.north) {
10862
+ return false;
10863
+ }
10864
+ // A view straddling the antimeridian reports west east of east, so the span is the outside.
10865
+ if (bounds.west > bounds.east) {
10866
+ return lon >= bounds.west || lon <= bounds.east;
10867
+ }
10868
+ return lon >= bounds.west && lon <= bounds.east;
10869
+ }
10870
+ HistoricFetchOrder.InBounds = InBounds;
10775
10871
  /*
10776
10872
  * Returns an Entity's world position, or null when it has none to place it by.
10777
10873
  */
@@ -11089,7 +11185,7 @@
11089
11185
  }
11090
11186
  if (plan.detail) {
11091
11187
  const bands = entities.length > HistoricFetchOrder.SMALL_SET
11092
- ? HistoricFetchOrder.ByCameraDistance({ viewer, entities })
11188
+ ? HistoricFetchOrder.ByCameraDistance({ viewer, entities, monitor: this.params.monitor })
11093
11189
  : [entities];
11094
11190
  for (const range of this.cache.MissingRanges(plan.detail.start, plan.detail.stop)) {
11095
11191
  if (this.disposed) {
@@ -11253,6 +11349,7 @@
11253
11349
  this.onSeriesDiscovered = onSeriesDiscovered;
11254
11350
  this.historicGatherer = new HistoricGatherer({
11255
11351
  viewer: this.viewer,
11352
+ monitor: this.monitor,
11256
11353
  getApi: () => this.apiGetter.getApi(),
11257
11354
  getAttrKey: () => { var _a; return (_a = this.item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historicAttrKey; },
11258
11355
  // A second either side, to take in records sitting exactly on the boundary.
@@ -13358,6 +13455,7 @@
13358
13455
  this.visualsManager = visualsManager;
13359
13456
  this.historicGatherer = new HistoricGatherer({
13360
13457
  viewer: this.viewer,
13458
+ monitor: this.monitor,
13361
13459
  getApi: () => this.apiGetter.getApi(),
13362
13460
  getAttrKey: () => { var _a; return (_a = this.item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historicAttrKey; },
13363
13461
  // Padding either side of a requested range, for clock desync between us and the server.
@@ -13992,6 +14090,18 @@
13992
14090
  this.lastFoundDateTimes = new Map();
13993
14091
  // ID of Entity IDs that need to be requested.
13994
14092
  this.eIdQueue = [];
14093
+ // Membership mirror of eIdQueue for quick checks.
14094
+ this.eIdQueued = new Set();
14095
+ // Resolves where an Entity sits without reading its record, when the caller can say.
14096
+ // Unset leaves the queue in insertion order, which is what sources that bake no coordinates get.
14097
+ this.positionOf = null;
14098
+ // How many IDs at the front of the queue were explicitly asked for first.
14099
+ // Camera ordering leaves those alone, since an explicit re-render outranks proximity.
14100
+ this.priorityCount = 0;
14101
+ // View focus the queue was last banded from.
14102
+ this.lastOrderFocus = null;
14103
+ // Reports what the camera is aimed at, rather than only where it sits.
14104
+ this.monitor = null;
13995
14105
  // All IDs that have ever been queued.
13996
14106
  // When nothing is in queue, we'll re-request the existing IDs based on timeline changes.
13997
14107
  this.allEIds = new Set();
@@ -14091,6 +14201,9 @@
14091
14201
  return;
14092
14202
  }
14093
14203
  this.eIdQueue = [];
14204
+ this.eIdQueued.clear();
14205
+ this.priorityCount = 0;
14206
+ this.lastOrderFocus = null;
14094
14207
  this.allEIds.clear();
14095
14208
  this.lastFoundDateTimes.clear();
14096
14209
  this.lastDateTime = null;
@@ -14100,29 +14213,148 @@
14100
14213
  return;
14101
14214
  }
14102
14215
  this.eIdQueue = Array.from(this.allEIds);
14216
+ this.eIdQueued = new Set(this.eIdQueue);
14217
+ this.priorityCount = 0;
14218
+ this.lastOrderFocus = null;
14219
+ }
14220
+ /**
14221
+ * Supplies a lookup from Entity ID to world position, letting the queue resolve what the camera
14222
+ * is nearest first. Sources that cannot answer should not call this, and are served in order.
14223
+ */
14224
+ SetPositionSource(positionOf) {
14225
+ this.positionOf = positionOf;
14226
+ this.lastOrderFocus = null;
14227
+ }
14228
+ /**
14229
+ * Supplies the view monitor, so the order follows the ground the camera is aimed at and prefers
14230
+ * what is on screen. Without one the ordering falls back to distance from the camera itself.
14231
+ */
14232
+ SetViewMonitor(monitor) {
14233
+ this.monitor = monitor;
14234
+ this.lastOrderFocus = null;
14235
+ }
14236
+ /**
14237
+ * Re-bands the pending queue so the nearest Entities are requested first.
14238
+ */
14239
+ orderQueueByCamera() {
14240
+ if (!this.positionOf || this.eIdQueue.length < EntityGatherer.MIN_ORDER_QUEUE) {
14241
+ return;
14242
+ }
14243
+ const focus = HistoricFetchOrder.FocusFrom({ viewer: this.viewer, monitor: this.monitor });
14244
+ if (!focus) {
14245
+ return;
14246
+ }
14247
+ // Re-banding on every batch would cost a full pass per request for no change in the answer.
14248
+ // Measured on the focus, so panning across the ground counts even when the eye barely moves.
14249
+ if (this.lastOrderFocus && !EntityGatherer.boundsChanged(this.lastOrderFocus, focus) &&
14250
+ Cesium.Cartesian3.distance(this.lastOrderFocus.origin, focus.origin) < EntityGatherer.REORDER_MOVE_METRES) {
14251
+ return;
14252
+ }
14253
+ this.lastOrderFocus = focus;
14254
+ const priority = this.priorityCount > 0 ? this.eIdQueue.slice(0, this.priorityCount) : [];
14255
+ const rest = this.priorityCount > 0 ? this.eIdQueue.slice(this.priorityCount) : this.eIdQueue;
14256
+ if (!rest.length) {
14257
+ return;
14258
+ }
14259
+ this.eIdQueue = priority.concat(this.bandIds(rest, focus));
14260
+ }
14261
+ /*
14262
+ * Returns whether the visible rectangle differs enough to change what counts as on screen.
14263
+ */
14264
+ static boundsChanged(a, b) {
14265
+ if (!a.bounds || !b.bounds) {
14266
+ return Boolean(a.bounds) !== Boolean(b.bounds);
14267
+ }
14268
+ return (a.bounds.north !== b.bounds.north ||
14269
+ a.bounds.south !== b.bounds.south ||
14270
+ a.bounds.east !== b.bounds.east ||
14271
+ a.bounds.west !== b.bounds.west);
14272
+ }
14273
+ /*
14274
+ * Returns the IDs reordered nearest band first, or unchanged when there is nothing to order by.
14275
+ */
14276
+ bandIds(entityIds, focus) {
14277
+ const from = focus || HistoricFetchOrder.FocusFrom({ viewer: this.viewer, monitor: this.monitor });
14278
+ if (!this.positionOf || !from || !entityIds.length) {
14279
+ return entityIds;
14280
+ }
14281
+ const banded = HistoricFetchOrder.ByCameraDistanceOf({
14282
+ focus: from,
14283
+ items: entityIds,
14284
+ posOf: (entityId) => {
14285
+ try {
14286
+ return this.positionOf(entityId);
14287
+ }
14288
+ catch {
14289
+ return null;
14290
+ }
14291
+ }
14292
+ });
14293
+ const ordered = [];
14294
+ for (const band of banded) {
14295
+ for (const entityId of band) {
14296
+ ordered.push(entityId);
14297
+ }
14298
+ }
14299
+ return ordered;
14103
14300
  }
14104
14301
  Queue(entityIds, topOfQueue = false) {
14105
14302
  if (this.disposed || !(entityIds === null || entityIds === void 0 ? void 0 : entityIds.length)) {
14106
14303
  return;
14107
14304
  }
14108
14305
  let changes = 0;
14109
- for (let i = 0; i < entityIds.length; i++) {
14110
- const eId = entityIds[i];
14111
- const index = this.eIdQueue.indexOf(eId);
14112
- if (topOfQueue) {
14113
- // If we want it to be at the top of the queue, remove it from the queue first.
14114
- if (index !== -1) {
14115
- this.eIdQueue.splice(index, 1);
14116
- }
14117
- this.eIdQueue.unshift(eId);
14118
- changes += 1;
14306
+ if (topOfQueue) {
14307
+ // Walked back to front so a repeated ID keeps the position of its last occurrence, then
14308
+ // laid down in that order, which is where inserting one at a time leaves them.
14309
+ const toFront = [];
14310
+ const promoting = new Set();
14311
+ let anyAlreadyQueued = false;
14312
+ for (let i = entityIds.length - 1; i >= 0; i--) {
14313
+ const eId = entityIds[i];
14314
+ this.allEIds.add(eId);
14315
+ if (promoting.has(eId)) {
14316
+ continue;
14317
+ }
14318
+ promoting.add(eId);
14319
+ toFront.push(eId);
14320
+ if (this.eIdQueued.has(eId)) {
14321
+ anyAlreadyQueued = true;
14322
+ }
14323
+ }
14324
+ if (anyAlreadyQueued && this.eIdQueue.length) {
14325
+ const kept = [];
14326
+ let removedFromPriority = 0;
14327
+ for (let i = 0; i < this.eIdQueue.length; i++) {
14328
+ const eId = this.eIdQueue[i];
14329
+ if (promoting.has(eId)) {
14330
+ if (i < this.priorityCount) {
14331
+ removedFromPriority += 1;
14332
+ }
14333
+ continue;
14334
+ }
14335
+ kept.push(eId);
14336
+ }
14337
+ this.eIdQueue = kept;
14338
+ this.priorityCount = Math.max(0, this.priorityCount - removedFromPriority);
14119
14339
  }
14120
- else if (index === -1) {
14340
+ this.eIdQueue = toFront.concat(this.eIdQueue);
14341
+ this.priorityCount += toFront.length;
14342
+ for (let i = 0; i < toFront.length; i++) {
14343
+ this.eIdQueued.add(toFront[i]);
14344
+ }
14345
+ changes = toFront.length;
14346
+ }
14347
+ else {
14348
+ for (let i = 0; i < entityIds.length; i++) {
14349
+ const eId = entityIds[i];
14350
+ this.allEIds.add(eId);
14351
+ if (this.eIdQueued.has(eId)) {
14352
+ continue;
14353
+ }
14354
+ this.eIdQueued.add(eId);
14121
14355
  this.eIdQueue.push(eId);
14122
14356
  changes += 1;
14123
14357
  }
14124
- // Flag that we've seen this ID.
14125
- this.allEIds.add(entityIds[i]);
14126
14358
  }
14127
14359
  if (!changes) {
14128
14360
  return;
@@ -14256,7 +14488,7 @@
14256
14488
  this.emitEntities(toEmit, tags);
14257
14489
  };
14258
14490
  const QUEUE_BATCH_SIZE = 500;
14259
- const requestedIds = [];
14491
+ const requestedIds = new Set();
14260
14492
  // If we have a queue, we need to request those first.
14261
14493
  if (this.eIdQueue.length) {
14262
14494
  const total = this.eIdQueue.length;
@@ -14265,7 +14497,12 @@
14265
14497
  if (this.historic && this.lastDateTime !== rTime) {
14266
14498
  break;
14267
14499
  }
14500
+ this.orderQueueByCamera();
14268
14501
  const batch = this.eIdQueue.splice(0, QUEUE_BATCH_SIZE);
14502
+ this.priorityCount = Math.max(0, this.priorityCount - batch.length);
14503
+ for (let i = 0; i < batch.length; i++) {
14504
+ this.eIdQueued.delete(batch[i]);
14505
+ }
14269
14506
  const { entities } = await BModels.Entity.GetListByIds({
14270
14507
  entityIds: batch,
14271
14508
  historicPoint: this.historic ? rTime : null,
@@ -14274,7 +14511,9 @@
14274
14511
  maxSearchTimeSec: 60 * 2
14275
14512
  });
14276
14513
  handleResponse(batch, entities);
14277
- requestedIds.push(...batch);
14514
+ for (let i = 0; i < batch.length; i++) {
14515
+ requestedIds.add(batch[i]);
14516
+ }
14278
14517
  done += batch.length;
14279
14518
  if (this._onQueueProgress) {
14280
14519
  let progress = (done / total) * 100;
@@ -14294,10 +14533,13 @@
14294
14533
  // Now run through all IDs that we've seen and request them.
14295
14534
  // We'll skip those that we just requested.
14296
14535
  let allEIds = Array.from(this.allEIds);
14297
- if (requestedIds.length) {
14298
- allEIds = allEIds.filter((eId) => requestedIds.indexOf(eId) === -1);
14536
+ if (requestedIds.size) {
14537
+ allEIds = allEIds.filter((eId) => !requestedIds.has(eId));
14299
14538
  }
14300
14539
  if (allEIds.length) {
14540
+ // A historic tick re-reads the whole set, so the near ones are worth resolving
14541
+ // first here even more than in the queue above.
14542
+ allEIds = this.bandIds(allEIds);
14301
14543
  while (allEIds.length && !this.disposed && !this.eIdQueue.length) {
14302
14544
  if (this.historic && this.lastDateTime !== rTime) {
14303
14545
  break;
@@ -14311,7 +14553,9 @@
14311
14553
  maxSearchTimeSec: 60 * 2
14312
14554
  });
14313
14555
  handleResponse(batch, entities);
14314
- requestedIds.push(...batch);
14556
+ for (let i = 0; i < batch.length; i++) {
14557
+ requestedIds.add(batch[i]);
14558
+ }
14315
14559
  }
14316
14560
  }
14317
14561
  // If we had leftovers because we stopped early, we need to re-run the tick.
@@ -14331,6 +14575,11 @@
14331
14575
  })();
14332
14576
  }
14333
14577
  }
14578
+ // Below this the whole queue clears in one or two requests, so ordering it buys nothing.
14579
+ EntityGatherer.MIN_ORDER_QUEUE = 500;
14580
+ // How far the point being looked at has to move before the queue is worth re-banding, in metres.
14581
+ // Under the nearest band's width, so a move that could change what is nearest still triggers one.
14582
+ EntityGatherer.REORDER_MOVE_METRES = 250;
14334
14583
 
14335
14584
  // ND-1641. BOOKMARKS - (DEMO) View does not match bookmark.
14336
14585
  // We have some evil hard-coded style mappings that need to be fixed.
@@ -14454,7 +14703,8 @@
14454
14703
  }
14455
14704
  Init(params) {
14456
14705
  var _a, _b;
14457
- let { viewer, api, cTileset, fallbackStyleId, styleMapping, expandSources, menuItemId, register, scenario, historic, applyStyles } = params;
14706
+ let { viewer, api, cTileset, fallbackStyleId, styleMapping, expandSources, menuItemId, register, scenario, historic, applyStyles, monitor } = params;
14707
+ this.monitor = monitor;
14458
14708
  this.viewer = viewer;
14459
14709
  this.api = api;
14460
14710
  this.cTileset = cTileset;
@@ -14510,6 +14760,14 @@
14510
14760
  }
14511
14761
  }
14512
14762
  });
14763
+ // Only Entities Sets bake coordinates into their features, so for any other source this
14764
+ // resolves nothing and the gather stays in insertion order.
14765
+ this.entityGatherer.SetViewMonitor(this.monitor);
14766
+ this.entityGatherer.SetPositionSource((entityId) => {
14767
+ var _a;
14768
+ const visual = (_a = this.getEntityRego(entityId)) === null || _a === void 0 ? void 0 : _a.visual;
14769
+ return (visual === null || visual === void 0 ? void 0 : visual._bakedPos) || null;
14770
+ });
14513
14771
  this.loaded = true;
14514
14772
  this.loadStyles();
14515
14773
  const feed = (_b = this.api) === null || _b === void 0 ? void 0 : _b.RecordChangeFeed;
@@ -19721,7 +19979,8 @@
19721
19979
  // Saves having to do a case-insensitive lookup every time.
19722
19980
  this.featurePropCache = new Map();
19723
19981
  this.featurePropsChecked = 0;
19724
- const { viewer, register: visualsManager, getters: apiGetter, item, initQueue } = params;
19982
+ const { viewer, register: visualsManager, getters: apiGetter, item, initQueue, monitor } = params;
19983
+ this.monitor = monitor;
19725
19984
  this.viewer = viewer;
19726
19985
  this.getters = apiGetter;
19727
19986
  this.item = item;
@@ -19830,7 +20089,8 @@
19830
20089
  menuItemId: this.item.id,
19831
20090
  register: this.visualsManager,
19832
20091
  historic: Manager.IsHistoric(this.item),
19833
- applyStyles: Boolean(this.item.ApplyStyles)
20092
+ applyStyles: Boolean(this.item.ApplyStyles),
20093
+ monitor: this.monitor
19834
20094
  });
19835
20095
  }
19836
20096
  this.onCTilesetLoad();
@@ -19990,6 +20250,16 @@
19990
20250
  else if (lowered === "internalid") {
19991
20251
  this.featurePropCache.set("internalId", prop);
19992
20252
  }
20253
+ else if (lowered === "latitude") {
20254
+ this.featurePropCache.set("latitude", prop);
20255
+ }
20256
+ else if (lowered === "longitude") {
20257
+ this.featurePropCache.set("longitude", prop);
20258
+ }
20259
+ // b3dm names the baked height TerrainHeight, the glTF metadata table calls it Altitude.
20260
+ else if (lowered === "terrainheight" || lowered === "altitude") {
20261
+ this.featurePropCache.set("height", prop);
20262
+ }
19993
20263
  }
19994
20264
  }
19995
20265
  Dispose() {
@@ -20144,7 +20414,8 @@
20144
20414
  menuItemId: this.item.id,
20145
20415
  register: this.visualsManager,
20146
20416
  historic: Manager.IsHistoric(this.item),
20147
- applyStyles: Boolean(this.item.ApplyStyles)
20417
+ applyStyles: Boolean(this.item.ApplyStyles),
20418
+ monitor: this.monitor
20148
20419
  });
20149
20420
  this.queueHandoffRestyle(this.visualsManager.GetRegos({ menuItemId: this.item.id }));
20150
20421
  }
@@ -20187,6 +20458,45 @@
20187
20458
  }
20188
20459
  }, 10);
20189
20460
  }
20461
+ /*
20462
+ * Reads a numeric feature property, returning null for anything that is not one.
20463
+ */
20464
+ static readNumber(feature, prop) {
20465
+ const raw = feature.getProperty(prop);
20466
+ // Coercing straight to a number would turn null, undefined and "" into a valid zero,
20467
+ // which for a coordinate is a real place off the coast of Africa.
20468
+ if (raw == null || raw === "" || typeof raw === "boolean") {
20469
+ return null;
20470
+ }
20471
+ const value = +raw;
20472
+ return isFinite(value) ? value : null;
20473
+ }
20474
+ /*
20475
+ * Returns the world position an Entities Set bakes into each feature, or null when it has none.
20476
+ */
20477
+ readBakedPos(feature) {
20478
+ const latProp = this.featurePropCache.get("latitude");
20479
+ const lonProp = this.featurePropCache.get("longitude");
20480
+ if (!latProp || !lonProp) {
20481
+ return null;
20482
+ }
20483
+ try {
20484
+ const lat = Manager.readNumber(feature, latProp);
20485
+ const lon = Manager.readNumber(feature, lonProp);
20486
+ // Degrees, per the batch table the generator writes. The range check doubles as a
20487
+ // guard against a source that bakes radians, which would otherwise read as valid.
20488
+ if (lat == null || lon == null || Math.abs(lat) > 90 || Math.abs(lon) > 180) {
20489
+ return null;
20490
+ }
20491
+ // A height that will not read is worth losing, the position it belongs to is not.
20492
+ const heightProp = this.featurePropCache.get("height");
20493
+ const height = heightProp ? Manager.readNumber(feature, heightProp) : 0;
20494
+ return Cesium.Cartesian3.fromDegrees(lon, lat, height !== null && height !== void 0 ? height : 0);
20495
+ }
20496
+ catch {
20497
+ return null;
20498
+ }
20499
+ }
20190
20500
  mapTilesetFeature(feature) {
20191
20501
  var _a, _b, _c;
20192
20502
  this.evaluateFeatureProps(feature);
@@ -20267,6 +20577,12 @@
20267
20577
  rego.internalId = parsed;
20268
20578
  }
20269
20579
  }
20580
+ // Stamped on the feature the properties came from, so a sibling keeps its own position
20581
+ // and nothing has to be cleaned up when the tile unloads.
20582
+ const bakedPos = this.readBakedPos(feature);
20583
+ if (bakedPos) {
20584
+ feature._bakedPos = bakedPos;
20585
+ }
20270
20586
  this.loadedCesiumEntities[rego.entityId] = rego;
20271
20587
  this.visualsManager.AddRego({
20272
20588
  rego,
@@ -24131,6 +24447,7 @@
24131
24447
  viewer: this.viewer,
24132
24448
  register: this.visualsRegister,
24133
24449
  getters: this.getters,
24450
+ monitor: this.sharedMonitor,
24134
24451
  item: params.item,
24135
24452
  initQueue: this.tilesetInitQueue
24136
24453
  });
@@ -40886,9 +41203,11 @@
40886
41203
  return;
40887
41204
  }
40888
41205
  let next = -1;
41206
+ let isTarget = false;
40889
41207
  if (this.pendingIndex !== -1) {
40890
41208
  next = this.pendingIndex;
40891
41209
  this.pendingIndex = -1;
41210
+ isTarget = true;
40892
41211
  }
40893
41212
  else {
40894
41213
  next = this.nearestUncachedFrame();
@@ -40897,7 +41216,7 @@
40897
41216
  return;
40898
41217
  }
40899
41218
  this.inFlightIndex = next;
40900
- this.fetchAndTint(next)
41219
+ this.fetchAndTint(next, isTarget)
40901
41220
  .catch(() => {
40902
41221
  // Eating it.
40903
41222
  })
@@ -40908,7 +41227,7 @@
40908
41227
  }
40909
41228
  });
40910
41229
  }
40911
- async fetchAndTint(idx) {
41230
+ async fetchAndTint(idx, isTarget = true) {
40912
41231
  const tiles = this.tiles;
40913
41232
  // Every tile's frame for one timestep sits contiguously, because the generator writes
40914
41233
  // frame major. So a tiled frame is still ONE range request, not one per tile.
@@ -40940,10 +41259,25 @@
40940
41259
  this.frameCache[idx] = decoded.pixels;
40941
41260
  this.frameDims[idx] = { width: decoded.width, height: decoded.height };
40942
41261
  this.valueCache[idx] = decoded.valuePixels || null;
40943
- if (idx === this.currentFrameIndex) {
41262
+ if (this.shouldPresent(idx, isTarget)) {
40944
41263
  this.beginCrossfadeTo(idx);
40945
41264
  }
40946
41265
  }
41266
+ /*
41267
+ * Whether a frame that has just decoded is worth putting on screen.
41268
+ */
41269
+ shouldPresent(idx, isTarget) {
41270
+ if (idx === this.currentFrameIndex) {
41271
+ return true;
41272
+ }
41273
+ // A prefetch is for a frame nobody asked to see, so it stays in the cache.
41274
+ if (!isTarget) {
41275
+ return false;
41276
+ }
41277
+ // Dragging the timeline retargets faster than a frame can be fetched, so the frame that
41278
+ // lands is rarely the one now under the playhead.
41279
+ return !this.frameCache[this.currentFrameIndex];
41280
+ }
40947
41281
  /*
40948
41282
  * Draws every tile of one frame into a single raster covering the drape extent.
40949
41283
  */
@@ -45481,7 +45815,7 @@ void main() {
45481
45815
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
45482
45816
  })(exports.StyleUtils || (exports.StyleUtils = {}));
45483
45817
 
45484
- const VERSION = "7.2.4";
45818
+ const VERSION = "7.2.6";
45485
45819
  /**
45486
45820
  * Updates the environment instance used by bruce-cesium to one specified.
45487
45821
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.