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
@@ -1,6 +1,6 @@
1
1
  import * as Cesium from 'cesium';
2
- import { Cartographic, ColorMaterialProperty, Entity, Color, ConstantProperty, CallbackProperty, Primitive, Cesium3DTileFeature, DistanceDisplayCondition, Math as Math$1, Cartesian3, JulianDate, Quaternion, Transforms, HeadingPitchRoll, Matrix4, HeightReference, ColorBlendMode, ShadowMode, ClassificationType, Model, ShaderSource, Ellipsoid, Geometry, GeometryAttribute, ComponentDatatype, PrimitiveType, BoundingSphere, GeometryPipeline, sampleTerrain, Texture, PixelFormat, Sampler, TextureWrap, TextureMinificationFilter, TextureMagnificationFilter, VertexArray, BufferUsage, ShaderProgram, RenderState, BlendingState, DrawCommand, Pass, Cartesian2, HorizontalOrigin, VerticalOrigin, ConstantPositionProperty, ImageMaterialProperty, PolygonHierarchy, PolylineGraphics, ArcType, CornerType, Rectangle, SceneTransforms, NearFarScalar, Matrix3, KmlDataSource, GeoJsonDataSource, SceneMode, HeadingPitchRange, Ion, IonResource, Cesium3DTileStyle, Cesium3DTileColorBlendMode, Cesium3DTileset, OrthographicFrustum, EasingFunction, EllipsoidTerrainProvider, IonImageryProvider, createWorldImagery, createWorldImageryAsync, BingMapsImageryProvider, BingMapsStyle, MapboxImageryProvider, MapboxStyleImageryProvider, ArcGisMapServerImageryProvider, OpenStreetMapImageryProvider, UrlTemplateImageryProvider, GridImageryProvider, GeographicTilingScheme, ImageryLayer, TileMapServiceImageryProvider, CesiumTerrainProvider, Intersect, ModelGraphics, PolygonGraphics, CorridorGraphics, PointGraphics, BillboardGraphics, EllipseGraphics, PolylineDashMaterialProperty, CustomDataSource, GeometryInstance, PolygonPipeline, CesiumInspector, defined, ClockRange, DynamicEnvironmentMapManager, EllipsoidGeodesic, sampleTerrainMostDetailed, ScreenSpaceEventHandler, ScreenSpaceEventType, Fullscreen, CzmlDataSource } from 'cesium';
3
- import { Cartes, Entity as Entity$1, ProjectViewTile, ENVIRONMENT, Calculator, ClientFile, EntityLod, EntityTag, EntityType, LRUCache, ObjectUtils, Style, Api, Bounds, Color as Color$1, Geometry as Geometry$1, Carto, MenuItem, ProjectView, ProjectViewBookmark, Tileset, ZoomControl, BruceEvent, EntityCoords, DataLab, DelayQueue, BruceApi, AccountConcept, RecordChangeFeed, EntityRelation, Camera, ProgramKey, ProjectViewLegacyTile, EntitySource, Account, AccountLimits, Session, UrlUtils, EntityAttachment, EntityAttachmentType, EntityAttribute, MathUtils, ProjectViewBookmarkGroup, EntityRelationType, EntityHistoricData } from 'bruce-models';
2
+ import { Cartographic, ColorMaterialProperty, Entity, Color, ConstantProperty, CallbackProperty, Primitive, Cesium3DTileFeature, ShaderSource, Cartesian3, Math as Math$1, Ellipsoid, Transforms, Matrix4, Geometry, GeometryAttribute, ComponentDatatype, PrimitiveType, BoundingSphere, GeometryPipeline, sampleTerrain, Texture, PixelFormat, Sampler, TextureWrap, TextureMinificationFilter, TextureMagnificationFilter, VertexArray, BufferUsage, ShaderProgram, RenderState, BlendingState, DrawCommand, Pass, Cartesian2, HeightReference, ColorBlendMode, HeadingPitchRoll, ShadowMode, ClassificationType, Model, DistanceDisplayCondition, JulianDate, Quaternion, ArcType, CornerType, ConstantPositionProperty, HorizontalOrigin, VerticalOrigin, SceneTransforms, ImageMaterialProperty, PolygonHierarchy, PolylineGraphics, Rectangle, KmlDataSource, NearFarScalar, Matrix3, GeoJsonDataSource, SceneMode, Cesium3DTileStyle, HeadingPitchRange, Ion, IonResource, Cesium3DTileColorBlendMode, EllipsoidTerrainProvider, IonImageryProvider, createWorldImagery, createWorldImageryAsync, BingMapsImageryProvider, BingMapsStyle, MapboxImageryProvider, MapboxStyleImageryProvider, ArcGisMapServerImageryProvider, OpenStreetMapImageryProvider, UrlTemplateImageryProvider, GridImageryProvider, GeographicTilingScheme, ImageryLayer, TileMapServiceImageryProvider, CesiumTerrainProvider, Cesium3DTileset, OrthographicFrustum, EasingFunction, Intersect, EllipsoidGeodesic, sampleTerrainMostDetailed, defined, PolygonPipeline, ModelGraphics, PolygonGraphics, CorridorGraphics, PointGraphics, BillboardGraphics, EllipseGraphics, PolylineDashMaterialProperty, GeometryInstance, CesiumInspector, ClockRange, DynamicEnvironmentMapManager, ScreenSpaceEventHandler, ScreenSpaceEventType, CustomDataSource, Fullscreen, CzmlDataSource } from 'cesium';
3
+ import { Cartes, Entity as Entity$1, ProjectViewTile, ENVIRONMENT, Calculator, ClientFile, EntityLod, EntityTag, EntityType, LRUCache, ObjectUtils, Style, Carto, Geometry as Geometry$1, Api, MenuItem, ProjectView, ProjectViewBookmark, Tileset, ZoomControl, BruceEvent, EntityCoords, Bounds, Color as Color$1, DataLab, DelayQueue, EntityRelation, BruceApi, AccountConcept, RecordChangeFeed, ProgramKey, ProjectViewLegacyTile, EntitySource, Camera, Account, AccountLimits, Session, UrlUtils, EntityAttachment, EntityAttachmentType, EntityAttribute, MathUtils, ProjectViewBookmarkGroup, EntityRelationType, EntityHistoricData } from 'bruce-models';
4
4
 
