bruce-cesium 7.2.4 → 7.2.5

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 +326 -34
  2. package/dist/bruce-cesium.es5.js.map +1 -1
  3. package/dist/bruce-cesium.umd.js +324 -32
  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 +111 -0
  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 +23 -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,16 @@
13992
14090
  this.lastFoundDateTimes = new Map();
13993
14091
  // ID of Entity IDs that need to be requested.
13994
14092
  this.eIdQueue = [];
14093
+ // Resolves where an Entity sits without reading its record, when the caller can say.
14094
+ // Unset leaves the queue in insertion order, which is what sources that bake no coordinates get.
14095
+ this.positionOf = null;
14096
+ // How many IDs at the front of the queue were explicitly asked for first.
14097
+ // Camera ordering leaves those alone, since an explicit re-render outranks proximity.
14098
+ this.priorityCount = 0;
14099
+ // View focus the queue was last banded from.
14100
+ this.lastOrderFocus = null;
14101
+ // Reports what the camera is aimed at, rather than only where it sits.
14102
+ this.monitor = null;
13995
14103
  // All IDs that have ever been queued.
13996
14104
  // When nothing is in queue, we'll re-request the existing IDs based on timeline changes.
13997
14105
  this.allEIds = new Set();
@@ -14091,6 +14199,8 @@
14091
14199
  return;
14092
14200
  }
14093
14201
  this.eIdQueue = [];
14202
+ this.priorityCount = 0;
14203
+ this.lastOrderFocus = null;
14094
14204
  this.allEIds.clear();
14095
14205
  this.lastFoundDateTimes.clear();
14096
14206
  this.lastDateTime = null;
@@ -14100,6 +14210,89 @@
14100
14210
  return;
14101
14211
  }
14102
14212
  this.eIdQueue = Array.from(this.allEIds);
14213
+ this.priorityCount = 0;
14214
+ this.lastOrderFocus = null;
14215
+ }
14216
+ /**
14217
+ * Supplies a lookup from Entity ID to world position, letting the queue resolve what the camera
14218
+ * is nearest first. Sources that cannot answer should not call this, and are served in order.
14219
+ */
14220
+ SetPositionSource(positionOf) {
14221
+ this.positionOf = positionOf;
14222
+ this.lastOrderFocus = null;
14223
+ }
14224
+ /**
14225
+ * Supplies the view monitor, so the order follows the ground the camera is aimed at and prefers
14226
+ * what is on screen. Without one the ordering falls back to distance from the camera itself.
14227
+ */
14228
+ SetViewMonitor(monitor) {
14229
+ this.monitor = monitor;
14230
+ this.lastOrderFocus = null;
14231
+ }
14232
+ /**
14233
+ * Re-bands the pending queue so the nearest Entities are requested first.
14234
+ */
14235
+ orderQueueByCamera() {
14236
+ if (!this.positionOf || this.eIdQueue.length < EntityGatherer.MIN_ORDER_QUEUE) {
14237
+ return;
14238
+ }
14239
+ const focus = HistoricFetchOrder.FocusFrom({ viewer: this.viewer, monitor: this.monitor });
14240
+ if (!focus) {
14241
+ return;
14242
+ }
14243
+ // Re-banding on every batch would cost a full pass per request for no change in the answer.
14244
+ // Measured on the focus, so panning across the ground counts even when the eye barely moves.
14245
+ if (this.lastOrderFocus && !EntityGatherer.boundsChanged(this.lastOrderFocus, focus) &&
14246
+ Cesium.Cartesian3.distance(this.lastOrderFocus.origin, focus.origin) < EntityGatherer.REORDER_MOVE_METRES) {
14247
+ return;
14248
+ }
14249
+ this.lastOrderFocus = focus;
14250
+ const priority = this.priorityCount > 0 ? this.eIdQueue.slice(0, this.priorityCount) : [];
14251
+ const rest = this.priorityCount > 0 ? this.eIdQueue.slice(this.priorityCount) : this.eIdQueue;
14252
+ if (!rest.length) {
14253
+ return;
14254
+ }
14255
+ this.eIdQueue = priority.concat(this.bandIds(rest, focus));
14256
+ }
14257
+ /*
14258
+ * Returns whether the visible rectangle differs enough to change what counts as on screen.
14259
+ */
14260
+ static boundsChanged(a, b) {
14261
+ if (!a.bounds || !b.bounds) {
14262
+ return Boolean(a.bounds) !== Boolean(b.bounds);
14263
+ }
14264
+ return (a.bounds.north !== b.bounds.north ||
14265
+ a.bounds.south !== b.bounds.south ||
14266
+ a.bounds.east !== b.bounds.east ||
14267
+ a.bounds.west !== b.bounds.west);
14268
+ }
14269
+ /*
14270
+ * Returns the IDs reordered nearest band first, or unchanged when there is nothing to order by.
14271
+ */
14272
+ bandIds(entityIds, focus) {
14273
+ const from = focus || HistoricFetchOrder.FocusFrom({ viewer: this.viewer, monitor: this.monitor });
14274
+ if (!this.positionOf || !from || !entityIds.length) {
14275
+ return entityIds;
14276
+ }
14277
+ const banded = HistoricFetchOrder.ByCameraDistanceOf({
14278
+ focus: from,
14279
+ items: entityIds,
14280
+ posOf: (entityId) => {
14281
+ try {
14282
+ return this.positionOf(entityId);
14283
+ }
14284
+ catch {
14285
+ return null;
14286
+ }
14287
+ }
14288
+ });
14289
+ const ordered = [];
14290
+ for (const band of banded) {
14291
+ for (const entityId of band) {
14292
+ ordered.push(entityId);
14293
+ }
14294
+ }
14295
+ return ordered;
14103
14296
  }