5
5
  /**
6
6
  * Ensures a number is returned from a given value.
@@ -2889,6 +2889,36 @@ function SetCPropIfChanged(params) {
2889
2889
  function ColorToCColor(color) {
2890
2890
  return new Color(color.red ? color.red / 255 : 0, color.green ? color.green / 255 : 0, color.blue ? color.blue / 255 : 0, color.alpha);
2891
2891
  }
2892
+ const degreesCache = new WeakMap();
2893
+ /**
2894
+ * Returns a world position's latitude and longitude in degrees, or null when it has none.
2895
+ * @param pos
2896
+ */
2897
+ function CartesianToDegrees(pos) {
2898
+ if (!pos) {
2899
+ return null;
2900
+ }
2901
+ const cached = degreesCache.get(pos);
2902
+ if (cached) {
2903
+ return cached;
2904
+ }
2905
+ let carto;
2906
+ try {
2907
+ carto = Cartographic.fromCartesian(pos);
2908
+ }
2909
+ catch {
2910
+ return null;
2911
+ }
2912
+ if (!carto) {
2913
+ return null;
2914
+ }
2915
+ const degrees = {
2916
+ lat: Math$1.toDegrees(carto.latitude),
2917
+ lon: Math$1.toDegrees(carto.longitude)
2918
+ };
2919
+ degreesCache.set(pos, degrees);
2920
+ return degrees;
2921
+ }
2892
2922
  /**
2893
2923
  * Removes duplicate points in a row and returns a new array.
2894
2924
  * Passed array is not mutated.
@@ -10758,41 +10788,107 @@ var HistoricFetchOrder;
10758
10788
  return Math.round(minMs + ((maxMs - minMs) * progress));
10759
10789
  }
10760
10790
  HistoricFetchOrder.IntervalFor = IntervalFor;
10791
+ // Upper bounds in metres for the distance bands, nearest first.
10792
+ HistoricFetchOrder.DEFAULT_BANDS = [500, 2000, 10000, 50000];
10761
10793
  /**
10762
- * Returns the Entities grouped into bands by distance from the camera, nearest band first.
10763
- *
10764
- * Bands rather than a true sort, so a small camera move does not reshuffle the whole queue and
10765
- * restart it. What the user is looking at resolves first either way.
10794
+ * Resolves what to order by from the view monitor, falling back to the camera itself.
10766
10795
  */