14104
14297
  Queue(entityIds, topOfQueue = false) {
14105
14298
  if (this.disposed || !(entityIds === null || entityIds === void 0 ? void 0 : entityIds.length)) {
@@ -14113,8 +14306,12 @@
14113
14306
  // If we want it to be at the top of the queue, remove it from the queue first.
14114
14307
  if (index !== -1) {
14115
14308
  this.eIdQueue.splice(index, 1);
14309
+ if (index < this.priorityCount) {
14310
+ this.priorityCount -= 1;
14311
+ }
14116
14312
  }
14117
14313
  this.eIdQueue.unshift(eId);
14314
+ this.priorityCount += 1;
14118
14315
  changes += 1;
14119
14316
  }
14120
14317
  else if (index === -1) {
@@ -14265,7 +14462,9 @@
14265
14462
  if (this.historic && this.lastDateTime !== rTime) {
14266
14463
  break;
14267
14464
  }
14465
+ this.orderQueueByCamera();
14268
14466
  const batch = this.eIdQueue.splice(0, QUEUE_BATCH_SIZE);
14467
+ this.priorityCount = Math.max(0, this.priorityCount - batch.length);
14269
14468
  const { entities } = await BModels.Entity.GetListByIds({
14270
14469
  entityIds: batch,
14271
14470
  historicPoint: this.historic ? rTime : null,
@@ -14298,6 +14497,9 @@
14298
14497
  allEIds = allEIds.filter((eId) => requestedIds.indexOf(eId) === -1);
14299
14498
  }
14300
14499
  if (allEIds.length) {
14500
+ // A historic tick re-reads the whole set, so the near ones are worth resolving
14501
+ // first here even more than in the queue above.
14502
+ allEIds = this.bandIds(allEIds);
14301
14503
  while (allEIds.length && !this.disposed && !this.eIdQueue.length) {
14302
14504
  if (this.historic && this.lastDateTime !== rTime) {
14303
14505
  break;
@@ -14331,6 +14533,11 @@
14331
14533
  })();
14332
14534
  }
14333
14535
  }
14536
+ // Below this the whole queue clears in one or two requests, so ordering it buys nothing.
14537
+ EntityGatherer.MIN_ORDER_QUEUE = 500;
14538
+ // How far the point being looked at has to move before the queue is worth re-banding, in metres.
14539
+ // Under the nearest band's width, so a move that could change what is nearest still triggers one.
14540
+ EntityGatherer.REORDER_MOVE_METRES = 250;
14334
14541
 
14335
14542
  // ND-1641. BOOKMARKS - (DEMO) View does not match bookmark.
14336
14543
  // We have some evil hard-coded style mappings that need to be fixed.
@@ -14454,7 +14661,8 @@
14454
14661
  }
14455
14662
  Init(params) {
14456
14663
  var _a, _b;
14457
- let { viewer, api, cTileset, fallbackStyleId, styleMapping, expandSources, menuItemId, register, scenario, historic, applyStyles } = params;
14664
+ let { viewer, api, cTileset, fallbackStyleId, styleMapping, expandSources, menuItemId, register, scenario, historic, applyStyles, monitor } = params;
14665
+ this.monitor = monitor;
14458
14666
  this.viewer = viewer;
14459
14667
  this.api = api;
14460
14668
  this.cTileset = cTileset;
@@ -14510,6 +14718,14 @@
14510
14718
  }
14511
14719
  }
14512
14720
  });