10767
- function ByCameraDistance(params) {
10796
+ function FocusFrom(params) {
10768
10797
  var _a;
10769
- const { viewer, entities } = params;
10770
- const bands = params.bands || [500, 2000, 10000, 50000];
10771
- if (!(entities === null || entities === void 0 ? void 0 : entities.length)) {
10798
+ const { viewer, monitor } = params;
10799
+ let origin = null;
10800
+ let bounds = null;
10801
+ try {
10802
+ // Null whenever the view has no ground intersection, eg. a camera aimed at the sky.
10803
+ const target = monitor === null || monitor === void 0 ? void 0 : monitor.GetTarget();
10804
+ if (target && isFinite(target.latitude) && isFinite(target.longitude)) {
10805
+ origin = Cartesian3.fromDegrees(target.longitude, target.latitude);
10806
+ }
10807
+ bounds = (monitor === null || monitor === void 0 ? void 0 : monitor.GetBounds()) || null;
10808
+ }
10809
+ catch {
10810
+ origin = null;
10811
+ bounds = null;
10812
+ }
10813
+ if (!origin) {
10814
+ origin = (_a = viewer === null || viewer === void 0 ? void 0 : viewer.camera) === null || _a === void 0 ? void 0 : _a.position;
10815
+ }
10816
+ if (!origin) {
10817
+ return null;
10818
+ }
10819
+ return bounds ? { origin, bounds } : { origin };
10820
+ }
10821
+ HistoricFetchOrder.FocusFrom = FocusFrom;
10822
+ /**
10823
+ * Returns the Entities grouped nearest first, on screen ahead of off screen.
10824
+ */
10825
+ function ByCameraDistance(params) {
10826
+ const { viewer, entities, monitor } = params;
10827
+ return ByCameraDistanceOf({
10828
+ focus: FocusFrom({ viewer, monitor }),
10829
+ items: entities,
10830
+ posOf: (entity) => SafePos(entity, viewer),
10831
+ bands: params.bands
10832
+ });
10833
+ }
10834
+ HistoricFetchOrder.ByCameraDistance = ByCameraDistance;
10835
+ /**
10836
+ * Bands anything that can name its own world position, on the same bounds as the Entity form.
10837
+ */
10838
+ function ByCameraDistanceOf(params) {
10839
+ const { focus, items, posOf } = params;
10840
+ const bands = params.bands || HistoricFetchOrder.DEFAULT_BANDS;
10841
+ if (!(items === null || items === void 0 ? void 0 : items.length)) {
10772
10842
  return [];
10773
10843
  }
10774
- const cameraPos = (_a = viewer === null || viewer === void 0 ? void 0 : viewer.camera) === null || _a === void 0 ? void 0 : _a.position;
10775
- if (!cameraPos) {
10776
- return [entities];
10844
+ if (!(focus === null || focus === void 0 ? void 0 : focus.origin)) {
10845
+ return [items];
10846
+ }
10847
+ // One band past the named bounds, for everything beyond the last of them. Then the same
10848
+ // again for what is off screen, so nothing visible waits behind something that is not.
10849
+ const perTier = bands.length + 1;
10850
+ const offScreenTier = focus.bounds ? perTier : 0;
10851
+ const placeless = offScreenTier + perTier;
10852
+ const grouped = [];
10853
+ for (let i = 0; i <= placeless; i++) {
10854
+ grouped.push([]);
10777
10855
  }
10778
- // One band past the named bounds, for everything beyond the last of them.
10779
- const banded = bands.map(() => []).concat([[]]);
10780
- for (const entity of entities) {
10781
- const pos = SafePos(entity, viewer);
10856
+ for (const item of items) {
10857
+ // An item with no position falls to the last group rather than blocking the near ones.
10858
+ const pos = posOf(item);
10782
10859
  if (!pos) {
10783
- banded[banded.length - 1].push(entity);
10860
+ grouped[placeless].push(item);
10784
10861
  continue;
10785
10862
  }
10786
- const distance = Cartesian3.distance(cameraPos, pos);
10787
- let index = bands.findIndex(bound => distance <= bound);
10788
- if (index < 0) {
10789
- index = banded.length - 1;
10863
+ const distance = Cartesian3.distance(focus.origin, pos);
10864
+ let band = bands.findIndex(bound => distance <= bound);
10865
+ if (band < 0) {
10866
+ band = bands.length;
10790
10867
  }
10791
- banded[index].push(entity);
10868
+ grouped[(focus.bounds && !InBounds(pos, focus.bounds)) ? offScreenTier + band : band].push(item);
10792
10869
  }
10793
- return banded.filter(band => band.length > 0);
10870
+ return grouped.filter(group => group.length > 0);
10794
10871
  }
10795
- HistoricFetchOrder.ByCameraDistance = ByCameraDistance;
10872
+ HistoricFetchOrder.ByCameraDistanceOf = ByCameraDistanceOf;
10873
+ /*
10874
+ * Returns whether a world position sits inside a ground rectangle given in degrees.
10875
+ */
10876
+ function InBounds(pos, bounds) {
10877
+ const degrees = CartesianToDegrees(pos);
10878
+ if (!degrees) {
10879
+ return false;
10880
+ }
10881
+ const { lat, lon } = degrees;
10882
+ if (lat < bounds.south || lat > bounds.north) {
10883
+ return false;
10884
+ }
10885
+ // A view straddling the antimeridian reports west east of east, so the span is the outside.
10886
+ if (bounds.west > bounds.east) {
10887
+ return lon >= bounds.west || lon <= bounds.east;
10888
+ }
10889
+ return lon >= bounds.west && lon <= bounds.east;
10890
+ }
10891
+ HistoricFetchOrder.InBounds = InBounds;
10796
10892
  /*
10797
10893
  * Returns an Entity's world position, or null when it has none to place it by.
10798
10894
  */
@@ -11110,7 +11206,7 @@ class HistoricGatherer {
11110
11206
  }
11111
11207
  if (plan.detail) {
11112
11208
  const bands = entities.length > HistoricFetchOrder.SMALL_SET
11113
- ? HistoricFetchOrder.ByCameraDistance({ viewer, entities })
11209
+ ? HistoricFetchOrder.ByCameraDistance({ viewer, entities, monitor: this.params.monitor })
11114
11210
  : [entities];
11115
11211
  for (const range of this.cache.MissingRanges(plan.detail.start, plan.detail.stop)) {
11116
11212
  if (this.disposed) {
@@ -11279,6 +11375,7 @@ var EntitiesRenderManager;
11279
11375
  this.onSeriesDiscovered = onSeriesDiscovered;
11280
11376
  this.historicGatherer = new HistoricGatherer({
11281
11377
  viewer: this.viewer,
11378
+ monitor: this.monitor,
11282
11379
  getApi: () => this.apiGetter.getApi(),
11283
11380
  getAttrKey: () => { var _a; return (_a = this.item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historicAttrKey; },
11284
11381
  // A second either side, to take in records sitting exactly on the boundary.
@@ -13395,6 +13492,7 @@ var EntitiesIdsRenderManager;
13395
13492
  this.visualsManager = visualsManager;
13396
13493
  this.historicGatherer = new HistoricGatherer({
13397
13494
  viewer: this.viewer,
13495
+ monitor: this.monitor,
13398
13496
  getApi: () => this.apiGetter.getApi(),
13399
13497
  getAttrKey: () => { var _a; return (_a = this.item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historicAttrKey; },
13400
13498
  // Padding either side of a requested range, for clock desync between us and the server.
@@ -14033,6 +14131,18 @@ class EntityGatherer {
14033
14131
  this.lastFoundDateTimes = new Map();
14034
14132
  // ID of Entity IDs that need to be requested.
14035
14133
  this.eIdQueue = [];
14134
+ // Membership mirror of eIdQueue for quick checks.
14135
+ this.eIdQueued = new Set();
14136
+ // Resolves where an Entity sits without reading its record, when the caller can say.
14137
+ // Unset leaves the queue in insertion order, which is what sources that bake no coordinates get.
14138
+ this.positionOf = null;
14139
+ // How many IDs at the front of the queue were explicitly asked for first.
14140
+ // Camera ordering leaves those alone, since an explicit re-render outranks proximity.
14141
+ this.priorityCount = 0;
14142
+ // View focus the queue was last banded from.
14143
+ this.lastOrderFocus = null;
14144
+ // Reports what the camera is aimed at, rather than only where it sits.
14145
+ this.monitor = null;
14036
14146
  // All IDs that have ever been queued.
14037
14147
  // When nothing is in queue, we'll re-request the existing IDs based on timeline changes.
14038
14148
  this.allEIds = new Set();
@@ -14132,6 +14242,9 @@ class EntityGatherer {
14132
14242
  return;
14133
14243
  }
14134
14244
  this.eIdQueue = [];
14245
+ this.eIdQueued.clear();
14246
+ this.priorityCount = 0;
14247
+ this.lastOrderFocus = null;
14135
14248
  this.allEIds.clear();
14136
14249
  this.lastFoundDateTimes.clear();
14137
14250
  this.lastDateTime = null;
@@ -14141,29 +14254,148 @@ class EntityGatherer {
14141
14254
  return;
14142
14255
  }
14143
14256
  this.eIdQueue = Array.from(this.allEIds);
14257
+ this.eIdQueued = new Set(this.eIdQueue);
14258
+ this.priorityCount = 0;
14259
+ this.lastOrderFocus = null;
14260
+ }
14261
+ /**
14262
+ * Supplies a lookup from Entity ID to world position, letting the queue resolve what the camera
14263
+ * is nearest first. Sources that cannot answer should not call this, and are served in order.
14264
+ */
14265
+ SetPositionSource(positionOf) {
14266
+ this.positionOf = positionOf;
14267
+ this.lastOrderFocus = null;
14268
+ }
14269
+ /**
14270
+ * Supplies the view monitor, so the order follows the ground the camera is aimed at and prefers
14271
+ * what is on screen. Without one the ordering falls back to distance from the camera itself.
14272
+ */
14273
+ SetViewMonitor(monitor) {
14274
+ this.monitor = monitor;
14275
+ this.lastOrderFocus = null;
14276
+ }
14277
+ /**
14278
+ * Re-bands the pending queue so the nearest Entities are requested first.
14279
+ */
14280
+ orderQueueByCamera() {
14281
+ if (!this.positionOf || this.eIdQueue.length < EntityGatherer.MIN_ORDER_QUEUE) {
14282
+ return;
14283
+ }
14284
+ const focus = HistoricFetchOrder.FocusFrom({ viewer: this.viewer, monitor: this.monitor });
14285
+ if (!focus) {
14286
+ return;
14287
+ }
14288
+ // Re-banding on every batch would cost a full pass per request for no change in the answer.
14289
+ // Measured on the focus, so panning across the ground counts even when the eye barely moves.
14290
+ if (this.lastOrderFocus && !EntityGatherer.boundsChanged(this.lastOrderFocus, focus) &&
14291
+ Cartesian3.distance(this.lastOrderFocus.origin, focus.origin) < EntityGatherer.REORDER_MOVE_METRES) {
14292
+ return;
14293
+ }
14294
+ this.lastOrderFocus = focus;
14295
+ const priority = this.priorityCount > 0 ? this.eIdQueue.slice(0, this.priorityCount) : [];
14296
+ const rest = this.priorityCount > 0 ? this.eIdQueue.slice(this.priorityCount) : this.eIdQueue;
14297
+ if (!rest.length) {
14298
+ return;
14299
+ }
14300
+ this.eIdQueue = priority.concat(this.bandIds(rest, focus));
14301
+ }
14302
+ /*
14303
+ * Returns whether the visible rectangle differs enough to change what counts as on screen.
14304
+ */
14305
+ static boundsChanged(a, b) {
14306
+ if (!a.bounds || !b.bounds) {
14307
+ return Boolean(a.bounds) !== Boolean(b.bounds);
14308
+ }
14309
+ return (a.bounds.north !== b.bounds.north ||
14310
+ a.bounds.south !== b.bounds.south ||
14311
+ a.bounds.east !== b.bounds.east ||
14312
+ a.bounds.west !== b.bounds.west);
14313
+ }
14314
+ /*
14315
+ * Returns the IDs reordered nearest band first, or unchanged when there is nothing to order by.
14316
+ */
14317
+ bandIds(entityIds, focus) {
14318
+ const from = focus || HistoricFetchOrder.FocusFrom({ viewer: this.viewer, monitor: this.monitor });
14319
+ if (!this.positionOf || !from || !entityIds.length) {
14320
+ return entityIds;
14321
+ }
14322
+ const banded = HistoricFetchOrder.ByCameraDistanceOf({
14323
+ focus: from,
14324
+ items: entityIds,
14325
+ posOf: (entityId) => {
14326
+ try {
14327
+ return this.positionOf(entityId);
14328
+ }
14329
+ catch {
14330
+ return null;
14331
+ }
14332
+ }
14333
+ });
14334
+ const ordered = [];
14335
+ for (const band of banded) {
14336
+ for (const entityId of band) {
14337
+ ordered.push(entityId);
14338
+ }
14339
+ }
14340
+ return ordered;
14144
14341
  }
14145
14342
  Queue(entityIds, topOfQueue = false) {
14146
14343
  if (this.disposed || !(entityIds === null || entityIds === void 0 ? void 0 : entityIds.length)) {
14147
14344
  return;
14148
14345
  }
14149
14346
  let changes = 0;
14150
- for (let i = 0; i < entityIds.length; i++) {
14151
- const eId = entityIds[i];
14152
- const index = this.eIdQueue.indexOf(eId);
14153
- if (topOfQueue) {
14154
- // If we want it to be at the top of the queue, remove it from the queue first.
14155
- if (index !== -1) {
14156
- this.eIdQueue.splice(index, 1);
14157
- }
14158
- this.eIdQueue.unshift(eId);
14159
- changes += 1;
14347
+ if (topOfQueue) {
14348
+ // Walked back to front so a repeated ID keeps the position of its last occurrence, then
14349
+ // laid down in that order, which is where inserting one at a time leaves them.
14350
+ const toFront = [];
14351
+ const promoting = new Set();
14352
+ let anyAlreadyQueued = false;
14353
+ for (let i = entityIds.length - 1; i >= 0; i--) {
14354
+ const eId = entityIds[i];
14355
+ this.allEIds.add(eId);
14356
+ if (promoting.has(eId)) {
14357
+ continue;
14358
+ }
14359
+ promoting.add(eId);
14360
+ toFront.push(eId);
14361
+ if (this.eIdQueued.has(eId)) {
14362
+ anyAlreadyQueued = true;
14363
+ }
14364
+ }
14365
+ if (anyAlreadyQueued && this.eIdQueue.length) {
14366
+ const kept = [];
14367
+ let removedFromPriority = 0;
14368
+ for (let i = 0; i < this.eIdQueue.length; i++) {
14369
+ const eId = this.eIdQueue[i];
14370
+ if (promoting.has(eId)) {
14371
+ if (i < this.priorityCount) {
14372
+ removedFromPriority += 1;
14373
+ }
14374
+ continue;
14375
+ }
14376
+ kept.push(eId);
14377
+ }
14378
+ this.eIdQueue = kept;
14379
+ this.priorityCount = Math.max(0, this.priorityCount - removedFromPriority);
14160
14380
  }
14161
- else if (index === -1) {
14381
+ this.eIdQueue = toFront.concat(this.eIdQueue);
14382
+ this.priorityCount += toFront.length;
14383
+ for (let i = 0; i < toFront.length; i++) {
14384
+ this.eIdQueued.add(toFront[i]);
14385
+ }
14386
+ changes = toFront.length;
14387
+ }
14388
+ else {
14389
+ for (let i = 0; i < entityIds.length; i++) {
14390
+ const eId = entityIds[i];
14391
+ this.allEIds.add(eId);
14392
+ if (this.eIdQueued.has(eId)) {
14393
+ continue;
14394
+ }
14395
+ this.eIdQueued.add(eId);
14162
14396
  this.eIdQueue.push(eId);
14163
14397
  changes += 1;
14164
14398
  }
14165
- // Flag that we've seen this ID.
14166
- this.allEIds.add(entityIds[i]);
14167
14399
  }
14168
14400
  if (!changes) {
14169
14401
  return;
@@ -14297,7 +14529,7 @@ class EntityGatherer {
14297
14529
  this.emitEntities(toEmit, tags);
14298
14530
  };
14299
14531
  const QUEUE_BATCH_SIZE = 500;
14300
- const requestedIds = [];
14532
+ const requestedIds = new Set();
14301
14533
  // If we have a queue, we need to request those first.
14302
14534
  if (this.eIdQueue.length) {
14303
14535
  const total = this.eIdQueue.length;
@@ -14306,7 +14538,12 @@ class EntityGatherer {
14306
14538
  if (this.historic && this.lastDateTime !== rTime) {
14307
14539
  break;
14308
14540
  }
14541
+ this.orderQueueByCamera();
14309
14542
  const batch = this.eIdQueue.splice(0, QUEUE_BATCH_SIZE);
14543
+ this.priorityCount = Math.max(0, this.priorityCount - batch.length);
14544
+ for (let i = 0; i < batch.length; i++) {
14545
+ this.eIdQueued.delete(batch[i]);
14546
+ }
14310
14547
  const { entities } = await Entity$1.GetListByIds({
14311
14548
  entityIds: batch,
14312
14549
  historicPoint: this.historic ? rTime : null,
@@ -14315,7 +14552,9 @@ class EntityGatherer {
14315
14552
  maxSearchTimeSec: 60 * 2
14316
14553
  });
14317
14554
  handleResponse(batch, entities);
14318
- requestedIds.push(...batch);
14555
+ for (let i = 0; i < batch.length; i++) {
14556
+ requestedIds.add(batch[i]);
14557
+ }
14319
14558
  done += batch.length;
14320
14559
  if (this._onQueueProgress) {
14321
14560
  let progress = (done / total) * 100;
@@ -14335,10 +14574,13 @@ class EntityGatherer {
14335
14574
  // Now run through all IDs that we've seen and request them.
14336
14575
  // We'll skip those that we just requested.
14337
14576
  let allEIds = Array.from(this.allEIds);
14338
- if (requestedIds.length) {
14339
- allEIds = allEIds.filter((eId) => requestedIds.indexOf(eId) === -1);
14577
+ if (requestedIds.size) {
14578
+ allEIds = allEIds.filter((eId) => !requestedIds.has(eId));
14340
14579
  }
14341
14580
  if (allEIds.length) {
14581
+ // A historic tick re-reads the whole set, so the near ones are worth resolving
14582
+ // first here even more than in the queue above.
14583
+ allEIds = this.bandIds(allEIds);
14342
14584
  while (allEIds.length && !this.disposed && !this.eIdQueue.length) {
14343
14585
  if (this.historic && this.lastDateTime !== rTime) {
14344
14586
  break;
@@ -14352,7 +14594,9 @@ class EntityGatherer {
14352
14594
  maxSearchTimeSec: 60 * 2
14353
14595
  });
14354
14596
  handleResponse(batch, entities);
14355
- requestedIds.push(...batch);
14597
+ for (let i = 0; i < batch.length; i++) {
14598
+ requestedIds.add(batch[i]);
14599
+ }
14356
14600
  }
14357
14601
  }
14358
14602
  // If we had leftovers because we stopped early, we need to re-run the tick.
@@ -14372,6 +14616,11 @@ class EntityGatherer {
14372
14616
  })();
14373
14617
  }
14374
14618
  }
14619
+ // Below this the whole queue clears in one or two requests, so ordering it buys nothing.
14620
+ EntityGatherer.MIN_ORDER_QUEUE = 500;
14621
+ // How far the point being looked at has to move before the queue is worth re-banding, in metres.
14622
+ // Under the nearest band's width, so a move that could change what is nearest still triggers one.
14623
+ EntityGatherer.REORDER_MOVE_METRES = 250;
14375
14624
 
14376
14625
  // ND-1641. BOOKMARKS - (DEMO) View does not match bookmark.
14377
14626
  // We have some evil hard-coded style mappings that need to be fixed.
@@ -14495,7 +14744,8 @@ class TilesetStyler {
14495
14744
  }
14496
14745
  Init(params) {
14497
14746
  var _a, _b;
14498
- let { viewer, api, cTileset, fallbackStyleId, styleMapping, expandSources, menuItemId, register, scenario, historic, applyStyles } = params;
14747
+ let { viewer, api, cTileset, fallbackStyleId, styleMapping, expandSources, menuItemId, register, scenario, historic, applyStyles, monitor } = params;
14748
+ this.monitor = monitor;
14499
14749
  this.viewer = viewer;
14500
14750
  this.api = api;
14501
14751
  this.cTileset = cTileset;
@@ -14551,6 +14801,14 @@ class TilesetStyler {
14551
14801
  }
14552
14802
  }
14553
14803
  });
14804
+ // Only Entities Sets bake coordinates into their features, so for any other source this
14805
+ // resolves nothing and the gather stays in insertion order.
14806
+ this.entityGatherer.SetViewMonitor(this.monitor);
14807
+ this.entityGatherer.SetPositionSource((entityId) => {
14808
+ var _a;
14809
+ const visual = (_a = this.getEntityRego(entityId)) === null || _a === void 0 ? void 0 : _a.visual;
14810
+ return (visual === null || visual === void 0 ? void 0 : visual._bakedPos) || null;
14811
+ });
14554
14812
  this.loaded = true;
14555
14813
  this.loadStyles();
14556
14814
  const feed = (_b = this.api) === null || _b === void 0 ? void 0 : _b.RecordChangeFeed;
@@ -19774,7 +20032,8 @@ var TilesetEntitiesRenderManager;
19774
20032
  // Saves having to do a case-insensitive lookup every time.
19775
20033
  this.featurePropCache = new Map();
19776
20034
  this.featurePropsChecked = 0;
19777
- const { viewer, register: visualsManager, getters: apiGetter, item, initQueue } = params;
20035
+ const { viewer, register: visualsManager, getters: apiGetter, item, initQueue, monitor } = params;
20036
+ this.monitor = monitor;
19778
20037
  this.viewer = viewer;
19779
20038
  this.getters = apiGetter;
19780
20039
  this.item = item;
@@ -19883,7 +20142,8 @@ var TilesetEntitiesRenderManager;
19883
20142
  menuItemId: this.item.id,
19884
20143
  register: this.visualsManager,
19885
20144
  historic: Manager.IsHistoric(this.item),
19886
- applyStyles: Boolean(this.item.ApplyStyles)
20145
+ applyStyles: Boolean(this.item.ApplyStyles),
20146
+ monitor: this.monitor
19887
20147
  });
19888
20148
  }
19889
20149
  this.onCTilesetLoad();
@@ -20043,6 +20303,16 @@ var TilesetEntitiesRenderManager;
20043
20303
  else if (lowered === "internalid") {
20044
20304
  this.featurePropCache.set("internalId", prop);
20045
20305
  }
20306
+ else if (lowered === "latitude") {
20307
+ this.featurePropCache.set("latitude", prop);
20308
+ }
20309
+ else if (lowered === "longitude") {
20310
+ this.featurePropCache.set("longitude", prop);
20311
+ }
20312
+ // b3dm names the baked height TerrainHeight, the glTF metadata table calls it Altitude.
20313
+ else if (lowered === "terrainheight" || lowered === "altitude") {
20314
+ this.featurePropCache.set("height", prop);
20315
+ }
20046
20316
  }
20047
20317
  }
20048
20318
  Dispose() {
@@ -20197,7 +20467,8 @@ var TilesetEntitiesRenderManager;
20197
20467
  menuItemId: this.item.id,
20198
20468
  register: this.visualsManager,
20199
20469
  historic: Manager.IsHistoric(this.item),
20200
- applyStyles: Boolean(this.item.ApplyStyles)
20470
+ applyStyles: Boolean(this.item.ApplyStyles),
20471
+ monitor: this.monitor
20201
20472
  });
20202
20473
  this.queueHandoffRestyle(this.visualsManager.GetRegos({ menuItemId: this.item.id }));
20203
20474
  }
@@ -20240,6 +20511,45 @@ var TilesetEntitiesRenderManager;
20240
20511
  }
20241
20512
  }, 10);
20242
20513
  }
20514
+ /*
20515
+ * Reads a numeric feature property, returning null for anything that is not one.
20516
+ */
20517
+ static readNumber(feature, prop) {
20518
+ const raw = feature.getProperty(prop);
20519
+ // Coercing straight to a number would turn null, undefined and "" into a valid zero,
20520
+ // which for a coordinate is a real place off the coast of Africa.
20521
+ if (raw == null || raw === "" || typeof raw === "boolean") {
20522
+ return null;
20523
+ }
20524
+ const value = +raw;
20525
+ return isFinite(value) ? value : null;
20526
+ }
20527
+ /*
20528
+ * Returns the world position an Entities Set bakes into each feature, or null when it has none.
20529
+ */
20530
+ readBakedPos(feature) {
20531
+ const latProp = this.featurePropCache.get("latitude");
20532
+ const lonProp = this.featurePropCache.get("longitude");
20533
+ if (!latProp || !lonProp) {
20534
+ return null;
20535
+ }
20536
+ try {
20537
+ const lat = Manager.readNumber(feature, latProp);
20538
+ const lon = Manager.readNumber(feature, lonProp);
20539
+ // Degrees, per the batch table the generator writes. The range check doubles as a
20540
+ // guard against a source that bakes radians, which would otherwise read as valid.
20541
+ if (lat == null || lon == null || Math.abs(lat) > 90 || Math.abs(lon) > 180) {
20542
+ return null;
20543
+ }
20544
+ // A height that will not read is worth losing, the position it belongs to is not.
20545
+ const heightProp = this.featurePropCache.get("height");
20546
+ const height = heightProp ? Manager.readNumber(feature, heightProp) : 0;
20547
+ return Cartesian3.fromDegrees(lon, lat, height !== null && height !== void 0 ? height : 0);
20548
+ }
20549
+ catch {
20550
+ return null;
20551
+ }
20552
+ }
20243
20553
  mapTilesetFeature(feature) {
20244
20554
  var _a, _b, _c;
20245
20555
  this.evaluateFeatureProps(feature);
@@ -20320,6 +20630,12 @@ var TilesetEntitiesRenderManager;
20320
20630
  rego.internalId = parsed;
20321
20631
  }
20322
20632
  }
20633
+ // Stamped on the feature the properties came from, so a sibling keeps its own position
20634
+ // and nothing has to be cleaned up when the tile unloads.
20635
+ const bakedPos = this.readBakedPos(feature);
20636
+ if (bakedPos) {
20637
+ feature._bakedPos = bakedPos;
20638
+ }
20323
20639
  this.loadedCesiumEntities[rego.entityId] = rego;
20324
20640
  this.visualsManager.AddRego({
20325
20641
  rego,
@@ -24205,6 +24521,7 @@ var MenuItemManager;
24205
24521
  viewer: this.viewer,
24206
24522
  register: this.visualsRegister,
24207
24523
  getters: this.getters,
24524
+ monitor: this.sharedMonitor,
24208
24525
  item: params.item,
24209
24526
  initQueue: this.tilesetInitQueue
24210
24527
  });
@@ -40999,9 +41316,11 @@ var TextureFrameSeriesAnimator;
40999
41316
  return;
41000
41317
  }
41001
41318
  let next = -1;
41319
+ let isTarget = false;
41002
41320
  if (this.pendingIndex !== -1) {
41003
41321
  next = this.pendingIndex;
41004
41322
  this.pendingIndex = -1;
41323
+ isTarget = true;
41005
41324
  }
41006
41325
  else {
41007
41326
  next = this.nearestUncachedFrame();
@@ -41010,7 +41329,7 @@ var TextureFrameSeriesAnimator;
41010
41329
  return;
41011
41330
  }
41012
41331
  this.inFlightIndex = next;
41013
- this.fetchAndTint(next)
41332
+ this.fetchAndTint(next, isTarget)
41014
41333
  .catch(() => {
41015
41334
  // Eating it.
41016
41335
  })
@@ -41021,7 +41340,7 @@ var TextureFrameSeriesAnimator;
41021
41340
  }
41022
41341
  });
41023
41342
  }
41024
- async fetchAndTint(idx) {
41343
+ async fetchAndTint(idx, isTarget = true) {
41025
41344
  const tiles = this.tiles;
41026
41345
  // Every tile's frame for one timestep sits contiguously, because the generator writes
41027
41346
  // frame major. So a tiled frame is still ONE range request, not one per tile.
@@ -41053,10 +41372,25 @@ var TextureFrameSeriesAnimator;
41053
41372
  this.frameCache[idx] = decoded.pixels;
41054
41373
  this.frameDims[idx] = { width: decoded.width, height: decoded.height };
41055
41374
  this.valueCache[idx] = decoded.valuePixels || null;
41056
- if (idx === this.currentFrameIndex) {
41375
+ if (this.shouldPresent(idx, isTarget)) {
41057
41376
  this.beginCrossfadeTo(idx);
41058
41377
  }
41059
41378
  }
41379
+ /*
41380
+ * Whether a frame that has just decoded is worth putting on screen.
41381
+ */
41382
+ shouldPresent(idx, isTarget) {
41383
+ if (idx === this.currentFrameIndex) {
41384
+ return true;
41385
+ }
41386
+ // A prefetch is for a frame nobody asked to see, so it stays in the cache.
41387
+ if (!isTarget) {
41388
+ return false;
41389
+ }
41390
+ // Dragging the timeline retargets faster than a frame can be fetched, so the frame that
41391
+ // lands is rarely the one now under the playhead.
41392
+ return !this.frameCache[this.currentFrameIndex];
41393
+ }
41060
41394
  /*
41061
41395
  * Draws every tile of one frame into a single raster covering the drape extent.
41062
41396
  */
@@ -45619,7 +45953,7 @@ var StyleUtils;
45619
45953
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
45620
45954
  })(StyleUtils || (StyleUtils = {}));
45621
45955
 
45622
- const VERSION = "7.2.4";
45956
+ const VERSION = "7.2.6";
45623
45957
  /**
45624
45958
  * Updates the environment instance used by bruce-cesium to one specified.
45625
45959
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.