14721
+ // Only Entities Sets bake coordinates into their features, so for any other source this
14722
+ // resolves nothing and the gather stays in insertion order.
14723
+ this.entityGatherer.SetViewMonitor(this.monitor);
14724
+ this.entityGatherer.SetPositionSource((entityId) => {
14725
+ var _a;
14726
+ const visual = (_a = this.getEntityRego(entityId)) === null || _a === void 0 ? void 0 : _a.visual;
14727
+ return (visual === null || visual === void 0 ? void 0 : visual._bakedPos) || null;
14728
+ });
14513
14729
  this.loaded = true;
14514
14730
  this.loadStyles();
14515
14731
  const feed = (_b = this.api) === null || _b === void 0 ? void 0 : _b.RecordChangeFeed;
@@ -19721,7 +19937,8 @@
19721
19937
  // Saves having to do a case-insensitive lookup every time.
19722
19938
  this.featurePropCache = new Map();
19723
19939
  this.featurePropsChecked = 0;
19724
- const { viewer, register: visualsManager, getters: apiGetter, item, initQueue } = params;
19940
+ const { viewer, register: visualsManager, getters: apiGetter, item, initQueue, monitor } = params;
19941
+ this.monitor = monitor;
19725
19942
  this.viewer = viewer;
19726
19943
  this.getters = apiGetter;
19727
19944
  this.item = item;
@@ -19830,7 +20047,8 @@
19830
20047
  menuItemId: this.item.id,
19831
20048
  register: this.visualsManager,
19832
20049
  historic: Manager.IsHistoric(this.item),
19833
- applyStyles: Boolean(this.item.ApplyStyles)
20050
+ applyStyles: Boolean(this.item.ApplyStyles),
20051
+ monitor: this.monitor
19834
20052
  });
19835
20053
  }
19836
20054
  this.onCTilesetLoad();
@@ -19990,6 +20208,16 @@
19990
20208
  else if (lowered === "internalid") {
19991
20209
  this.featurePropCache.set("internalId", prop);
19992
20210
  }
20211
+ else if (lowered === "latitude") {
20212
+ this.featurePropCache.set("latitude", prop);
20213
+ }
20214
+ else if (lowered === "longitude") {
20215
+ this.featurePropCache.set("longitude", prop);
20216
+ }
20217
+ // b3dm names the baked height TerrainHeight, the glTF metadata table calls it Altitude.
20218
+ else if (lowered === "terrainheight" || lowered === "altitude") {
20219
+ this.featurePropCache.set("height", prop);
20220
+ }
19993
20221
  }
19994
20222
  }
19995
20223
  Dispose() {
@@ -20144,7 +20372,8 @@
20144
20372
  menuItemId: this.item.id,
20145
20373
  register: this.visualsManager,
20146
20374
  historic: Manager.IsHistoric(this.item),
20147
- applyStyles: Boolean(this.item.ApplyStyles)
20375
+ applyStyles: Boolean(this.item.ApplyStyles),
20376
+ monitor: this.monitor
20148
20377
  });
20149
20378
  this.queueHandoffRestyle(this.visualsManager.GetRegos({ menuItemId: this.item.id }));
20150
20379
  }
@@ -20187,6 +20416,45 @@
20187
20416
  }
20188
20417
  }, 10);
20189
20418
  }
20419
+ /*
20420
+ * Reads a numeric feature property, returning null for anything that is not one.
20421
+ */
20422
+ static readNumber(feature, prop) {
20423
+ const raw = feature.getProperty(prop);
20424
+ // Coercing straight to a number would turn null, undefined and "" into a valid zero,
20425
+ // which for a coordinate is a real place off the coast of Africa.
20426
+ if (raw == null || raw === "" || typeof raw === "boolean") {
20427
+ return null;
20428
+ }
20429
+ const value = +raw;
20430
+ return isFinite(value) ? value : null;
20431
+ }
20432
+ /*
20433
+ * Returns the world position an Entities Set bakes into each feature, or null when it has none.
20434
+ */
20435
+ readBakedPos(feature) {
20436
+ const latProp = this.featurePropCache.get("latitude");
20437
+ const lonProp = this.featurePropCache.get("longitude");
20438
+ if (!latProp || !lonProp) {
20439
+ return null;
20440
+ }
20441
+ try {
20442
+ const lat = Manager.readNumber(feature, latProp);
20443
+ const lon = Manager.readNumber(feature, lonProp);
20444
+ // Degrees, per the batch table the generator writes. The range check doubles as a
20445
+ // guard against a source that bakes radians, which would otherwise read as valid.
20446
+ if (lat == null || lon == null || Math.abs(lat) > 90 || Math.abs(lon) > 180) {
20447
+ return null;
20448
+ }
20449
+ // A height that will not read is worth losing, the position it belongs to is not.
20450
+ const heightProp = this.featurePropCache.get("height");
20451
+ const height = heightProp ? Manager.readNumber(feature, heightProp) : 0;
20452
+ return Cesium.Cartesian3.fromDegrees(lon, lat, height !== null && height !== void 0 ? height : 0);
20453
+ }
20454
+ catch {
20455
+ return null;
20456
+ }
20457
+ }
20190
20458
  mapTilesetFeature(feature) {
20191
20459
  var _a, _b, _c;
20192
20460
  this.evaluateFeatureProps(feature);
@@ -20267,6 +20535,12 @@
20267
20535
  rego.internalId = parsed;
20268
20536
  }
20269
20537
  }
20538
+ // Stamped on the feature the properties came from, so a sibling keeps its own position
20539
+ // and nothing has to be cleaned up when the tile unloads.
20540
+ const bakedPos = this.readBakedPos(feature);
20541
+ if (bakedPos) {
20542
+ feature._bakedPos = bakedPos;
20543
+ }
20270
20544
  this.loadedCesiumEntities[rego.entityId] = rego;
20271
20545
  this.visualsManager.AddRego({
20272
20546
  rego,
@@ -24131,6 +24405,7 @@
24131
24405
  viewer: this.viewer,
24132
24406
  register: this.visualsRegister,
24133
24407
  getters: this.getters,
24408
+ monitor: this.sharedMonitor,
24134
24409
  item: params.item,
24135
24410
  initQueue: this.tilesetInitQueue
24136
24411
  });
@@ -40886,9 +41161,11 @@
40886
41161
  return;
40887
41162
  }
40888
41163
  let next = -1;
41164
+ let isTarget = false;
40889
41165
  if (this.pendingIndex !== -1) {
40890
41166
  next = this.pendingIndex;
40891
41167
  this.pendingIndex = -1;
41168
+ isTarget = true;
40892
41169
  }
40893
41170
  else {
40894
41171
  next = this.nearestUncachedFrame();
@@ -40897,7 +41174,7 @@
40897
41174
  return;
40898
41175
  }
40899
41176
  this.inFlightIndex = next;
40900
- this.fetchAndTint(next)
41177
+ this.fetchAndTint(next, isTarget)
40901
41178
  .catch(() => {
40902
41179
  // Eating it.
40903
41180
  })
@@ -40908,7 +41185,7 @@
40908
41185
  }
40909
41186
  });
40910
41187
  }
40911
- async fetchAndTint(idx) {
41188
+ async fetchAndTint(idx, isTarget = true) {
40912
41189
  const tiles = this.tiles;
40913
41190
  // Every tile's frame for one timestep sits contiguously, because the generator writes
40914
41191
  // frame major. So a tiled frame is still ONE range request, not one per tile.
@@ -40940,10 +41217,25 @@
40940
41217
  this.frameCache[idx] = decoded.pixels;
40941
41218
  this.frameDims[idx] = { width: decoded.width, height: decoded.height };
40942
41219
  this.valueCache[idx] = decoded.valuePixels || null;
40943
- if (idx === this.currentFrameIndex) {
41220
+ if (this.shouldPresent(idx, isTarget)) {
40944
41221
  this.beginCrossfadeTo(idx);
40945
41222
  }
40946
41223
  }
41224
+ /*
41225
+ * Whether a frame that has just decoded is worth putting on screen.
41226
+ */
41227
+ shouldPresent(idx, isTarget) {
41228
+ if (idx === this.currentFrameIndex) {
41229
+ return true;
41230
+ }
41231
+ // A prefetch is for a frame nobody asked to see, so it stays in the cache.
41232
+ if (!isTarget) {
41233
+ return false;
41234
+ }
41235
+ // Dragging the timeline retargets faster than a frame can be fetched, so the frame that
41236
+ // lands is rarely the one now under the playhead.
41237
+ return !this.frameCache[this.currentFrameIndex];
41238
+ }
40947
41239
  /*
40948
41240
  * Draws every tile of one frame into a single raster covering the drape extent.
40949
41241
  */
@@ -45481,7 +45773,7 @@ void main() {
45481
45773
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
45482
45774
  })(exports.StyleUtils || (exports.StyleUtils = {}));
45483
45775
 
45484
- const VERSION = "7.2.4";
45776
+ const VERSION = "7.2.5";
45485
45777
  /**
45486
45778
  * Updates the environment instance used by bruce-cesium to one specified.
45487
45779
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.