bruce-cesium 7.2.0 → 7.2.2

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 (43) hide show
  1. package/dist/bruce-cesium.es5.js +2237 -336
  2. package/dist/bruce-cesium.es5.js.map +1 -1
  3. package/dist/bruce-cesium.umd.js +2230 -333
  4. package/dist/bruce-cesium.umd.js.map +1 -1
  5. package/dist/lib/bruce-cesium.js +2 -1
  6. package/dist/lib/bruce-cesium.js.map +1 -1
  7. package/dist/lib/internal/image-utils.js +106 -1
  8. package/dist/lib/internal/image-utils.js.map +1 -1
  9. package/dist/lib/rendering/displaced-surface-primitive.js +41 -8
  10. package/dist/lib/rendering/displaced-surface-primitive.js.map +1 -1
  11. package/dist/lib/rendering/entity-render-engine-polygon.js +211 -13
  12. package/dist/lib/rendering/entity-render-engine-polygon.js.map +1 -1
  13. package/dist/lib/rendering/render-managers/common/historic-utils.js +471 -0
  14. package/dist/lib/rendering/render-managers/common/historic-utils.js.map +1 -0
  15. package/dist/lib/rendering/render-managers/entities/entities-ids-render-manager.js +14 -124
  16. package/dist/lib/rendering/render-managers/entities/entities-ids-render-manager.js.map +1 -1
  17. package/dist/lib/rendering/render-managers/entities/entities-render-manager.js +16 -13
  18. package/dist/lib/rendering/render-managers/entities/entities-render-manager.js.map +1 -1
  19. package/dist/lib/rendering/render-managers/tilesets/tileset-cad-render-manager.js +31 -109
  20. package/dist/lib/rendering/render-managers/tilesets/tileset-cad-render-manager.js.map +1 -1
  21. package/dist/lib/rendering/render-managers/tilesets/tileset-entities-render-manager.js +23 -7
  22. package/dist/lib/rendering/render-managers/tilesets/tileset-entities-render-manager.js.map +1 -1
  23. package/dist/lib/rendering/texture-frame-series-animator.js +621 -28
  24. package/dist/lib/rendering/texture-frame-series-animator.js.map +1 -1
  25. package/dist/lib/rendering/texture-value-labels.js +687 -0
  26. package/dist/lib/rendering/texture-value-labels.js.map +1 -0
  27. package/dist/lib/rendering/tileset-styler.js +28 -24
  28. package/dist/lib/rendering/tileset-styler.js.map +1 -1
  29. package/dist/lib/rendering/visuals-register.js +8 -3
  30. package/dist/lib/rendering/visuals-register.js.map +1 -1
  31. package/dist/types/bruce-cesium.d.ts +2 -1
  32. package/dist/types/internal/image-utils.d.ts +25 -0
  33. package/dist/types/rendering/displaced-surface-primitive.d.ts +5 -2
  34. package/dist/types/rendering/render-managers/common/historic-utils.d.ts +159 -0
  35. package/dist/types/rendering/render-managers/entities/entities-ids-render-manager.d.ts +1 -0
  36. package/dist/types/rendering/render-managers/entities/entities-render-manager.d.ts +5 -0
  37. package/dist/types/rendering/render-managers/tilesets/tileset-cad-render-manager.d.ts +1 -0
  38. package/dist/types/rendering/render-managers/tilesets/tileset-entities-render-manager.d.ts +2 -0
  39. package/dist/types/rendering/texture-frame-series-animator.d.ts +172 -1
  40. package/dist/types/rendering/texture-value-labels.d.ts +139 -0
  41. package/dist/types/rendering/tileset-styler.d.ts +2 -0
  42. package/dist/types/rendering/visuals-register.d.ts +1 -0
  43. package/package.json +2 -2
@@ -1,6 +1,6 @@
1
1
  import * as Cesium from 'cesium';
2
- import { Cartographic, ColorMaterialProperty, Entity, Color, ConstantProperty, CallbackProperty, Primitive, Cesium3DTileFeature, ShaderSource, Cartesian3, Math as Math$1, Ellipsoid, Transforms, Matrix4, Geometry, GeometryAttribute, ComponentDatatype, PrimitiveType, BoundingSphere, GeometryPipeline, sampleTerrain, Texture, PixelFormat, Sampler, TextureWrap, TextureMinificationFilter, TextureMagnificationFilter, VertexArray, BufferUsage, ShaderProgram, RenderState, BlendingState, DrawCommand, Pass, Cartesian2, DistanceDisplayCondition, JulianDate, Quaternion, HeadingPitchRoll, HeightReference, ColorBlendMode, ShadowMode, ClassificationType, Model, HorizontalOrigin, VerticalOrigin, ConstantPositionProperty, ArcType, CornerType, ImageMaterialProperty, PolygonHierarchy, PolylineGraphics, Rectangle, SceneTransforms, NearFarScalar, Matrix3, KmlDataSource, SceneMode, GeoJsonDataSource, Cesium3DTileStyle, HeadingPitchRange, Ion, IonResource, 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, EllipsoidGeodesic, sampleTerrainMostDetailed, defined, CustomDataSource, PolygonPipeline, CesiumInspector, ClockRange, DynamicEnvironmentMapManager, GeometryInstance, ScreenSpaceEventHandler, ScreenSpaceEventType, Fullscreen, CzmlDataSource } from 'cesium';
3
- import { Cartes, Entity as Entity$1, ProjectViewTile, ENVIRONMENT, Calculator, ClientFile, EntityLod, EntityTag, EntityType, LRUCache, ObjectUtils, Style, Api, Carto, Geometry as Geometry$1, MenuItem, ProjectView, ProjectViewBookmark, Tileset, ZoomControl, Bounds, Color as Color$1, BruceEvent, EntityCoords, DataLab, DelayQueue, EntityHistoricData, EntityRelation, AccountConcept, RecordChangeFeed, BruceApi, EntitySource, Camera, ProgramKey, ProjectViewLegacyTile, Account, AccountLimits, Session, UrlUtils, ProjectViewBookmarkGroup, EntityAttachment, EntityAttachmentType, EntityAttribute, MathUtils, EntityRelationType } 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, DistanceDisplayCondition, JulianDate, Quaternion, HeadingPitchRoll, HeightReference, ColorBlendMode, ShadowMode, ClassificationType, Model, ArcType, CornerType, ConstantPositionProperty, HorizontalOrigin, VerticalOrigin, SceneTransforms, ImageMaterialProperty, PolygonHierarchy, PolylineGraphics, Rectangle, NearFarScalar, Matrix3, GeoJsonDataSource, SceneMode, KmlDataSource, Ion, IonResource, Cesium3DTileStyle, HeadingPitchRange, EllipsoidTerrainProvider, IonImageryProvider, createWorldImagery, createWorldImageryAsync, BingMapsImageryProvider, BingMapsStyle, MapboxImageryProvider, MapboxStyleImageryProvider, ArcGisMapServerImageryProvider, OpenStreetMapImageryProvider, UrlTemplateImageryProvider, GridImageryProvider, GeographicTilingScheme, ImageryLayer, TileMapServiceImageryProvider, CesiumTerrainProvider, Cesium3DTileColorBlendMode, Cesium3DTileset, Intersect, OrthographicFrustum, EasingFunction, EllipsoidGeodesic, sampleTerrainMostDetailed, defined, PolygonPipeline, CustomDataSource, CesiumInspector, ClockRange, DynamicEnvironmentMapManager, GeometryInstance, ScreenSpaceEventHandler, ScreenSpaceEventType, Fullscreen, ModelGraphics, PolygonGraphics, CorridorGraphics, PointGraphics, BillboardGraphics, EllipseGraphics, PolylineDashMaterialProperty, 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, Bounds, Color as Color$1, BruceEvent, EntityCoords, DataLab, DelayQueue, AccountConcept, RecordChangeFeed, BruceApi, EntityRelation, EntitySource, ProgramKey, ProjectViewLegacyTile, 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.
@@ -9054,7 +9054,8 @@ var VisualsRegister;
9054
9054
  entityId: rego.entityId,
9055
9055
  menuItemId: rego.menuItemId,
9056
9056
  doUpdate: false,
9057
- requestRender: false
9057
+ requestRender: false,
9058
+ keepVisual: rego.visual
9058
9059
  });
9059
9060
  }
9060
9061
  const entityId = rego.entityId;
@@ -9347,7 +9348,9 @@ var VisualsRegister;
9347
9348
  EntityLabel.Detatch({
9348
9349
  rego
9349
9350
  });
9350
- if (doRemove != false) {
9351
+ // Never tear down the graphic the caller is registering: the renderers hand back
9352
+ // the same object when they reuse it, so removing it here destroys what replaces it.
9353
+ if (doRemove != false && rego.visual !== params.keepVisual) {
9351
9354
  removeEntity(this.viewer, rego.visual);
9352
9355
  }
9353
9356
  rego.visual = null;
@@ -9401,7 +9404,9 @@ var VisualsRegister;
9401
9404
  EntityLabel.Detatch({
9402
9405
  rego
9403
9406
  });
9404
- if (doRemove != false) {
9407
+ // Never tear down the graphic the caller is registering: the renderers hand back
9408
+ // the same object when they reuse it, so removing it here destroys what replaces it.
9409
+ if (doRemove != false && rego.visual !== params.keepVisual) {
9405
9410
  removeEntity(this.viewer, rego.visual);
9406
9411
  }
9407
9412
  rego.visual = null;
@@ -10721,6 +10726,468 @@ class PointClustering {
10721
10726
  }
10722
10727
  }
10723
10728
 
10729
+ /**
10730
+ * How often a historic refresh is worth running, and which Entities it should ask about first.
10731
+ *
10732
+ * A handful of tracked vehicles can afford to re-read on almost every clock tick. Thousands of
10733
+ * parcels cannot, and refreshing them that often was never buying anything since the answer barely
10734
+ * changes between ticks.
10735
+ */
10736
+ var HistoricFetchOrder;
10737
+ (function (HistoricFetchOrder) {
10738
+ // The cadence for a set small enough that latency is what the user notices.
10739
+ HistoricFetchOrder.MIN_INTERVAL_MS = 250;
10740
+ // The cadence for a set large enough that the request itself is what the user notices.
10741
+ HistoricFetchOrder.MAX_INTERVAL_MS = 10000;
10742
+ // Below this many Entities the cadence stays at its fastest.
10743
+ HistoricFetchOrder.SMALL_SET = 30;
10744
+ // At and above this many it stays at its slowest.
10745
+ HistoricFetchOrder.LARGE_SET = 5000;
10746
+ /**
10747
+ * Returns how long to wait between historic refreshes for a set of this size.
10748
+ */
10749
+ function IntervalFor(entityCount, minMs = HistoricFetchOrder.MIN_INTERVAL_MS, maxMs = HistoricFetchOrder.MAX_INTERVAL_MS) {
10750
+ if (!(entityCount > HistoricFetchOrder.SMALL_SET)) {
10751
+ return minMs;
10752
+ }
10753
+ if (entityCount >= HistoricFetchOrder.LARGE_SET) {
10754
+ return maxMs;
10755
+ }
10756
+ // Log scaled, so going from 30 to 300 Entities costs more than going from 3000 to 5000.
10757
+ const progress = Math.log(entityCount / HistoricFetchOrder.SMALL_SET) / Math.log(HistoricFetchOrder.LARGE_SET / HistoricFetchOrder.SMALL_SET);
10758
+ return Math.round(minMs + ((maxMs - minMs) * progress));
10759
+ }
10760
+ HistoricFetchOrder.IntervalFor = IntervalFor;
10761
+ /**
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.
10766
+ */
10767
+ function ByCameraDistance(params) {
10768
+ 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)) {
10772
+ return [];
10773
+ }
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];
10777
+ }
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);
10782
+ if (!pos) {
10783
+ banded[banded.length - 1].push(entity);
10784
+ continue;
10785
+ }
10786
+ const distance = Cartesian3.distance(cameraPos, pos);
10787
+ let index = bands.findIndex(bound => distance <= bound);
10788
+ if (index < 0) {
10789
+ index = banded.length - 1;
10790
+ }
10791
+ banded[index].push(entity);
10792
+ }
10793
+ return banded.filter(band => band.length > 0);
10794
+ }
10795
+ HistoricFetchOrder.ByCameraDistance = ByCameraDistance;
10796
+ /*
10797
+ * Returns an Entity's world position, or null when it has none to place it by.
10798
+ */
10799
+ function SafePos(entity, viewer) {
10800
+ try {
10801
+ const pos = EntityUtils.GetPos({
10802
+ entity: entity,
10803
+ viewer: viewer,
10804
+ allowRendered: true
10805
+ });
10806
+ if (!pos || isNaN(pos.x) || isNaN(pos.y) || isNaN(pos.z)) {
10807
+ return null;
10808
+ }
10809
+ return pos;
10810
+ }
10811
+ catch {
10812
+ return null;
10813
+ }
10814
+ }
10815
+ })(HistoricFetchOrder || (HistoricFetchOrder = {}));
10816
+ /**
10817
+ * Decides what slice of history is worth fetching for the clock as it is currently moving.
10818
+ */
10819
+ var HistoricFetchPlan;
10820
+ (function (HistoricFetchPlan) {
10821
+ // How many seconds of playback the detail window tries to stay ahead by.
10822
+ HistoricFetchPlan.DEFAULT_LEAD_SECONDS = 10;
10823
+ // The detail window never shrinks below this, so a paused clock still has something around it.
10824
+ HistoricFetchPlan.DEFAULT_MIN_DETAIL_MS = 60 * 1000;
10825
+ // Nor grows past it, which is what stops a fast scrub asking for the whole archive.
10826
+ HistoricFetchPlan.DEFAULT_MAX_DETAIL_MS = 6 * 60 * 60 * 1000;
10827
+ // Records the coarse pass may spend describing the whole range.
10828
+ HistoricFetchPlan.DEFAULT_SAMPLE = 200;
10829
+ /*
10830
+ * Returns how wide the detail window should be for the rate the clock is moving at.
10831
+ */
10832
+ function DetailSpanFor(trajectory, leadSeconds, minMs, maxMs) {
10833
+ const rate = Math.abs((trajectory === null || trajectory === void 0 ? void 0 : trajectory.ratePerSecondMs) || 0);
10834
+ const span = rate * leadSeconds;
10835
+ if (!(span > minMs)) {
10836
+ return minMs;
10837
+ }
10838
+ return Math.min(span, maxMs);
10839
+ }
10840
+ HistoricFetchPlan.DetailSpanFor = DetailSpanFor;
10841
+ /**
10842
+ * Returns the detail window and the coarse pass for the clock as it stands.
10843
+ */
10844
+ function Plan(params) {
10845
+ var _a;
10846
+ const { currentTime, windowStart, windowStop, trajectory, leadSeconds = HistoricFetchPlan.DEFAULT_LEAD_SECONDS, minDetailMs = HistoricFetchPlan.DEFAULT_MIN_DETAIL_MS, maxDetailMs = HistoricFetchPlan.DEFAULT_MAX_DETAIL_MS, sample = HistoricFetchPlan.DEFAULT_SAMPLE } = params;
10847
+ if (!(windowStop > windowStart)) {
10848
+ return { detail: null, coarse: null };
10849
+ }
10850
+ // A range that fits the detail budget has nothing worth sampling: reading it whole is both
10851
+ // cheaper than two requests and better than an approximation of it.
10852
+ if ((windowStop - windowStart) <= maxDetailMs) {
10853
+ return { detail: { start: windowStart, stop: windowStop }, coarse: null };
10854
+ }
10855
+ const span = DetailSpanFor(trajectory, leadSeconds, minDetailMs, maxDetailMs);
10856
+ const direction = (_a = trajectory === null || trajectory === void 0 ? void 0 : trajectory.direction) !== null && _a !== void 0 ? _a : 0;
10857
+ // Time running forward only ever reveals what is ahead, so the window leads rather than centres.
10858
+ // A little is still kept behind so a record just passed can still be interpolated from.
10859
+ const behind = direction === 0 ? span / 2 : span * 0.2;
10860
+ const ahead = direction === 0 ? span / 2 : span * 0.8;
10861
+ let start = direction < 0 ? currentTime - ahead : currentTime - behind;
10862
+ let stop = direction < 0 ? currentTime + behind : currentTime + ahead;
10863
+ start = Math.max(windowStart, start);
10864
+ stop = Math.min(windowStop, stop);
10865
+ if (!(stop > start)) {
10866
+ return { detail: null, coarse: { start: windowStart, stop: windowStop, sample } };
10867
+ }
10868
+ const detail = { start, stop };
10869
+ // Nothing to be coarse about once the detail window is the whole range.
10870
+ if (start <= windowStart && stop >= windowStop) {
10871
+ return { detail, coarse: null };
10872
+ }
10873
+ return { detail, coarse: { start: windowStart, stop: windowStop, sample } };
10874
+ }
10875
+ HistoricFetchPlan.Plan = Plan;
10876
+ /**
10877
+ * Tracks which way the clock is moving and how fast, from the times it is observed at.
10878
+ * Playback and a user dragging the timeline both look the same here, which is the point.
10879
+ */
10880
+ class TrajectoryTracker {
10881
+ // Smooths the rate so one slow frame does not collapse the window.
10882
+ constructor(smoothing = 0.4) {
10883
+ this.smoothing = smoothing;
10884
+ this.lastTime = null;
10885
+ this.lastObservedAt = null;
10886
+ this.direction = 0;
10887
+ this.ratePerSecondMs = 0;
10888
+ }
10889
+ /**
10890
+ * Records where the clock is now. observedAt is the wall clock, in milliseconds.
10891
+ */
10892
+ Observe(sceneTime, observedAt) {
10893
+ if (this.lastTime == null) {
10894
+ this.lastTime = sceneTime;
10895
+ this.lastObservedAt = observedAt;
10896
+ return;
10897
+ }
10898
+ const elapsed = observedAt - this.lastObservedAt;
10899
+ const moved = sceneTime - this.lastTime;
10900
+ this.lastTime = sceneTime;
10901
+ this.lastObservedAt = observedAt;
10902
+ if (!(elapsed > 0)) {
10903
+ return;
10904
+ }
10905
+ if (moved === 0) {
10906
+ this.direction = 0;
10907
+ this.ratePerSecondMs = 0;
10908
+ return;
10909
+ }
10910
+ this.direction = moved > 0 ? 1 : -1;
10911
+ const rate = Math.abs(moved) / (elapsed / 1000);
10912
+ this.ratePerSecondMs = this.ratePerSecondMs === 0
10913
+ ? rate
10914
+ : (this.ratePerSecondMs * (1 - this.smoothing)) + (rate * this.smoothing);
10915
+ }
10916
+ Get() {
10917
+ return { direction: this.direction, ratePerSecondMs: this.ratePerSecondMs };
10918
+ }
10919
+ Reset() {
10920
+ this.lastTime = null;
10921
+ this.lastObservedAt = null;
10922
+ this.direction = 0;
10923
+ this.ratePerSecondMs = 0;
10924
+ }
10925
+ }
10926
+ HistoricFetchPlan.TrajectoryTracker = TrajectoryTracker;
10927
+ })(HistoricFetchPlan || (HistoricFetchPlan = {}));
10928
+ /**
10929
+ * Tracks which time ranges of historic data have already been requested, and the records they
10930
+ * returned, so moving the clock only fetches the part that is genuinely new.
10931
+ *
10932
+ * Coverage is tracked by the ranges that were asked for, not by the timestamps that came back. A
10933
+ * window holding no records is still covered, otherwise an empty stretch is re-requested forever.
10934
+ */
10935
+ class HistoricRangeCache {
10936
+ // Ranges this far apart are treated as one, so a scrub does not leave a trail of slivers.
10937
+ constructor(mergeToleranceMs = 60000) {
10938
+ this.mergeToleranceMs = mergeToleranceMs;
10939
+ // Requested ranges, kept sorted and non-overlapping.
10940
+ this.held = [];
10941
+ // Ranges covered only by a sampled read, which describe their span without holding every record.
10942
+ this.heldCoarse = [];
10943
+ this.records = {};
10944
+ }
10945
+ /**
10946
+ * Returns the parts of the window that have not been requested yet.
10947
+ */
10948
+ MissingRanges(start, stop) {
10949
+ if (!(stop > start)) {
10950
+ return [];
10951
+ }
10952
+ const missing = [];
10953
+ let cursor = start;
10954
+ for (const range of this.held) {
10955
+ if (range.stop <= cursor) {
10956
+ continue;
10957
+ }
10958
+ if (range.start >= stop) {
10959
+ break;
10960
+ }
10961
+ if (range.start > cursor) {
10962
+ missing.push({ start: cursor, stop: Math.min(range.start, stop) });
10963
+ }
10964
+ cursor = Math.max(cursor, range.stop);
10965
+ if (cursor >= stop) {
10966
+ break;
10967
+ }
10968
+ }
10969
+ if (cursor < stop) {
10970
+ missing.push({ start: cursor, stop: stop });
10971
+ }
10972
+ return this.mergeAdjacent(missing);
10973
+ }
10974
+ /**
10975
+ * Marks a range as requested and merges the records it returned into what is already held.
10976
+ */
10977
+ Absorb(start, stop, recordsByIds, coarse = false) {
10978
+ if (stop > start) {
10979
+ if (coarse) {
10980
+ this.heldCoarse = this.mergeAdjacent([...this.heldCoarse, { start, stop }].sort((a, b) => a.start - b.start));
10981
+ }
10982
+ else {
10983
+ this.held = this.mergeAdjacent([...this.held, { start, stop }].sort((a, b) => a.start - b.start));
10984
+ }
10985
+ }
10986
+ if (!recordsByIds) {
10987
+ return;
10988
+ }
10989
+ for (const entityId of Object.keys(recordsByIds)) {
10990
+ const incoming = recordsByIds[entityId] || [];
10991
+ const current = this.records[entityId] || [];
10992
+ // Keyed by instant so a range fetched twice cannot double up its records.
10993
+ const byInstant = new Map();
10994
+ for (const record of current) {
10995
+ byInstant.set(new Date(record.dateTime).getTime(), record);
10996
+ }
10997
+ for (const record of incoming) {
10998
+ byInstant.set(new Date(record.dateTime).getTime(), record);
10999
+ }
11000
+ this.records[entityId] = Array.from(byInstant.values())
11001
+ .sort((a, b) => new Date(a.dateTime).getTime() - new Date(b.dateTime).getTime());
11002
+ }
11003
+ }
11004
+ GetRecords() {
11005
+ return this.records;
11006
+ }
11007
+ /**
11008
+ * Returns whether the window is already covered in full.
11009
+ */
11010
+ Covers(start, stop) {
11011
+ return this.MissingRanges(start, stop).length === 0;
11012
+ }
11013
+ /**
11014
+ * Returns whether a sampled pass already describes the window.
11015
+ */
11016
+ CoversCoarse(start, stop) {
11017
+ if (!(stop > start)) {
11018
+ return true;
11019
+ }
11020
+ return this.heldCoarse.some(range => range.start <= start && range.stop >= stop);
11021
+ }
11022
+ Clear() {
11023
+ this.held = [];
11024
+ this.heldCoarse = [];
11025
+ this.records = {};
11026
+ }
11027
+ mergeAdjacent(ranges) {
11028
+ if (ranges.length <= 1) {
11029
+ return ranges;
11030
+ }
11031
+ const sorted = [...ranges].sort((a, b) => a.start - b.start);
11032
+ const merged = [];
11033
+ let current = { ...sorted[0] };
11034
+ for (let i = 1; i < sorted.length; i++) {
11035
+ const next = sorted[i];
11036
+ if (next.start - current.stop <= this.mergeToleranceMs) {
11037
+ current.stop = Math.max(current.stop, next.stop);
11038
+ }
11039
+ else {
11040
+ merged.push(current);
11041
+ current = { ...next };
11042
+ }
11043
+ }
11044
+ merged.push(current);
11045
+ return merged;
11046
+ }
11047
+ }
11048
+ /**
11049
+ * Pulls historic records for the visible clock range: what to ask for, in what order, and what has
11050
+ * already been asked for.
11051
+ */
11052
+ class HistoricGatherer {
11053
+ constructor(params) {
11054
+ this.params = params;
11055
+ this.trajectory = new HistoricFetchPlan.TrajectoryTracker();
11056
+ // Range key to the request in flight for it, so the same range is never asked for twice at once.
11057
+ this.inFlight = new Map();
11058
+ this.disposed = false;
11059
+ this.cache = new HistoricRangeCache();
11060
+ }
11061
+ get Records() {
11062
+ return this.cache.GetRecords();
11063
+ }
11064
+ Clear() {
11065
+ this.cache.Clear();
11066
+ this.trajectory.Reset();
11067
+ this.inFlight.clear();
11068
+ }
11069
+ Dispose() {
11070
+ this.disposed = true;
11071
+ this.Clear();
11072
+ }
11073
+ /**
11074
+ * Returns how long to wait between refreshes for a set of this size.
11075
+ */
11076
+ IntervalFor(entityCount) {
11077
+ return HistoricFetchOrder.IntervalFor(entityCount);
11078
+ }
11079
+ /**
11080
+ * Fetches whatever the current clock range needs that is not already held.
11081
+ * Returns whether anything was requested, alongside every record held for these Entities.
11082
+ */
11083
+ async Gather(entities) {
11084
+ var _a;
11085
+ if (this.disposed || !(entities === null || entities === void 0 ? void 0 : entities.length)) {
11086
+ return { fetched: false, recordsByIds: this.cache.GetRecords() };
11087
+ }
11088
+ const { viewer } = this.params;
11089
+ const windowStart = new Date(viewer.clock.startTime.toString()).getTime();
11090
+ const windowStop = new Date(viewer.clock.stopTime.toString()).getTime();
11091
+ const currentTime = new Date(viewer.clock.currentTime.toString()).getTime();
11092
+ this.trajectory.Observe(currentTime, Date.now());
11093
+ const liveEdge = ViewUtils.GetTimeDetails({ viewer }).isLive ? currentTime : null;
11094
+ const liveTail = (_a = this.params.liveTailMs) !== null && _a !== void 0 ? _a : HistoricGatherer.LIVE_TAIL_MS;
11095
+ const plan = HistoricFetchPlan.Plan({
11096
+ currentTime,
11097
+ windowStart,
11098
+ windowStop,
11099
+ trajectory: this.trajectory.Get(),
11100
+ sample: this.params.sample
11101
+ });
11102
+ let fetched = false;
11103
+ if (plan.coarse && !this.cache.CoversCoarse(plan.coarse.start, plan.coarse.stop)) {
11104
+ const records = await this.request(plan.coarse.start, plan.coarse.stop, entities, plan.coarse.sample);
11105
+ if (this.disposed) {
11106
+ return { fetched, recordsByIds: this.cache.GetRecords() };
11107
+ }
11108
+ this.cache.Absorb(plan.coarse.start, this.holdTo(plan.coarse.stop, liveEdge, liveTail), records, true);
11109
+ fetched = true;
11110
+ }
11111
+ if (plan.detail) {
11112
+ const bands = entities.length > HistoricFetchOrder.SMALL_SET
11113
+ ? HistoricFetchOrder.ByCameraDistance({ viewer, entities })
11114
+ : [entities];
11115
+ for (const range of this.cache.MissingRanges(plan.detail.start, plan.detail.stop)) {
11116
+ if (this.disposed) {
11117
+ break;
11118
+ }
11119
+ for (const band of bands) {
11120
+ if (this.disposed) {
11121
+ break;
11122
+ }
11123
+ const records = await this.request(range.start, range.stop, band, 0);
11124
+ if (this.disposed) {
11125
+ break;
11126
+ }
11127
+ // Records land as each band arrives, while the range is only marked held once
11128
+ // every band has been asked for.
11129
+ this.cache.Absorb(0, 0, records);
11130
+ fetched = true;
11131
+ }
11132
+ if (this.disposed) {
11133
+ break;
11134
+ }
11135
+ const heldTo = this.holdTo(range.stop, liveEdge, liveTail);
11136
+ if (heldTo > range.start) {
11137
+ this.cache.Absorb(range.start, heldTo, {});
11138
+ }
11139
+ }
11140
+ }
11141
+ return { fetched, recordsByIds: this.cache.GetRecords() };
11142
+ }
11143
+ /*
11144
+ * Returns how much of a range that was just read may be marked as held.
11145
+ */
11146
+ holdTo(stop, liveEdge, liveTailMs) {
11147
+ if (liveEdge == null) {
11148
+ return stop;
11149
+ }
11150
+ return Math.min(stop, liveEdge - liveTailMs);
11151
+ }
11152
+ /*
11153
+ * Reads one range, joining a request already in flight for it rather than repeating it.
11154
+ */
11155
+ request(start, stop, entities, sample) {
11156
+ var _a;
11157
+ const padding = (_a = this.params.paddingMs) !== null && _a !== void 0 ? _a : 0;
11158
+ const dateTimeFrom = new Date(start - padding).toISOString();
11159
+ const dateTimeTo = new Date(stop + padding).toISOString();
11160
+ const entityIds = entities.map(x => { var _a; return (_a = x.Bruce) === null || _a === void 0 ? void 0 : _a.ID; }).filter(x => !!x);
11161
+ const key = `${dateTimeFrom}|${dateTimeTo}|${sample}|${entityIds.join(",")}`;
11162
+ const existing = this.inFlight.get(key);
11163
+ if (existing) {
11164
+ return existing;
11165
+ }
11166
+ const prom = EntityHistoricData.GetRecordsByEntity({
11167
+ attrKey: this.params.getAttrKey(),
11168
+ dateTimeFrom,
11169
+ dateTimeTo,
11170
+ entityIds,
11171
+ sample: sample > 0 ? sample : undefined,
11172
+ api: this.params.getApi()
11173
+ }).then((res) => {
11174
+ const records = res.recordsByIds || {};
11175
+ if (!this.disposed && this.params.onRecords) {
11176
+ this.params.onRecords(records);
11177
+ }
11178
+ return records;
11179
+ }).finally(() => {
11180
+ this.inFlight.delete(key);
11181
+ });
11182
+ this.inFlight.set(key, prom);
11183
+ return prom;
11184
+ }
11185
+ }
11186
+ // How far back from the live edge is left unheld, so a record written moments ago still falls in
11187
+ // something that gets asked for again. It has to outlast a write, a commit and a refresh, plus
11188
+ // whatever the writer's clock and this one disagree by.
11189
+ HistoricGatherer.LIVE_TAIL_MS = 15000;
11190
+
10724
11191
  const BATCH_SIZE = 500;
10725
11192
  const CHECK_BATCH_SIZE = 250;
10726
11193
  function getValue$2(viewer, obj) {
@@ -10810,6 +11277,13 @@ var EntitiesRenderManager;
10810
11277
  this.item = item;
10811
11278
  this.visualsManager = visualsManager;
10812
11279
  this.onSeriesDiscovered = onSeriesDiscovered;
11280
+ this.historicGatherer = new HistoricGatherer({
11281
+ viewer: this.viewer,
11282
+ getApi: () => this.apiGetter.getApi(),
11283
+ getAttrKey: () => { var _a; return (_a = this.item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historicAttrKey; },
11284
+ // A second either side, to take in records sitting exactly on the boundary.
11285
+ paddingMs: 1000
11286
+ });
10813
11287
  this.useGeojson = item.renderAsGeojson == true;
10814
11288
  if (item.enableClustering) {
10815
11289
  this.clustering = new PointClustering(this.visualsManager, this.item.id);
@@ -11773,6 +12247,13 @@ var EntitiesRenderManager;
11773
12247
  this.renderAsIndividualsChain = next;
11774
12248
  return next;
11775
12249
  }
12250
+ /**
12251
+ * Returns the historic records to interpolate between, fetching only what is not already held.
12252
+ */
12253
+ async gatherHistoric(entities) {
12254
+ const { recordsByIds } = await this.historicGatherer.Gather(entities);
12255
+ return recordsByIds;
12256
+ }
11776
12257
  async doRenderAsIndividuals(entities, force = false) {
11777
12258
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
11778
12259
  // When live we just want to show the latest pos.
@@ -11787,19 +12268,7 @@ var EntitiesRenderManager;
11787
12268
  const isHistoric = ((_a = this.item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historic) || ((_b = this.item.BruceEntity) === null || _b === void 0 ? void 0 : _b.historicAttrKey);
11788
12269
  // If we're interpolating then we request additional records to quickly swap between.
11789
12270
  if (!isLive && isHistoric && entities.length && this.item.historicInterpolation) {
11790
- // We'll add/remove 1 second to ensure we cover all records.
11791
- const startTmp = JulianDate.toDate(this.viewer.clock.startTime);
11792
- const stopTmp = JulianDate.toDate(this.viewer.clock.stopTime);
11793
- const startStr = new Date(startTmp.getTime() - 1000).toISOString();
11794
- const stopStr = new Date(stopTmp.getTime() + 1000).toISOString();
11795
- const historicData = await EntityHistoricData.GetList({
11796
- attrKey: this.item.BruceEntity.historicAttrKey,
11797
- dateTimeFrom: startStr,
11798
- dateTimeTo: stopStr,
11799
- entityIds: entities.map(x => x.Bruce.ID),
11800
- api: this.apiGetter.getApi()
11801
- });
11802
- entitiesHistoric = historicData.recordsByIds;
12271
+ entitiesHistoric = await this.gatherHistoric(entities);
11803
12272
  }
11804
12273
  if (this.disposed) {
11805
12274
  this.doDispose();
@@ -12924,6 +13393,13 @@ var EntitiesIdsRenderManager;
12924
13393
  this.monitor = monitor;
12925
13394
  this.item = item;
12926
13395
  this.visualsManager = visualsManager;
13396
+ this.historicGatherer = new HistoricGatherer({
13397
+ viewer: this.viewer,
13398
+ getApi: () => this.apiGetter.getApi(),
13399
+ getAttrKey: () => { var _a; return (_a = this.item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historicAttrKey; },
13400
+ // Padding either side of a requested range, for clock desync between us and the server.
13401
+ paddingMs: 15000
13402
+ });
12927
13403
  if (this.item.enableClustering) {
12928
13404
  this.clustering = new PointClustering(visualsManager, this.item.id, (_a = this.item) === null || _a === void 0 ? void 0 : _a.clustering);
12929
13405
  }
@@ -13273,138 +13749,22 @@ var EntitiesIdsRenderManager;
13273
13749
  * @param entities
13274
13750
  */
13275
13751
  async getHistoricInfo(entities) {
13276
- // Time padding in milliseconds (15 seconds).
13277
- // Helps account for desync between client and server.
13278
- const TIME_PADDING_MS = 15000;
13279
- const minDateTimeStr = this.viewer.clock.startTime.toString();
13280
- const maxDateTimeStr = this.viewer.clock.stopTime.toString();
13281
- const minDateTime = new Date(minDateTimeStr).getTime();
13282
- const maxDateTime = new Date(maxDateTimeStr).getTime();
13283
- const tDetails = ViewUtils.GetTimeDetails({
13284
- viewer: this.viewer,
13285
- });
13286
- const isLive = tDetails.isLive;
13287
- let rangesToRequest = [{
13288
- start: minDateTime,
13289
- stop: maxDateTime
13290
- }];
13291
- // If we already have cached data, determine what ranges we're missing.
13292
- if (Object.keys(this.entitiesHistoric).length >= entities.length) {
13293
- let foundMinDateTime = null;
13294
- let foundMaxDateTime = null;
13295
- // Find the min/max based on cached data.
13296
- // Since we set the values sorted, we only have to check the first and last records for each entity.
13297
- for (const entityId of Object.keys(this.entitiesHistoric)) {
13298
- const records = this.entitiesHistoric[entityId] || [];
13299
- if (records.length) {
13300
- const dateTime = new Date(records[0].dateTime).getTime();
13301
- if (foundMinDateTime == null || dateTime < foundMinDateTime) {
13302
- foundMinDateTime = dateTime;
13303
- }
13304
- const dateTime2 = new Date(records[records.length - 1].dateTime).getTime();
13305
- if (foundMaxDateTime == null || dateTime2 > foundMaxDateTime) {
13306
- foundMaxDateTime = dateTime2;
13307
- }
13308
- }
13309
- }
13310
- // Complete overlap - we already have all the data.
13311
- if (foundMinDateTime != null && foundMaxDateTime != null && foundMinDateTime <= minDateTime && foundMaxDateTime >= maxDateTime) {
13312
- return [false, this.entitiesHistoric];
13313
- }
13314
- // Calculate missing ranges.
13315
- rangesToRequest = [];
13316
- // Check if we need data before our cached range.
13317
- if (foundMinDateTime != null && foundMinDateTime > minDateTime) {
13318
- // When live we focus on getting the next range rather than old data.
13319
- // Ends up being one less request to perform every second.
13320
- if (!isLive || foundMinDateTime == null) {
13321
- rangesToRequest.push({
13322
- start: minDateTime,
13323
- stop: new Date(foundMinDateTime).getTime()
13324
- });
13325
- }
13326
- }
13327
- // Check if we need data after our cached range.
13328
- if (foundMaxDateTime != null && foundMaxDateTime < maxDateTime) {
13329
- rangesToRequest.push({
13330
- start: new Date(foundMaxDateTime).getTime(),
13331
- stop: maxDateTime
13332
- });
13333
- }
13334
- // If the ranges to request are within a tolerance, combine them.
13335
- if (rangesToRequest.length > 1) {
13336
- const TOLERANCE_MS = 60000; // 1 minute.
13337
- let currentRange = rangesToRequest[0];
13338
- for (let i = 1; i < rangesToRequest.length; i++) {
13339
- const nextRange = rangesToRequest[i];
13340
- if (nextRange.start - currentRange.stop <= TOLERANCE_MS) {
13341
- currentRange.stop = nextRange.stop;
13342
- }
13343
- else {
13344
- currentRange = nextRange;
13345
- }
13346
- }
13347
- }
13348
- }
13349
- const entityIds = entities.map(x => x.Bruce.ID);
13350
- const combined = { ...this.entitiesHistoric };
13351
- // Make requests for each missing range
13352
- for (const range of rangesToRequest) {
13353
- if (this.disposed) {
13354
- break;
13355
- }
13356
- const start = new Date(range.start - TIME_PADDING_MS);
13357
- const stop = new Date(range.stop + TIME_PADDING_MS);
13358
- const historicData = await EntityHistoricData.GetList({
13359
- attrKey: this.item.BruceEntity.historicAttrKey,
13360
- dateTimeFrom: start.toISOString(),
13361
- dateTimeTo: stop.toISOString(),
13362
- entityIds: entityIds,
13363
- api: this.apiGetter.getApi()
13364
- });
13365
- if (this.disposed) {
13366
- break;
13367
- }
13368
- // Merge the new data with existing data
13369
- const records = historicData.recordsByIds;
13370
- const recordsIds = Object.keys(records);
13371
- for (let i = 0; i < recordsIds.length; i++) {
13372
- const entityId = recordsIds[i];
13373
- const latest = records[entityId] || [];
13374
- const current = combined[entityId] || [];
13375
- // Use a Map to de-duplicate by timestamp.
13376
- const tmp = new Map();
13377
- for (let j = 0; j < current.length; j++) {
13378
- const record = current[j];
13379
- const dateTime = new Date(record.dateTime).getTime();
13380
- tmp.set(dateTime, record);
13381
- }
13382
- for (let j = 0; j < latest.length; j++) {
13383
- const record = latest[j];
13384
- const dateTime = new Date(record.dateTime).getTime();
13385
- tmp.set(dateTime, record);
13386
- }
13387
- // Convert to array and sort by date.
13388
- const sorted = Array.from(tmp.values()).sort((a, b) => {
13389
- return new Date(a.dateTime).getTime() - new Date(b.dateTime).getTime();
13390
- });
13391
- combined[entityId] = sorted;
13392
- }
13393
- }
13752
+ const { fetched, recordsByIds } = await this.historicGatherer.Gather(entities);
13394
13753
  if (!this.disposed) {
13395
- this.entitiesHistoric = combined;
13754
+ this.entitiesHistoric = recordsByIds;
13396
13755
  }
13397
- return [rangesToRequest.length > 0, combined];
13756
+ return [fetched, recordsByIds];
13398
13757
  }
13399
13758
  viewerDateTimeSub() {
13400
13759
  var _a, _b;
13401
13760
  if ((!((_a = this.item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historic) && !((_b = this.item.BruceEntity) === null || _b === void 0 ? void 0 : _b.historicAttrKey)) || this.viewerDateTimeChangeRemoval) {
13402
13761
  return;
13403
13762
  }
13404
- const THROTTLE_INTERVAL = 2000;
13763
+ // Scaled by how many Entities are on screen rather than fixed, since the cost of a refresh
13764
+ // is the set size and a large set barely changes between ticks anyway.
13405
13765
  let queue = new DelayQueue(() => {
13406
13766
  this.onGetterUpdate(Object.keys(this.renderedEntities));
13407
- }, THROTTLE_INTERVAL, true);
13767
+ }, this.historicGatherer.IntervalFor(Object.keys(this.renderedEntities).length), true);
13408
13768
  let clockTickRemoval;
13409
13769
  let prevTick = this.viewer.clock.currentTime.toString();
13410
13770
  clockTickRemoval = this.viewer.clock.onTick.addEventListener(() => {
@@ -14096,6 +14456,9 @@ class TilesetStyler {
14096
14456
  // Indicates that we are retrieving historic records.
14097
14457
  // This means that the current scene's time is included in the request.
14098
14458
  this.historic = false;
14459
+ // When false the Entity data is still gathered and the regos still updated, but no colour is
14460
+ // applied. A historic item that did not ask for styling needs the first without the second.
14461
+ this.applyStyles = true;
14099
14462
  this.reRenderState = new EntityReRenderMaintainState();
14100
14463
  // More expensive process.
14101
14464
  // When an Entity is styled, the rego is reviewed and updated if needed.
@@ -14132,13 +14495,16 @@ class TilesetStyler {
14132
14495
  }
14133
14496
  Init(params) {
14134
14497
  var _a, _b;
14135
- let { viewer, api, cTileset, fallbackStyleId, styleMapping, expandSources, menuItemId, register, scenario, historic } = params;
14498
+ let { viewer, api, cTileset, fallbackStyleId, styleMapping, expandSources, menuItemId, register, scenario, historic, applyStyles } = params;
14136
14499
  this.viewer = viewer;
14137
14500
  this.api = api;
14138
14501
  this.cTileset = cTileset;
14139
14502
  this.register = register;
14140
14503
  this.menuItemId = menuItemId;
14141
14504
  this.historic = Boolean(historic);
14505
+ if (applyStyles != null) {
14506
+ this.applyStyles = Boolean(applyStyles);
14507
+ }
14142
14508
  if (expandSources != null) {
14143
14509
  this.expandSources = expandSources;
14144
14510
  }
@@ -14632,33 +14998,31 @@ class TilesetStyler {
14632
14998
  return;
14633
14999
  }
14634
15000
  const { settings: style, styleId: resolvedStyleId } = this.getTilesetFeatureStyleWithId(rego.entityTypeId);
14635
- const fillColorTrace = (style && ((_a = style.modelStyle) === null || _a === void 0 ? void 0 : _a.fillColor)) ? Calculator.TraceGetColor(style.modelStyle.fillColor, data, tags) : { value: null, effective: null };
14636
- const bColor = fillColorTrace.value;
14637
- let cColor = null;
14638
- if (bColor == null) {
14639
- cColor = Color.WHITE;
14640
- }
14641
- else {
14642
- cColor = colorToCColor$3(bColor);
14643
- }
15001
+ const fillColorTrace = (this.applyStyles && style && ((_a = style.modelStyle) === null || _a === void 0 ? void 0 : _a.fillColor)) ? Calculator.TraceGetColor(style.modelStyle.fillColor, data, tags) : { value: null, effective: null };
15002
+ // Without styling the feature keeps whatever the tileset baked in. Falling through would paint
15003
+ // it the default white, which is a visible change for an item that only asked for historic.
14644
15004
  const override = this.overrideFeatureColor.get(rego.entityId) == true;
14645
- CesiumEntityStyler.SetDefaultColor({
14646
- color: cColor,
14647
- entity: visual,
14648
- viewer: this.viewer,
14649
- override: override
14650
- });
14651
- this.overrideFeatureColor.set(rego.entityId, true);
14652
- if (((_b = data === null || data === void 0 ? void 0 : data.Bruce) === null || _b === void 0 ? void 0 : _b.InternalID) != null) {
15005
+ if (this.applyStyles) {
15006
+ const bColor = fillColorTrace.value;
15007
+ const cColor = bColor == null ? Color.WHITE : colorToCColor$3(bColor);
15008
+ CesiumEntityStyler.SetDefaultColor({
15009
+ color: cColor,
15010
+ entity: visual,
15011
+ viewer: this.viewer,
15012
+ override: override
15013
+ });
15014
+ this.overrideFeatureColor.set(rego.entityId, true);
15015
+ if (resolvedStyleId != null) {
15016
+ rego.styleId = resolvedStyleId;
15017
+ }
15018
+ rego.styleEffective = (_b = StyleEffective.Combine([
15019
+ { key: "color", effective: fillColorTrace.effective }
15020
+ ])) !== null && _b !== void 0 ? _b : rego.styleEffective;
15021
+ }
15022
+ if (((_c = data === null || data === void 0 ? void 0 : data.Bruce) === null || _c === void 0 ? void 0 : _c.InternalID) != null) {
14653
15023
  rego.internalId = data.Bruce.InternalID;
14654
15024
  this.styledByInternalId.set(data.Bruce.InternalID, rego.entityId);
14655
15025
  }
14656
- if (resolvedStyleId != null) {
14657
- rego.styleId = resolvedStyleId;
14658
- }
14659
- rego.styleEffective = (_c = StyleEffective.Combine([
14660
- { key: "color", effective: fillColorTrace.effective }
14661
- ])) !== null && _c !== void 0 ? _c : rego.styleEffective;
14662
15026
  this.styledEntityIds.set(rego.entityId, true);
14663
15027
  this._styleProgressQueue.Call();
14664
15028
  // Since we only need to update it for scenarios right now.
@@ -15629,6 +15993,7 @@ var TilesetCadRenderManager;
15629
15993
  // Series of points to help interpolate movement when the timeline changes.
15630
15994
  this.historicPossesInitialLoaded = false;
15631
15995
  this.historicAnimation = null;
15996
+ this.historicGatherer = null;
15632
15997
  this.historicPossesLoadingProm = null;
15633
15998
  // Queue of loaded in features that we haven't processed yet.
15634
15999
  this.featureQueue = [];
@@ -16666,120 +17031,41 @@ var TilesetCadRenderManager;
16666
17031
  const api = this.getters.GetBruceApi({
16667
17032
  accountId: accountId
16668
17033
  });
16669
- // Time padding in milliseconds (15 seconds).
16670
- // Helps account for desync between client and server.
16671
- const TIME_PADDING_MS = 15000;
16672
- // 'start-stop' time string that maps to a pending request.
16673
- // Helps us avoid repeated requests that are the same.
16674
- const pendingRequests = new Map();
16675
- /**
16676
- * Returns a list of historic positions for a given time range.
16677
- * @param startStr
16678
- * @param stopStr
16679
- * @returns
17034
+ // The root Entity is the only one here, so nothing is banded and the plan sees a set of one.
17035
+ const rootEntity = { Bruce: { ID: this.rootId } };
17036
+ /*
17037
+ * Turns historic records into the animated positions this tileset moves along.
16680
17038
  */
16681
- const getPossesForRange = async (startStr, stopStr) => {
16682
- const requestKey = `${startStr}-${stopStr}`;
16683
- if (pendingRequests.has(requestKey)) {
16684
- return pendingRequests.get(requestKey);
16685
- }
16686
- const requestPromise = new Promise(async (res) => {
16687
- try {
16688
- const historicData = await EntityHistoricData.GetList({
16689
- attrKey: null,
16690
- dateTimeFrom: startStr,
16691
- dateTimeTo: stopStr,
16692
- entityIds: [this.rootId],
16693
- api: api
16694
- });
16695
- const posses = CesiumAnimatedProperty.GetSeriesPossesForHistoricEntity(this.viewer, HeightReference.NONE, HeightReference.CLAMP_TO_GROUND, historicData.recordsByIds[this.rootId]);
16696
- res(posses);
16697
- }
16698
- catch (e) {
16699
- console.error(e);
16700
- res([]);
16701
- }
16702
- finally {
16703
- pendingRequests.delete(requestKey);
16704
- }
16705
- });
16706
- pendingRequests.set(requestKey, requestPromise);
16707
- return requestPromise;
17039
+ const toPosses = (records) => {
17040
+ return CesiumAnimatedProperty.GetSeriesPossesForHistoricEntity(this.viewer, HeightReference.NONE, HeightReference.CLAMP_TO_GROUND, records);
16708
17041
  };
17042
+ this.historicGatherer = new HistoricGatherer({
17043
+ viewer: this.viewer,
17044
+ getApi: () => api,
17045
+ // The assembly root snapshots the whole Entity, so no key narrows it.
17046
+ getAttrKey: () => null,
17047
+ // Time padding in milliseconds (15 seconds).
17048
+ // Helps account for desync between client and server.
17049
+ paddingMs: 15000,
17050
+ onRecords: (recordsByIds) => {
17051
+ var _a;
17052
+ const records = recordsByIds[this.rootId];
17053
+ if (!(records === null || records === void 0 ? void 0 : records.length) || !((_a = this.historicAnimation) === null || _a === void 0 ? void 0 : _a.addPositions)) {
17054
+ return;
17055
+ }
17056
+ this.historicAnimation.addPositions(toPosses(records));
17057
+ }
17058
+ });
16709
17059
  /**
16710
- * Checks if the current timeline range is not fully loaded and requests the difference.
16711
- * This will call getPossesForRange() if it needs more data.
17060
+ * Fetches whatever the current timeline range needs that is not already held.
17061
+ * Positions reach the animation through the gatherer's callback as each batch lands.
16712
17062
  * @returns
16713
17063
  */
16714
17064
  const checkTimelineRange = async () => {
16715
- // Current timeline range.
16716
- const minDateTime = new Date(this.viewer.clock.startTime.toString()).getTime();
16717
- const maxDateTime = new Date(this.viewer.clock.stopTime.toString()).getTime();
16718
- // What we have loaded.
16719
- const range = this.historicAnimation.getDateRange();
16720
- const foundMinDateTime = (range === null || range === void 0 ? void 0 : range.minDate) ? range.minDate.getTime() : null;
16721
- const foundMaxDateTime = (range === null || range === void 0 ? void 0 : range.maxDate) ? range.maxDate.getTime() : null;
16722
- // See if the current range is within the range we already have.
16723
- if (this.historicPossesInitialLoaded &&
16724
- foundMinDateTime &&
16725
- foundMaxDateTime &&
16726
- minDateTime >= foundMinDateTime &&
16727
- maxDateTime <= foundMaxDateTime) {
16728
- return;
16729
- }
16730
- const tDetails = ViewUtils.GetTimeDetails({
16731
- viewer: this.viewer,
16732
- });
16733
- const isLive = tDetails.isLive;
16734
- // See if the requested range is before or after the range we have.
16735
- const fetchBefore = !foundMinDateTime || (!isLive && minDateTime < foundMinDateTime);
16736
- const fetchAfter = !foundMaxDateTime || maxDateTime > foundMaxDateTime;
16737
- if (!fetchBefore && !fetchAfter) {
16738
- // Already have the data we need.
17065
+ if (this.disposed) {
16739
17066
  return;
16740
17067
  }
16741
- // No known range so we need to fetch the whole thing.
16742
- if (!foundMinDateTime || !foundMaxDateTime) {
16743
- const startStr = new Date(minDateTime - TIME_PADDING_MS).toISOString();
16744
- const stopStr = new Date(maxDateTime + TIME_PADDING_MS).toISOString();
16745
- const newPositions = await getPossesForRange(startStr, stopStr);
16746
- if (this.disposed) {
16747
- return;
16748
- }
16749
- if (this.historicAnimation && this.historicAnimation.addPositions) {
16750
- this.historicAnimation.addPositions(newPositions);
16751
- }
16752
- }
16753
- else {
16754
- // The data we want is before the range we've currently loaded.
16755
- if (fetchBefore) {
16756
- // Calculate the missing difference and request it.
16757
- const startStr = new Date(minDateTime - TIME_PADDING_MS).toISOString();
16758
- const stopStr = new Date(foundMinDateTime + TIME_PADDING_MS).toISOString();
16759
- getPossesForRange(startStr, stopStr).then((newPositions) => {
16760
- if (this.disposed) {
16761
- return;
16762
- }
16763
- if (this.historicAnimation && this.historicAnimation.addPositions) {
16764
- this.historicAnimation.addPositions(newPositions);
16765
- }
16766
- });
16767
- }
16768
- // The data we want is after the range we've currently loaded.
16769
- if (fetchAfter) {
16770
- // Calculate the missing difference and request it.
16771
- const startStr = new Date(foundMaxDateTime - TIME_PADDING_MS).toISOString();
16772
- const stopStr = new Date(maxDateTime + TIME_PADDING_MS).toISOString();
16773
- getPossesForRange(startStr, stopStr).then((newPositions) => {
16774
- if (this.disposed) {
16775
- return;
16776
- }
16777
- if (this.historicAnimation && this.historicAnimation.addPositions) {
16778
- this.historicAnimation.addPositions(newPositions);
16779
- }
16780
- });
16781
- }
16782
- }
17068
+ await this.historicGatherer.Gather([rootEntity]);
16783
17069
  };
16784
17070
  /**
16785
17071
  * Requests the initial set of historic positions for the timeline range.
@@ -16793,11 +17079,8 @@ var TilesetCadRenderManager;
16793
17079
  res(false);
16794
17080
  return;
16795
17081
  }
16796
- const startTmp = JulianDate.toDate(this.viewer.clock.startTime);
16797
- const stopTmp = JulianDate.toDate(this.viewer.clock.stopTime);
16798
- const startStr = new Date(startTmp.getTime() - 1000).toISOString();
16799
- const stopStr = new Date(stopTmp.getTime() + 1000).toISOString();
16800
- const positions = await getPossesForRange(startStr, stopStr);
17082
+ const { recordsByIds } = await this.historicGatherer.Gather([rootEntity]);
17083
+ const positions = toPosses(recordsByIds[this.rootId] || []);
16801
17084
  if (this.disposed) {
16802
17085
  res(false);
16803
17086
  return;
@@ -19584,12 +19867,11 @@ var TilesetEntitiesRenderManager;
19584
19867
  }
19585
19868
  this.viewer.scene.requestRender();
19586
19869
  TilesetRenderEngine.OnTilesetReady(this.cTileset).then(() => {
19587
- var _a;
19588
19870
  try {
19589
19871
  if (this.disposed || this.viewer.isDestroyed()) {
19590
19872
  return;
19591
19873
  }
19592
- if (this.item.ApplyStyles) {
19874
+ if (Manager.NeedsStyler(this.item)) {
19593
19875
  const api = this.getters.GetBruceApi();
19594
19876
  this.styler.Init({
19595
19877
  viewer: this.viewer,
@@ -19600,7 +19882,8 @@ var TilesetEntitiesRenderManager;
19600
19882
  expandSources: false,
19601
19883
  menuItemId: this.item.id,
19602
19884
  register: this.visualsManager,
19603
- historic: (_a = this.item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historic
19885
+ historic: Manager.IsHistoric(this.item),
19886
+ applyStyles: Boolean(this.item.ApplyStyles)
19604
19887
  });
19605
19888
  }
19606
19889
  this.onCTilesetLoad();
@@ -19807,6 +20090,19 @@ var TilesetEntitiesRenderManager;
19807
20090
  get HandoffKey() {
19808
20091
  return Manager.GetHandoffKey(this.item);
19809
20092
  }
20093
+ /*
20094
+ * Returns whether the item asks for Entities as they were at the scene time.
20095
+ */
20096
+ static IsHistoric(item) {
20097
+ var _a, _b;
20098
+ return Boolean(((_a = item === null || item === void 0 ? void 0 : item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historic) || ((_b = item === null || item === void 0 ? void 0 : item.BruceEntity) === null || _b === void 0 ? void 0 : _b.historicAttrKey));
20099
+ }
20100
+ /*
20101
+ * Returns whether the styler has to run, which it does for historic even without styling.
20102
+ */
20103
+ static NeedsStyler(item) {
20104
+ return Boolean(item === null || item === void 0 ? void 0 : item.ApplyStyles) || Manager.IsHistoric(item);
20105
+ }
19810
20106
  static GetHandoffKey(item) {
19811
20107
  var _a, _b;
19812
20108
  const tilesetId = (_a = item === null || item === void 0 ? void 0 : item.tileset) === null || _a === void 0 ? void 0 : _a.TilesetID;
@@ -19816,7 +20112,10 @@ var TilesetEntitiesRenderManager;
19816
20112
  const accountId = ((_b = item.tileset) === null || _b === void 0 ? void 0 : _b.ClientAccountID) || "";
19817
20113
  // Styled <-> unstyled has no cheap revert path, force a reload instead.
19818
20114
  const applyStyles = Boolean(item.ApplyStyles);
19819
- return `${accountId}:${tilesetId}:${applyStyles}`;
20115
+ // Historic drives a different gather,
20116
+ // so a handoff must not reuse a styler set up for the other mode.
20117
+ const historic = Manager.IsHistoric(item);
20118
+ return `${accountId}:${tilesetId}:${applyStyles}:${historic}`;
19820
20119
  }
19821
20120
  // Returns null (fall back to a normal Dispose()) if the tileset hasn't finished loading yet.
19822
20121
  PrepareHandoff() {
@@ -19856,7 +20155,6 @@ var TilesetEntitiesRenderManager;
19856
20155
  // Takes ownership of a PrepareHandoff() payload instead of loading via Init().
19857
20156
  // Then re-parents every already-registered rego to this menu item and re-runs styling.
19858
20157
  AdoptHandoff(payload) {
19859
- var _a;
19860
20158
  this.cTileset = payload.cTileset;
19861
20159
  this.tileset = payload.tileset;
19862
20160
  this.typeId = payload.typeId;
@@ -19888,7 +20186,7 @@ var TilesetEntitiesRenderManager;
19888
20186
  toPriority: this.renderPriority,
19889
20187
  requestRender: false
19890
20188
  });
19891
- if (this.item.ApplyStyles) {
20189
+ if (Manager.NeedsStyler(this.item)) {
19892
20190
  this.styler.Init({
19893
20191
  viewer: this.viewer,
19894
20192
  api: this.getters.GetBruceApi(),
@@ -19898,7 +20196,8 @@ var TilesetEntitiesRenderManager;
19898
20196
  expandSources: false,
19899
20197
  menuItemId: this.item.id,
19900
20198
  register: this.visualsManager,
19901
- historic: (_a = this.item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historic
20199
+ historic: Manager.IsHistoric(this.item),
20200
+ applyStyles: Boolean(this.item.ApplyStyles)
19902
20201
  });
19903
20202
  this.queueHandoffRestyle(this.visualsManager.GetRegos({ menuItemId: this.item.id }));
19904
20203
  }
@@ -39989,6 +40288,107 @@ function ApplyGrayscaleColorMask(imageData, minColor, maxColor) {
39989
40288
  data[i + 3] = Math.round(srcAlpha * maskAlpha * 255);
39990
40289
  }
39991
40290
  }
40291
+ /**
40292
+ * The colour a ramp gives a normalised value.
40293
+ * @param sorted stops in ascending position, normalised into the texture's value range
40294
+ */
40295
+ function SampleRamp(sorted, t) {
40296
+ let upper = 0;
40297
+ while (upper < sorted.length && sorted[upper].position < t) {
40298
+ upper++;
40299
+ }
40300
+ if (upper === 0) {
40301
+ return sorted[0].color;
40302
+ }
40303
+ if (upper >= sorted.length) {
40304
+ return sorted[sorted.length - 1].color;
40305
+ }
40306
+ const a = sorted[upper - 1];
40307
+ const b = sorted[upper];
40308
+ const span = b.position - a.position;
40309
+ // Equal positions are a hard step rather than a division by zero, which is how a band
40310
+ // gets a crisp edge instead of a gradient into its neighbour.
40311
+ const f = span > 0 ? (t - a.position) / span : 1;
40312
+ return {
40313
+ red: a.color.red + (b.color.red - a.color.red) * f,
40314
+ green: a.color.green + (b.color.green - a.color.green) * f,
40315
+ blue: a.color.blue + (b.color.blue - a.color.blue) * f,
40316
+ alpha: a.color.alpha + (b.color.alpha - a.color.alpha) * f
40317
+ };
40318
+ }
40319
+ /**
40320
+ * A ramp baked into RGBA bytes, for a shader that cannot walk stops per fragment.
40321
+ * @param steps entries in the lookup, each covering an equal slice of the value range
40322
+ */
40323
+ function RampLookup(stops, low, high, steps = 256) {
40324
+ const sorted = stops && stops.length > 0
40325
+ ? stops.slice().sort((a, b) => a.position - b.position)
40326
+ : null;
40327
+ const out = new Uint8Array(steps * 4);
40328
+ for (let i = 0; i < steps; i++) {
40329
+ const t = steps > 1 ? i / (steps - 1) : 0;
40330
+ const color = sorted
40331
+ ? SampleRamp(sorted, t)
40332
+ : {
40333
+ red: low.red + (high.red - low.red) * t,
40334
+ green: low.green + (high.green - low.green) * t,
40335
+ blue: low.blue + (high.blue - low.blue) * t,
40336
+ alpha: low.alpha + (high.alpha - low.alpha) * t
40337
+ };
40338
+ out[i * 4] = Math.round(color.red);
40339
+ out[i * 4 + 1] = Math.round(color.green);
40340
+ out[i * 4 + 2] = Math.round(color.blue);
40341
+ out[i * 4 + 3] = Math.round(Math.min(1, Math.max(0, color.alpha)) * 255);
40342
+ }
40343
+ return out;
40344
+ }
40345
+ function ApplyGradientStops(imageData, stops) {
40346
+ if (!stops || stops.length === 0) {
40347
+ return;
40348
+ }
40349
+ const sorted = stops.slice().sort((a, b) => a.position - b.position);
40350
+ const data = imageData.data;
40351
+ for (let i = 0; i < data.length; i += 4) {
40352
+ const t = data[i] / 255;
40353
+ const color = SampleRamp(sorted, t);
40354
+ data[i] = Math.round(color.red);
40355
+ data[i + 1] = Math.round(color.green);
40356
+ data[i + 2] = Math.round(color.blue);
40357
+ const srcAlpha = data[i + 3] / 255;
40358
+ data[i + 3] = Math.round(srcAlpha * color.alpha * 255);
40359
+ }
40360
+ }
40361
+ /**
40362
+ * Draws the texture's own cell grid into the tinted pixels.
40363
+ *
40364
+ * Drawn in texture space rather than screen space so it stays with the cells it describes and needs
40365
+ * no per-frame work as the camera moves.
40366
+ */
40367
+ function ApplyCellBorders(imageData, cellTexels, widthTexels, color) {
40368
+ if (!(cellTexels > 0) || !(widthTexels > 0) || !color || color.alpha <= 0) {
40369
+ return;
40370
+ }
40371
+ const { width, height, data } = imageData;
40372
+ const edge = Math.max(1, Math.round(widthTexels));
40373
+ for (let y = 0; y < height; y++) {
40374
+ const onRow = (y % cellTexels) < edge;
40375
+ for (let x = 0; x < width; x++) {
40376
+ if (!onRow && (x % cellTexels) >= edge) {
40377
+ continue;
40378
+ }
40379
+ const i = (y * width + x) * 4;
40380
+ // Only where something is already drawn, so a border cannot invent coverage the data
40381
+ // does not have.
40382
+ if (data[i + 3] === 0) {
40383
+ continue;
40384
+ }
40385
+ const a = color.alpha;
40386
+ data[i] = Math.round(data[i] * (1 - a) + color.red * a);
40387
+ data[i + 1] = Math.round(data[i + 1] * (1 - a) + color.green * a);
40388
+ data[i + 2] = Math.round(data[i + 2] * (1 - a) + color.blue * a);
40389
+ }
40390
+ }
40391
+ }
39992
40392
  function loadImage(src) {
39993
40393
  return new Promise((res, rej) => {
39994
40394
  const image = new Image();
@@ -40000,6 +40400,87 @@ function loadImage(src) {
40000
40400
 
40001
40401
  var TextureFrameSeriesAnimator;
40002
40402
  (function (TextureFrameSeriesAnimator) {
40403
+ // Values are measured up from the ground beneath them.
40404
+ TextureFrameSeriesAnimator.QUANTITY_THICKNESS = "thickness";
40405
+ // Values are heights against a vertical datum, so they need that datum to be placed.
40406
+ TextureFrameSeriesAnimator.QUANTITY_ELEVATION = "elevation";
40407
+ TextureFrameSeriesAnimator.LAYOUT_TILES = "tiles";
40408
+ /**
40409
+ * The generator that produced an archive, for telling a stale one from a current one.
40410
+ *
40411
+ * Read the version rather than sniffing for fields: an archive can legitimately omit Tiles or
40412
+ * GeoidSeparation and still be current, so their absence says nothing about its age.
40413
+ * @param metadata the archive's Data.generation
40414
+ */
40415
+ function GeneratorVersion(metadata) {
40416
+ if (!metadata || typeof metadata.GeneratorVersion !== "number") {
40417
+ return 1;
40418
+ }
40419
+ return metadata.GeneratorVersion;
40420
+ }
40421
+ TextureFrameSeriesAnimator.GeneratorVersion = GeneratorVersion;
40422
+ /**
40423
+ * Whether an archive is an adaptive pyramid rather than one raster.
40424
+ * @param metadata the archive's Data.generation
40425
+ */
40426
+ function IsTiledLayout(metadata) {
40427
+ return (Boolean(metadata && metadata.Layout === TextureFrameSeriesAnimator.LAYOUT_TILES &&
40428
+ metadata.Tiles && metadata.Tiles.length > 0));
40429
+ }
40430
+ TextureFrameSeriesAnimator.IsTiledLayout = IsTiledLayout;
40431
+ // Frames carry the value as the source reported it.
40432
+ TextureFrameSeriesAnimator.MEASURE_ABSOLUTE = "absolute";
40433
+ // Frames carry the value minus each cell's own minimum over the series.
40434
+ TextureFrameSeriesAnimator.MEASURE_ANOMALY = "anomaly";
40435
+ /**
40436
+ * Turns a value in the attribute's own units into the 0 to 1 position the ramp is indexed by.
40437
+ *
40438
+ * Stops are authored in real units, since "hide anything under 0.1 m" is the sentence a user
40439
+ * actually has, and the archive's own published range is what makes that expressible.
40440
+ * @param metadata the archive's Data.generation
40441
+ * @param value in the attribute's units
40442
+ */
40443
+ function NormalisePosition(metadata, value) {
40444
+ const lo = metadata && typeof metadata.ValueMin === "number" ? metadata.ValueMin : 0;
40445
+ const hi = metadata && typeof metadata.ValueMax === "number" ? metadata.ValueMax : 1;
40446
+ const span = hi - lo;
40447
+ if (!(span > 0)) {
40448
+ return 0;
40449
+ }
40450
+ return Math.min(1, Math.max(0, (value - lo) / span));
40451
+ }
40452
+ TextureFrameSeriesAnimator.NormalisePosition = NormalisePosition;
40453
+ /**
40454
+ * Whether the frames are a departure from each cell's own normal rather than a raw reading.
40455
+ *
40456
+ * Worth asking before labelling anything: the two measures need different words for the same
40457
+ * number, and calling an anomaly "water depth" is a worse error than the coarse ramp it replaced.
40458
+ * @param metadata the archive's Data.generation
40459
+ */
40460
+ function IsAnomalyMeasure(metadata) {
40461
+ return Boolean(metadata && metadata.Measure === TextureFrameSeriesAnimator.MEASURE_ANOMALY);
40462
+ }
40463
+ TextureFrameSeriesAnimator.IsAnomalyMeasure = IsAnomalyMeasure;
40464
+ /**
40465
+ * How much the value moves anywhere in the series, in source units.
40466
+ *
40467
+ * Falls back to the published range, which is the right answer for an anomaly archive (its range
40468
+ * IS the movement) and the only available one for an archive predating AnomalyMax.
40469
+ * @param metadata the archive's Data.generation
40470
+ */
40471
+ function MovingRange(metadata) {
40472
+ if (!metadata) {
40473
+ return undefined;
40474
+ }
40475
+ if (typeof metadata.AnomalyMax === "number" && metadata.AnomalyMax > 0) {
40476
+ return metadata.AnomalyMax;
40477
+ }
40478
+ if (typeof metadata.ValueMin === "number" && typeof metadata.ValueMax === "number") {
40479
+ return Math.abs(metadata.ValueMax - metadata.ValueMin);
40480
+ }
40481
+ return undefined;
40482
+ }
40483
+ TextureFrameSeriesAnimator.MovingRange = MovingRange;
40003
40484
  /**
40004
40485
  * Detects whether a ClientFile's `Data.generation` metadata describes a frame archive rather than a single static image,
40005
40486
  * so a caller can decide whether to construct an Animator or fall back to the existing static-texture path.
@@ -40009,15 +40490,51 @@ var TextureFrameSeriesAnimator;
40009
40490
  return Boolean(data && data.generation && Array.isArray(data.generation.Frames) && data.generation.Frames.length > 0);
40010
40491
  }
40011
40492
  TextureFrameSeriesAnimator.IsFrameArchiveMetadata = IsFrameArchiveMetadata;
40012
- // How far covered values bleed outward into uncovered texels before the GPU samples them.
40493
+ // First resolvable static number out of a calculator field list, since that is all a border needs.
40494
+ function resolveNumber(fields) {
40495
+ if (!fields) {
40496
+ return 0;
40497
+ }
40498
+ for (const field of fields) {
40499
+ const value = Number(field && field.value);
40500
+ if (Number.isFinite(value)) {
40501
+ return value;
40502
+ }
40503
+ }
40504
+ return 0;
40505
+ }
40506
+ // First resolvable static colour out of a calculator field list.
40507
+ function resolveColor(fields) {
40508
+ if (!fields) {
40509
+ return null;
40510
+ }
40511
+ for (const field of fields) {
40512
+ const parsed = typeof (field === null || field === void 0 ? void 0 : field.value) === "string"
40513
+ ? Color$1.ColorFromStr(field.value)
40514
+ : null;
40515
+ if (parsed) {
40516
+ return parsed;
40517
+ }
40518
+ }
40519
+ return null;
40520
+ }
40521
+ /*
40522
+ * Everything about an Animator that a style can change, as a comparable string.
40523
+ */
40524
+ function AppearanceSignature(options) {
40525
+ var _a, _b, _c;
40526
+ return JSON.stringify([
40527
+ (_a = options.textureColorMask) !== null && _a !== void 0 ? _a : null,
40528
+ (_b = options.cellBorder) !== null && _b !== void 0 ? _b : null,
40529
+ (_c = options.cellTexels) !== null && _c !== void 0 ? _c : null,
40530
+ Boolean(options.maskBaseline)
40531
+ ]);
40532
+ }
40533
+ TextureFrameSeriesAnimator.AppearanceSignature = AppearanceSignature;
40534
+ const MAX_COMPOSITE_TEXELS = 2048;
40013
40535
  const VALUE_DILATE_PASSES = 2;
40014
40536
  /*
40015
40537
  * Bleeds covered values outward into uncovered texels, leaving alpha untouched.
40016
- *
40017
- * The GPU samples the value texture with linear filtering, so an uncovered texel's RGB still gets
40018
- * averaged into the vertices next to it even though its alpha is zero. Land sits at one end of the
40019
- * ramp, so without this the coastline grows a row of spikes exactly where the data stops. Masking the
40020
- * baseline creates more of these edges, which is what makes this necessary rather than cosmetic.
40021
40538
  */
40022
40539
  function dilateValues(value, width, height, passes) {
40023
40540
  const texels = width * height;
@@ -40063,7 +40580,7 @@ var TextureFrameSeriesAnimator;
40063
40580
  const DEFAULT_HIGH_COLOR = { red: 21, green: 96, blue: 196, alpha: 0.92 };
40064
40581
  class Animator {
40065
40582
  constructor(options) {
40066
- var _a, _b;
40583
+ var _a, _b, _c;
40067
40584
  this.removeOnTick = null;
40068
40585
  this.removeCrossfadeTick = null;
40069
40586
  this.disposed = false;
@@ -40089,8 +40606,12 @@ var TextureFrameSeriesAnimator;
40089
40606
  // forcing Cesium to re-upload the texture only when something changed.
40090
40607
  this.pool = [document.createElement("canvas"), document.createElement("canvas")];
40091
40608
  this.poolIdx = 0;
40609
+ this.scratch = null;
40092
40610
  this.valueDims = null;
40093
40611
  this.presentVersion = 0;
40612
+ this.extremesLoad = null;
40613
+ this.floorPixels = null;
40614
+ this.ceilingPixels = null;
40094
40615
  if (!options.entity.polygon) {
40095
40616
  throw new Error("TextureFrameSeriesAnimator requires an entity with polygon graphics.");
40096
40617
  }
@@ -40102,16 +40623,41 @@ var TextureFrameSeriesAnimator;
40102
40623
  this.archiveUrl = options.archiveUrl;
40103
40624
  this.frames = options.frames;
40104
40625
  this.crossfadeMs = (_a = options.crossfadeMs) !== null && _a !== void 0 ? _a : DEFAULT_CROSSFADE_MS;
40626
+ this.appearance = AppearanceSignature(options);
40105
40627
  const mask = options.textureColorMask;
40106
40628
  this.lowColor = (mask && Color$1.ColorFromStr(mask.minColor)) || DEFAULT_LOW_COLOR;
40107
40629
  this.highColor = (mask && Color$1.ColorFromStr(mask.maxColor)) || DEFAULT_HIGH_COLOR;
40630
+ // Positions are authored in the attribute's units and normalised once here, so the
40631
+ // per-pixel loop stays a comparison against 0 to 1 like the two-colour path.
40632
+ const points = mask === null || mask === void 0 ? void 0 : mask.points;
40633
+ this.rampStops = (points && points.length > 0)
40634
+ ? points.map((p) => ({
40635
+ position: NormalisePosition(options.metadata, p.position),
40636
+ color: Color$1.ColorFromStr(p.color) || DEFAULT_LOW_COLOR
40637
+ }))
40638
+ : null;
40639
+ const border = options.cellBorder;
40640
+ const borderWidth = border ? resolveNumber(border.width) : 0;
40641
+ const borderColor = border ? resolveColor(border.color) : null;
40642
+ // Zero width or a fully transparent colour means no grid, matching how the polygon's own
40643
+ // outline behaves, so an existing style that wants no border keeps getting none.
40644
+ this.cellBorder = (border && borderWidth > 0 && borderColor && borderColor.alpha > 0)
40645
+ ? {
40646
+ cellTexels: Math.max(1, Math.round((_b = options.cellTexels) !== null && _b !== void 0 ? _b : 1)),
40647
+ widthTexels: borderWidth,
40648
+ color: borderColor
40649
+ }
40650
+ : null;
40108
40651
  this.frameDates = this.frames.map((f) => JulianDate.fromIso8601(f.Timestamp));
40109
40652
  this.frameCache = new Array(this.frames.length).fill(null);
40110
40653
  this.frameDims = new Array(this.frames.length).fill(null);
40111
40654
  this.valueCache = new Array(this.frames.length).fill(null);
40112
40655
  this.produceValueCanvas = Boolean(options.produceValueCanvas);
40656
+ this.metadata = options.metadata;
40657
+ this.drapeExtent = options.drapeExtent;
40658
+ this.tiles = IsTiledLayout(options.metadata) ? options.metadata.Tiles : undefined;
40113
40659
  this.baselineMaskEntry = options.baselineMask || null;
40114
- this.maskBaseline = (_b = options.maskBaseline) !== null && _b !== void 0 ? _b : Boolean(options.baselineMask);
40660
+ this.maskBaseline = (_c = options.maskBaseline) !== null && _c !== void 0 ? _c : Boolean(options.baselineMask);
40115
40661
  this.valueCanvas = this.produceValueCanvas ? document.createElement("canvas") : null;
40116
40662
  this.driveMaterial = options.driveMaterial !== false;
40117
40663
  this.originalMaterial = options.entity.polygon.material;
@@ -40147,6 +40693,10 @@ var TextureFrameSeriesAnimator;
40147
40693
  IsDisposed() {
40148
40694
  return this.disposed;
40149
40695
  }
40696
+ // What this Animator was built to look like, for deciding whether it can be reused.
40697
+ GetAppearanceSignature() {
40698
+ return this.appearance;
40699
+ }
40150
40700
  /**
40151
40701
  * The archive URL this instance was constructed with,
40152
40702
  * lets a caller re-rendering the same entity tell whether an existing instance is already correct, without reaching into private state.
@@ -40161,6 +40711,15 @@ var TextureFrameSeriesAnimator;
40161
40711
  GetImageProperty() {
40162
40712
  return this.imageProperty;
40163
40713
  }
40714
+ /**
40715
+ * The presented frame's TINTED pixels, where alpha is what the ramp actually painted.
40716
+ *
40717
+ * Distinct from the value canvas, whose alpha only says a texel has data. A cell can hold a
40718
+ * reading and still be painted nothing, which is exactly what a hidden floor band does.
40719
+ */
40720
+ GetDisplayedPixels() {
40721
+ return this.displayedPixels;
40722
+ }
40164
40723
  /**
40165
40724
  * The untinted canvas carrying the presented frame's value in RGB and its coverage in alpha,
40166
40725
  * or null unless the instance was constructed with produceValueCanvas.
@@ -40273,11 +40832,33 @@ var TextureFrameSeriesAnimator;
40273
40832
  });
40274
40833
  }
40275
40834
  async fetchAndTint(idx) {
40276
- const entry = this.frames[idx];
40277
- const start = entry.ByteOffset;
40278
- const end = entry.ByteOffset + entry.ByteLength - 1;
40279
- const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${start}-${end}` } });
40280
- const buffer = await response.arrayBuffer();
40835
+ const tiles = this.tiles;
40836
+ let buffer;
40837
+ let span = 0;
40838
+ if (tiles) {
40839
+ // Every tile's frame for one timestep sits contiguously, because the generator writes
40840
+ // frame major. So a tiled frame is still ONE range request, not one per tile.
40841
+ let lo = Number.MAX_SAFE_INTEGER;
40842
+ let hi = 0;
40843
+ for (const tile of tiles) {
40844
+ const at = tile.Frames[idx];
40845
+ if (!at) {
40846
+ continue;
40847
+ }
40848
+ lo = Math.min(lo, at.ByteOffset);
40849
+ hi = Math.max(hi, at.ByteOffset + at.ByteLength);
40850
+ }
40851
+ span = lo;
40852
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
40853
+ buffer = await response.arrayBuffer();
40854
+ }
40855
+ else {
40856
+ const entry = this.frames[idx];
40857
+ const start = entry.ByteOffset;
40858
+ const end = entry.ByteOffset + entry.ByteLength - 1;
40859
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${start}-${end}` } });
40860
+ buffer = await response.arrayBuffer();
40861
+ }
40281
40862
  if (this.disposed) {
40282
40863
  return;
40283
40864
  }
@@ -40287,7 +40868,9 @@ var TextureFrameSeriesAnimator;
40287
40868
  if (this.disposed) {
40288
40869
  return;
40289
40870
  }
40290
- const decoded = await this.decodeAndTint(buffer);
40871
+ const decoded = tiles
40872
+ ? await this.composeTiles(tiles, idx, buffer, span)
40873
+ : await this.decodeAndTint(buffer);
40291
40874
  if (this.disposed) {
40292
40875
  return;
40293
40876
  }
@@ -40316,21 +40899,438 @@ var TextureFrameSeriesAnimator;
40316
40899
  canvas.height = image.height;
40317
40900
  const ctx = canvas.getContext("2d");
40318
40901
  ctx.drawImage(image, 0, 0);
40319
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
40902
+ return this.tint(ctx.getImageData(0, 0, canvas.width, canvas.height));
40903
+ }
40904
+ /*
40905
+ * Draws every tile of one frame into a single raster covering the drape extent.
40906
+ */
40907
+ async composeTiles(tiles, idx, buffer, spanStart) {
40908
+ const target = this.compositeExtent();
40909
+ const size = this.compositeSize(tiles, target);
40910
+ const canvas = document.createElement("canvas");
40911
+ canvas.width = size.width;
40912
+ canvas.height = size.height;
40913
+ const ctx = canvas.getContext("2d");
40914
+ ctx.imageSmoothingEnabled = false;
40915
+ ctx.clearRect(0, 0, size.width, size.height);
40916
+ const images = await this.decodeSlices(buffer, spanStart, tiles.map((tile) => tile.Frames[idx]));
40917
+ for (let i = 0; i < tiles.length; i++) {
40918
+ const image = images[i];
40919
+ if (!image || this.disposed) {
40920
+ continue;
40921
+ }
40922
+ const at = this.tileRect(tiles[i], target, size);
40923
+ ctx.drawImage(image, at.x, at.y, at.w, at.h);
40924
+ }
40925
+ return this.tint(ctx.getImageData(0, 0, size.width, size.height));
40926
+ }
40927
+ /*
40928
+ * Where a composited frame is draped, which is the polygon's rectangle when there is one.
40929
+ */
40930
+ compositeExtent() {
40931
+ if (this.drapeExtent) {
40932
+ return this.drapeExtent;
40933
+ }
40934
+ const m = this.metadata;
40935
+ return {
40936
+ West: m && m.West != null ? m.West : -180,
40937
+ East: m && m.East != null ? m.East : 180,
40938
+ South: m && m.South != null ? m.South : -90,
40939
+ North: m && m.North != null ? m.North : 90
40940
+ };
40941
+ }
40942
+ /*
40943
+ * Composite resolution, capped by what can be uploaded as a texture every frame.
40944
+ */
40945
+ compositeSize(tiles, target) {
40946
+ let finest = Number.MAX_VALUE;
40947
+ for (const tile of tiles) {
40948
+ if (tile.TexelMetres > 0) {
40949
+ finest = Math.min(finest, tile.TexelMetres);
40950
+ }
40951
+ }
40952
+ if (!(finest > 0) || finest === Number.MAX_VALUE) {
40953
+ finest = 1;
40954
+ }
40955
+ const mid = (target.South + target.North) / 2 * Math.PI / 180;
40956
+ const widthM = Math.max((target.East - target.West) * 111320 * Math.cos(mid), 1);
40957
+ const heightM = Math.max((target.North - target.South) * 110574, 1);
40958
+ let width = Math.ceil(widthM / finest);
40959
+ let height = Math.ceil(heightM / finest);
40960
+ const longest = Math.max(width, height);
40961
+ if (longest > MAX_COMPOSITE_TEXELS) {
40962
+ const shrink = MAX_COMPOSITE_TEXELS / longest;
40963
+ width = Math.max(1, Math.round(width * shrink));
40964
+ height = Math.max(1, Math.round(height * shrink));
40965
+ }
40966
+ return { width: Math.max(1, width), height: Math.max(1, height) };
40967
+ }
40968
+ /*
40969
+ * Applies the ramp, borders and baseline mask to raw value pixels, whatever produced them.
40970
+ */
40971
+ tint(imageData) {
40972
+ const canvas = { width: imageData.width, height: imageData.height };
40320
40973
  // Copied before tinting: the ramp is a lerp between two colours, so the value cannot be
40321
40974
  // recovered from the tinted pixels afterwards.
40322
40975
  const valuePixels = this.produceValueCanvas ? new Uint8ClampedArray(imageData.data) : undefined;
40323
- ApplyGrayscaleColorMask(imageData, this.lowColor, this.highColor);
40976
+ // Stops win when supplied: they can express a band and a hidden floor, which a two
40977
+ // colour ramp cannot. The pair stays the fallback so an older style still draws.
40978
+ if (this.rampStops && this.rampStops.length > 0) {
40979
+ ApplyGradientStops(imageData, this.rampStops);
40980
+ }
40981
+ else {
40982
+ ApplyGrayscaleColorMask(imageData, this.lowColor, this.highColor);
40983
+ }
40984
+ if (this.cellBorder) {
40985
+ ApplyCellBorders(imageData, this.cellBorder.cellTexels, this.cellBorder.widthTexels, this.cellBorder.color);
40986
+ }
40324
40987
  this.applyBaselineMask(imageData.data, valuePixels, canvas.width, canvas.height);
40325
40988
  if (valuePixels) {
40326
40989
  dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES);
40327
40990
  }
40328
40991
  return { pixels: imageData.data, width: canvas.width, height: canvas.height, valuePixels };
40329
40992
  }
40993
+ /*
40994
+ * Paints one frame's pixels onto a canvas, resampled into the drape extent when there is one.
40995
+ */
40996
+ writeInto(canvas, pixels, dims) {
40997
+ const rect = this.sourceRect(dims);
40998
+ if (!rect) {
40999
+ canvas.width = dims.width;
41000
+ canvas.height = dims.height;
41001
+ const direct = canvas.getContext("2d");
41002
+ const image = direct.createImageData(dims.width, dims.height);
41003
+ image.data.set(pixels);
41004
+ direct.putImageData(image, 0, 0);
41005
+ return;
41006
+ }
41007
+ if (!this.scratch) {
41008
+ this.scratch = document.createElement("canvas");
41009
+ }
41010
+ this.scratch.width = dims.width;
41011
+ this.scratch.height = dims.height;
41012
+ const scratchCtx = this.scratch.getContext("2d");
41013
+ const image = scratchCtx.createImageData(dims.width, dims.height);
41014
+ image.data.set(pixels);
41015
+ scratchCtx.putImageData(image, 0, 0);
41016
+ canvas.width = dims.width;
41017
+ canvas.height = dims.height;
41018
+ const ctx = canvas.getContext("2d");
41019
+ // Nearest neighbour: a value texel has to survive the resample exactly, or a label reads
41020
+ // a blend of two cells rather than the cell it points at.
41021
+ ctx.imageSmoothingEnabled = false;
41022
+ ctx.clearRect(0, 0, dims.width, dims.height);
41023
+ ctx.drawImage(this.scratch, rect.x, rect.y, rect.width, rect.height, 0, 0, dims.width, dims.height);
41024
+ }
41025
+ /*
41026
+ * The part of the archive raster that the drape extent covers, in source pixels.
41027
+ */
41028
+ sourceRect(dims) {
41029
+ const drape = this.drapeExtent;
41030
+ const m = this.metadata;
41031
+ // A composited frame was drawn into the drape extent already, so resampling it again
41032
+ // would apply the same correction twice.
41033
+ if (this.tiles) {
41034
+ return null;
41035
+ }
41036
+ if (!drape || !m || m.West == null || m.East == null || m.South == null || m.North == null) {
41037
+ return null;
41038
+ }
41039
+ const spanLon = m.East - m.West;
41040
+ const spanLat = m.North - m.South;
41041
+ if (!(spanLon > 0) || !(spanLat > 0)) {
41042
+ return null;
41043
+ }
41044
+ const x = (drape.West - m.West) / spanLon * dims.width;
41045
+ const width = (drape.East - drape.West) / spanLon * dims.width;
41046
+ const y = (m.North - drape.North) / spanLat * dims.height;
41047
+ const height = (drape.North - drape.South) / spanLat * dims.height;
41048
+ // A drape that already matches the archive is the common case and must not pay for a
41049
+ // resample, nor lose a half pixel to rounding.
41050
+ if (Math.abs(x) < 0.01 && Math.abs(y) < 0.01
41051
+ && Math.abs(width - dims.width) < 0.01 && Math.abs(height - dims.height) < 0.01) {
41052
+ return null;
41053
+ }
41054
+ return { x, y, width, height };
41055
+ }
41056
+ /**
41057
+ * The archive's per-cell floor and ceiling rasters, once EnsureExtremes has resolved.
41058
+ */
41059
+ GetExtremes() {
41060
+ return { floor: this.floorPixels, ceiling: this.ceilingPixels };
41061
+ }
41062
+ /**
41063
+ * Fetches and decodes the floor and ceiling rasters, at most once.
41064
+ */
41065
+ EnsureExtremes() {
41066
+ if (this.extremesLoad) {
41067
+ return this.extremesLoad;
41068
+ }
41069
+ if (this.tiles) {
41070
+ this.extremesLoad = this.loadTiledExtremes();
41071
+ return this.extremesLoad;
41072
+ }
41073
+ const floor = this.metadata && this.metadata.Floor;
41074
+ const ceiling = this.metadata && this.metadata.Ceiling;
41075
+ if (!floor && !ceiling) {
41076
+ this.extremesLoad = Promise.resolve();
41077
+ return this.extremesLoad;
41078
+ }
41079
+ this.extremesLoad = Promise.all([
41080
+ floor ? this.loadImageDataAt(floor) : Promise.resolve(null),
41081
+ ceiling ? this.loadImageDataAt(ceiling) : Promise.resolve(null)
41082
+ ]).then(([f, c]) => {
41083
+ if (this.disposed) {
41084
+ return;
41085
+ }
41086
+ this.floorPixels = f;
41087
+ this.ceilingPixels = c;
41088
+ }).catch((e) => {
41089
+ // Missing extremes cost a label its range, and must not take the animation with them.
41090
+ console.warn("TextureFrameSeriesAnimator: could not load the extremes rasters.", e);
41091
+ });
41092
+ return this.extremesLoad;
41093
+ }
41094
+ /*
41095
+ * Composites each tile's baseline mask into one covering the composited frames.
41096
+ *
41097
+ * A tiled archive publishes the mask per tile, so reading the archive level BaselineMask
41098
+ * finds nothing and hiding dry cells silently does nothing at all.
41099
+ */
41100
+ async loadTiledBaselineMask() {
41101
+ const tiles = this.tiles;
41102
+ if (!tiles) {
41103
+ return;
41104
+ }
41105
+ const entries = tiles.map((tile) => tile.BaselineMask);
41106
+ let lo = Number.MAX_SAFE_INTEGER;
41107
+ let hi = 0;
41108
+ for (const at of entries) {
41109
+ if (!at) {
41110
+ continue;
41111
+ }
41112
+ lo = Math.min(lo, at.ByteOffset);
41113
+ hi = Math.max(hi, at.ByteOffset + at.ByteLength);
41114
+ }
41115
+ if (!(hi > lo)) {
41116
+ return;
41117
+ }
41118
+ try {
41119
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
41120
+ const block = await response.arrayBuffer();
41121
+ if (this.disposed) {
41122
+ return;
41123
+ }
41124
+ const images = await this.decodeSlices(block, lo, entries);
41125
+ if (this.disposed) {
41126
+ return;
41127
+ }
41128
+ const target = this.compositeExtent();
41129
+ const size = this.compositeSize(tiles, target);
41130
+ const canvas = document.createElement("canvas");
41131
+ canvas.width = size.width;
41132
+ canvas.height = size.height;
41133
+ const ctx = canvas.getContext("2d");
41134
+ ctx.imageSmoothingEnabled = false;
41135
+ ctx.clearRect(0, 0, size.width, size.height);
41136
+ for (let i = 0; i < tiles.length; i++) {
41137
+ if (!images[i]) {
41138
+ continue;
41139
+ }
41140
+ const at = this.tileRect(tiles[i], target, size);
41141
+ ctx.drawImage(images[i], at.x, at.y, at.w, at.h);
41142
+ }
41143
+ const pixels = ctx.getImageData(0, 0, size.width, size.height).data;
41144
+ const flags = new Uint8Array(size.width * size.height);
41145
+ for (let p = 0; p < flags.length; p++) {
41146
+ flags[p] = pixels[p * 4 + 3] >= 128 ? 1 : 0;
41147
+ }
41148
+ this.baselineFlags = flags;
41149
+ this.baselineDims = { width: size.width, height: size.height };
41150
+ }
41151
+ catch (e) {
41152
+ console.warn("TextureFrameSeriesAnimator: could not load the tiled baseline mask.", e);
41153
+ }
41154
+ }
41155
+ /*
41156
+ * Composites each tile's floor and ceiling into rasters matching the composited frames.
41157
+ *
41158
+ * A tiled archive has no whole-extent extremes to fetch, only per-tile ones, and a label
41159
+ * indexes them by the same texel as the value canvas, so they have to be laid out the same way.
41160
+ */
41161
+ async loadTiledExtremes() {
41162
+ const tiles = this.tiles;
41163
+ if (!tiles) {
41164
+ return;
41165
+ }
41166
+ const target = this.compositeExtent();
41167
+ const size = this.compositeSize(tiles, target);
41168
+ // One request for the whole extremes block. They sit contiguously after the frames, so
41169
+ // fetching per tile would be 164 round trips for an 82 tile pyramid.
41170
+ let lo = Number.MAX_SAFE_INTEGER;
41171
+ let hi = 0;
41172
+ for (const tile of tiles) {
41173
+ for (const at of [tile.Floor, tile.Ceiling]) {
41174
+ if (!at) {
41175
+ continue;
41176
+ }
41177
+ lo = Math.min(lo, at.ByteOffset);
41178
+ hi = Math.max(hi, at.ByteOffset + at.ByteLength);
41179
+ }
41180
+ }
41181
+ if (!(hi > lo)) {
41182
+ return;
41183
+ }
41184
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
41185
+ const block = await response.arrayBuffer();
41186
+ if (this.disposed) {
41187
+ return;
41188
+ }
41189
+ const draw = async (pick) => {
41190
+ const images = await this.decodeSlices(block, lo, tiles.map(pick));
41191
+ if (this.disposed) {
41192
+ return null;
41193
+ }
41194
+ const canvas = document.createElement("canvas");
41195
+ canvas.width = size.width;
41196
+ canvas.height = size.height;
41197
+ const ctx = canvas.getContext("2d");
41198
+ ctx.imageSmoothingEnabled = false;
41199
+ ctx.clearRect(0, 0, size.width, size.height);
41200
+ let drew = false;
41201
+ for (let i = 0; i < tiles.length; i++) {
41202
+ const image = images[i];
41203
+ if (!image) {
41204
+ continue;
41205
+ }
41206
+ const at = this.tileRect(tiles[i], target, size);
41207
+ ctx.drawImage(image, at.x, at.y, at.w, at.h);
41208
+ drew = true;
41209
+ }
41210
+ return drew ? ctx.getImageData(0, 0, size.width, size.height) : null;
41211
+ };
41212
+ try {
41213
+ this.floorPixels = await draw((tile) => tile.Floor);
41214
+ this.ceilingPixels = await draw((tile) => tile.Ceiling);
41215
+ }
41216
+ catch (e) {
41217
+ console.warn("TextureFrameSeriesAnimator: could not load the tiled extremes.", e);
41218
+ }
41219
+ }
41220
+ /*
41221
+ * Where one tile lands on the composite, snapped to whole pixels.
41222
+ *
41223
+ * Rounding each edge rather than the origin and size is what makes neighbours meet exactly:
41224
+ * a tile's right edge and the next tile's left edge round to the same pixel, so no seam of
41225
+ * background shows between them.
41226
+ */
41227
+ tileRect(tile, target, size) {
41228
+ const spanLon = target.East - target.West;
41229
+ const spanLat = target.North - target.South;
41230
+ const x0 = Math.round((tile.West - target.West) / spanLon * size.width);
41231
+ const x1 = Math.round((tile.East - target.West) / spanLon * size.width);
41232
+ const y0 = Math.round((target.North - tile.North) / spanLat * size.height);
41233
+ const y1 = Math.round((target.North - tile.South) / spanLat * size.height);
41234
+ return { x: x0, y: y0, w: Math.max(1, x1 - x0), h: Math.max(1, y1 - y0) };
41235
+ }
41236
+ /*
41237
+ * Decodes many tile images at once.
41238
+ *
41239
+ * Serially awaiting each decode makes a frame cost as many round trips through the image
41240
+ * decoder as there are tiles, which for an 82 tile pyramid is seconds rather than one.
41241
+ */
41242
+ decodeSlices(block, base, entries) {
41243
+ return Promise.all(entries.map(async (at) => {
41244
+ if (!at) {
41245
+ return null;
41246
+ }
41247
+ const slice = block.slice(at.ByteOffset - base, at.ByteOffset - base + at.ByteLength);
41248
+ const objectUrl = URL.createObjectURL(new Blob([slice], { type: "image/png" }));
41249
+ try {
41250
+ return await loadImage(objectUrl);
41251
+ }
41252
+ catch {
41253
+ return null;
41254
+ }
41255
+ finally {
41256
+ URL.revokeObjectURL(objectUrl);
41257
+ }
41258
+ }));
41259
+ }
41260
+ /*
41261
+ * Range GETs one entry and decodes it to an image.
41262
+ */
41263
+ async loadImageAt(entry) {
41264
+ const start = entry.ByteOffset;
41265
+ const end = entry.ByteOffset + entry.ByteLength - 1;
41266
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${start}-${end}` } });
41267
+ const buffer = await response.arrayBuffer();
41268
+ const objectUrl = URL.createObjectURL(new Blob([buffer], { type: "image/png" }));
41269
+ try {
41270
+ return await loadImage(objectUrl);
41271
+ }
41272
+ finally {
41273
+ URL.revokeObjectURL(objectUrl);
41274
+ }
41275
+ }
41276
+ /*
41277
+ * Puts a static raster through the same resample the frames get.
41278
+ */
41279
+ remap(image) {
41280
+ if (!image) {
41281
+ return null;
41282
+ }
41283
+ const rect = this.sourceRect({ width: image.width, height: image.height });
41284
+ if (!rect) {
41285
+ return image;
41286
+ }
41287
+ const from = document.createElement("canvas");
41288
+ from.width = image.width;
41289
+ from.height = image.height;
41290
+ from.getContext("2d").putImageData(image, 0, 0);
41291
+ const to = document.createElement("canvas");
41292
+ to.width = image.width;
41293
+ to.height = image.height;
41294
+ const ctx = to.getContext("2d");
41295
+ ctx.imageSmoothingEnabled = false;
41296
+ ctx.clearRect(0, 0, image.width, image.height);
41297
+ ctx.drawImage(from, rect.x, rect.y, rect.width, rect.height, 0, 0, image.width, image.height);
41298
+ return ctx.getImageData(0, 0, image.width, image.height);
41299
+ }
41300
+ /*
41301
+ * Range GETs one entry out of the blob and decodes it to pixels.
41302
+ */
41303
+ async loadImageDataAt(entry) {
41304
+ const start = entry.ByteOffset;
41305
+ const end = entry.ByteOffset + entry.ByteLength - 1;
41306
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${start}-${end}` } });
41307
+ const buffer = await response.arrayBuffer();
41308
+ const blob = new Blob([buffer], { type: "image/png" });
41309
+ const objectUrl = URL.createObjectURL(blob);
41310
+ let image;
41311
+ try {
41312
+ image = await loadImage(objectUrl);
41313
+ }
41314
+ finally {
41315
+ URL.revokeObjectURL(objectUrl);
41316
+ }
41317
+ const canvas = document.createElement("canvas");
41318
+ canvas.width = image.width;
41319
+ canvas.height = image.height;
41320
+ const ctx = canvas.getContext("2d");
41321
+ ctx.drawImage(image, 0, 0);
41322
+ return ctx.getImageData(0, 0, canvas.width, canvas.height);
41323
+ }
40330
41324
  /**
40331
41325
  * Fetches and decodes the archive's static baseline mask, at most once.
40332
41326
  */
40333
41327
  ensureBaselineMask() {
41328
+ if (this.maskBaseline && this.tiles) {
41329
+ if (!this.baselineLoad) {
41330
+ this.baselineLoad = this.loadTiledBaselineMask();
41331
+ }
41332
+ return this.baselineLoad;
41333
+ }
40334
41334
  if (!this.maskBaseline || !this.baselineMaskEntry) {
40335
41335
  return Promise.resolve();
40336
41336
  }
@@ -40443,24 +41443,17 @@ var TextureFrameSeriesAnimator;
40443
41443
  presentPixels(pixels, dims, valuePixels) {
40444
41444
  this.poolIdx = 1 - this.poolIdx;
40445
41445
  const target = this.pool[this.poolIdx];
40446
- target.width = dims.width;
40447
- target.height = dims.height;
40448
- const ctx = target.getContext("2d");
40449
- const imgData = ctx.createImageData(dims.width, dims.height);
40450
- imgData.data.set(pixels);
40451
- ctx.putImageData(imgData, 0, 0);
41446
+ this.writeInto(target, pixels, dims);
40452
41447
  this.displayedPixels = pixels;
40453
41448
  if (this.valueCanvas && valuePixels) {
40454
- this.valueCanvas.width = dims.width;
40455
- this.valueCanvas.height = dims.height;
40456
- const valueCtx = this.valueCanvas.getContext("2d");
40457
- const valueImgData = valueCtx.createImageData(dims.width, dims.height);
40458
- valueImgData.data.set(valuePixels);
40459
- valueCtx.putImageData(valueImgData, 0, 0);
41449
+ this.writeInto(this.valueCanvas, valuePixels, dims);
40460
41450
  this.displayedValuePixels = valuePixels;
40461
41451
  this.valueDims = dims;
40462
41452
  }
40463
41453
  this.presentVersion++;
41454
+ if (this.viewer && this.viewer.scene && this.viewer.scene.requestRenderMode) {
41455
+ this.viewer.scene.requestRender();
41456
+ }
40464
41457
  }
40465
41458
  }
40466
41459
  TextureFrameSeriesAnimator.Animator = Animator;
@@ -40472,6 +41465,688 @@ var TextureFrameSeriesAnimator;
40472
41465
  }
40473
41466
  })(TextureFrameSeriesAnimator || (TextureFrameSeriesAnimator = {}));
40474
41467
 
41468
+ /**
41469
+ * Prints a value texture's own numbers over the cells they belong to.
41470
+ */
41471
+ var TextureValueLabels;
41472
+ (function (TextureValueLabels) {
41473
+ // Off by default: a caller can still set a floor, but thinning is the mechanism that keeps a
41474
+ // distant view readable.
41475
+ const DEFAULT_MIN_CELL_PIXELS = 0;
41476
+ const MIN_VISIBLE_PIXELS = 60;
41477
+ const MAX_STRIDE = 512;
41478
+ const STRIDE_HYSTERESIS = 1.4;
41479
+ const DEFAULT_MIN_SPACING_PIXELS = 78;
41480
+ const DEFAULT_MAX_LABELS = 240;
41481
+ const LABEL_RADIUS = 4;
41482
+ const LABEL_FONT = "600 11px ui-monospace, SFMono-Regular, Menlo, monospace";
41483
+ // Clear space either side of a label box, so neighbours read as separate.
41484
+ const LABEL_GAP_PIXELS = 16;
41485
+ const LABEL_ANCHOR_COLOR = "rgba(255,255,255,0.85)";
41486
+ // 8 rather than 5: below this a cell reads as texture rather than as a boundary, and it also
41487
+ // bounds the worst case, since a full screen of 8 px cells fits inside MAX_GRID_CELLS.
41488
+ const DEFAULT_MIN_GRID_CELL_PIXELS = 8;
41489
+ const DEFAULT_GRID_WIDTH_PIXELS = 1;
41490
+ // Enough for a full screen at the minimum legible cell size, with headroom for an oblique view
41491
+ // whose texel window is larger than the screen. The budget is shared across tiles, so sizing it
41492
+ // too tightly made the grid depend on which tile happened to be walked first.
41493
+ const MAX_GRID_CELLS = 40000;
41494
+ // Probes per side when hunting a labellable cell inside one lattice block.
41495
+ const MAX_LABEL_PROBES_PER_SIDE = 8;
41496
+ const TILE_OUTLINE_ALPHA = 0.45;
41497
+ // Cells per grid square at the coarsest. Beyond this a tile is outlined instead.
41498
+ const MAX_GRID_STRIDE = 256;
41499
+ const LINE_HEIGHT = 13;
41500
+ /**
41501
+ * A clip outline from a projected ring, or null when the ring cannot be drawn.
41502
+ * @param projected one entry per ring vertex, null where the vertex has no window coordinate
41503
+ */
41504
+ function ClipOutline(projected) {
41505
+ if (projected.length < 3) {
41506
+ return null;
41507
+ }
41508
+ const out = [];
41509
+ for (const at of projected) {
41510
+ if (!at) {
41511
+ return null;
41512
+ }
41513
+ out.push(at);
41514
+ }
41515
+ return out;
41516
+ }
41517
+ TextureValueLabels.ClipOutline = ClipOutline;
41518
+ /**
41519
+ * How many cells apart labels should sit, as a power of two.
41520
+ * @param options cellPixels is one cell's size on screen, held is the stride already in use
41521
+ */
41522
+ function ChooseLabelStride(options) {
41523
+ const { cellPixels, visibleCols, visibleRows, maxLabels, minSpacingPixels } = options;
41524
+ const fits = (s, slack) => cellPixels * s >= minSpacingPixels / slack
41525
+ && Math.ceil(visibleCols / s) * Math.ceil(visibleRows / s) <= maxLabels * slack;
41526
+ let stride = 1;
41527
+ while (stride < MAX_STRIDE && !fits(stride, 1)) {
41528
+ stride *= 2;
41529
+ }
41530
+ const held = options.held;
41531
+ if (held && fits(held, STRIDE_HYSTERESIS)) {
41532
+ if (held === stride / 2 && cellPixels * held >= minSpacingPixels / STRIDE_HYSTERESIS) {
41533
+ return held;
41534
+ }
41535
+ if (held === stride * 2 && cellPixels * stride < minSpacingPixels * STRIDE_HYSTERESIS) {
41536
+ return held;
41537
+ }
41538
+ }
41539
+ return stride;
41540
+ }
41541
+ TextureValueLabels.ChooseLabelStride = ChooseLabelStride;
41542
+ class Labels {
41543
+ constructor(options) {
41544
+ this.source = null;
41545
+ this.painted = null;
41546
+ this.floor = null;
41547
+ this.ceiling = null;
41548
+ this.disposed = false;
41549
+ this.drawn = 0;
41550
+ this.stride = 0;
41551
+ this.viewer = options.viewer;
41552
+ this.extent = options.extent;
41553
+ this.settings = options.settings || {};
41554
+ this.metadata = options.metadata;
41555
+ this.tiles = options.tiles;
41556
+ this.clipRing = options.clipRing;
41557
+ this.canvas = document.createElement("canvas");
41558
+ this.canvas.style.position = "absolute";
41559
+ this.canvas.style.inset = "0";
41560
+ this.canvas.style.pointerEvents = "none";
41561
+ const parent = this.viewer.canvas.parentElement;
41562
+ if (parent) {
41563
+ parent.appendChild(this.canvas);
41564
+ }
41565
+ this.ctx = this.canvas.getContext("2d");
41566
+ }
41567
+ SetExtent(extent) {
41568
+ this.extent = extent;
41569
+ }
41570
+ /**
41571
+ * The current frame's decoded values, as the animator's value canvas already provides them.
41572
+ * @param source value pixels, one texel per cell
41573
+ */
41574
+ SetSource(source) {
41575
+ this.source = source;
41576
+ }
41577
+ /**
41578
+ * The tinted pixels of the same frame, so the grid can skip cells the ramp painted nothing in.
41579
+ * @param painted RGBA of the presented frame, or null to fall back to coverage alone
41580
+ */
41581
+ SetPainted(painted) {
41582
+ this.painted = painted;
41583
+ }
41584
+ /**
41585
+ * The archive's per-cell extremes, when it publishes them. Without these a label can still
41586
+ * print the current reading, just not the range behind it.
41587
+ */
41588
+ SetExtremes(floor, ceiling) {
41589
+ this.floor = floor;
41590
+ this.ceiling = ceiling;
41591
+ }
41592
+ GetDrawnCount() {
41593
+ return this.drawn;
41594
+ }
41595
+ Clear() {
41596
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
41597
+ this.drawn = 0;
41598
+ }
41599
+ Dispose() {
41600
+ this.disposed = true;
41601
+ if (this.canvas.parentElement) {
41602
+ this.canvas.parentElement.removeChild(this.canvas);
41603
+ }
41604
+ }
41605
+ /**
41606
+ * Redraws every visible label. Cheap enough to call per rendered frame, because the cell
41607
+ * count it considers is bounded by the view rather than by the texture.
41608
+ */
41609
+ Render() {
41610
+ var _a, _b, _c;
41611
+ if (this.disposed || !this.source) {
41612
+ return;
41613
+ }
41614
+ const view = this.viewer.canvas;
41615
+ if (this.canvas.width !== view.clientWidth || this.canvas.height !== view.clientHeight) {
41616
+ this.canvas.width = view.clientWidth;
41617
+ this.canvas.height = view.clientHeight;
41618
+ }
41619
+ this.Clear();
41620
+ const cols = this.source.width;
41621
+ const rows = this.source.height;
41622
+ const window = this.visibleTexels(cols, rows);
41623
+ if (!window) {
41624
+ return;
41625
+ }
41626
+ const midCol = (window.col0 + window.col1) / 2;
41627
+ const midRow = (window.row0 + window.row1) / 2;
41628
+ const a = this.project(this.lonAt(midCol, cols), this.latAt(midRow, rows));
41629
+ const b = this.project(this.lonAt(midCol + 1, cols), this.latAt(midRow + 1, rows));
41630
+ if (!a || !b) {
41631
+ return;
41632
+ }
41633
+ const cellPixels = Math.min(Math.abs(b.x - a.x), Math.abs(b.y - a.y));
41634
+ // Ahead of the label threshold on purpose: a grid still reads at a cell size where a
41635
+ // number no longer fits.
41636
+ this.drawGrid(window, cols, rows, cellPixels);
41637
+ if (cellPixels < ((_a = this.settings.minCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_CELL_PIXELS)) {
41638
+ return;
41639
+ }
41640
+ const visibleWidth = cellPixels * (window.col1 - window.col0 + 1);
41641
+ const visibleHeight = cellPixels * (window.row1 - window.row0 + 1);
41642
+ if (Math.max(visibleWidth, visibleHeight) < MIN_VISIBLE_PIXELS) {
41643
+ return;
41644
+ }
41645
+ const parts = this.settings.parts || ["value", "floor", "ceiling"];
41646
+ this.ctx.font = LABEL_FONT;
41647
+ this.ctx.textAlign = "center";
41648
+ const maxLabels = (_b = this.settings.maxLabels) !== null && _b !== void 0 ? _b : DEFAULT_MAX_LABELS;
41649
+ this.stride = ChooseLabelStride({
41650
+ cellPixels,
41651
+ visibleCols: window.col1 - window.col0 + 1,
41652
+ visibleRows: window.row1 - window.row0 + 1,
41653
+ maxLabels,
41654
+ // Measured, not assumed: the spacing that keeps labels apart is whatever the widest
41655
+ // one is, so changing what a label says cannot quietly start overlapping them.
41656
+ minSpacingPixels: Math.max((_c = this.settings.minSpacingPixels) !== null && _c !== void 0 ? _c : DEFAULT_MIN_SPACING_PIXELS, this.widestLabel(parts)),
41657
+ held: this.stride
41658
+ });
41659
+ const stride = this.stride;
41660
+ const firstRow = Math.floor(window.row0 / stride) * stride;
41661
+ const firstCol = Math.floor(window.col0 / stride) * stride;
41662
+ for (let row = firstRow; row <= window.row1 && this.drawn < maxLabels; row += stride) {
41663
+ for (let col = firstCol; col <= window.col1 && this.drawn < maxLabels; col += stride) {
41664
+ const found = this.readNear(col, row, cols, rows, stride);
41665
+ if (!found) {
41666
+ continue;
41667
+ }
41668
+ const point = this.project(this.lonAt(found.col, cols), this.latAt(found.row, rows));
41669
+ if (!point || point.x < 0 || point.y < 0
41670
+ || point.x > this.canvas.width || point.y > this.canvas.height) {
41671
+ continue;
41672
+ }
41673
+ this.drawLabel(point, found.reading, parts, this.cellOutline(found.col, found.row, cols, rows));
41674
+ }
41675
+ }
41676
+ }
41677
+ /*
41678
+ * Outlines each covered cell, from a projected lattice of its corners.
41679
+ */
41680
+ drawGrid(window, cols, rows, cellPixels) {
41681
+ var _a, _b;
41682
+ const color = this.settings.gridColor;
41683
+ if (!color) {
41684
+ return;
41685
+ }
41686
+ if (this.tiles && this.tiles.length) {
41687
+ this.ctx.save();
41688
+ this.applyRingClip();
41689
+ this.drawTileGrid(color);
41690
+ this.ctx.restore();
41691
+ return;
41692
+ }
41693
+ if (cellPixels < ((_a = this.settings.minGridCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_GRID_CELL_PIXELS)) {
41694
+ return;
41695
+ }
41696
+ // Corners are shared by four cells, so projecting the lattice once rather than per cell
41697
+ // cuts the work by about four and keeps neighbouring outlines exactly coincident.
41698
+ const wide = window.col1 - window.col0 + 1;
41699
+ const high = window.row1 - window.row0 + 1;
41700
+ if (wide * high > MAX_GRID_CELLS) {
41701
+ return;
41702
+ }
41703
+ const lattice = new Array((wide + 1) * (high + 1));
41704
+ for (let r = 0; r <= high; r++) {
41705
+ for (let c = 0; c <= wide; c++) {
41706
+ // Corners sit half a texel out from the centres lonAt/latAt return.
41707
+ const lon = this.lonAt(window.col0 + c - 0.5, cols);
41708
+ const lat = this.latAt(window.row0 + r - 0.5, rows);
41709
+ lattice[r * (wide + 1) + c] = this.project(lon, lat);
41710
+ }
41711
+ }
41712
+ const path = new Path2D();
41713
+ let drew = false;
41714
+ for (let r = 0; r < high; r++) {
41715
+ for (let c = 0; c < wide; c++) {
41716
+ if (!this.isCovered(window.col0 + c, window.row0 + r, cols)) {
41717
+ continue;
41718
+ }
41719
+ const tl = lattice[r * (wide + 1) + c];
41720
+ const tr = lattice[r * (wide + 1) + c + 1];
41721
+ const bl = lattice[(r + 1) * (wide + 1) + c];
41722
+ const br = lattice[(r + 1) * (wide + 1) + c + 1];
41723
+ if (!tl || !tr || !bl || !br) {
41724
+ continue;
41725
+ }
41726
+ path.moveTo(tl.x, tl.y);
41727
+ path.lineTo(tr.x, tr.y);
41728
+ path.lineTo(br.x, br.y);
41729
+ path.lineTo(bl.x, bl.y);
41730
+ path.closePath();
41731
+ drew = true;
41732
+ }
41733
+ }
41734
+ if (!drew) {
41735
+ return;
41736
+ }
41737
+ this.ctx.strokeStyle = color;
41738
+ this.ctx.lineWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
41739
+ this.ctx.stroke(path);
41740
+ +" alpha" + this.ctx.globalAlpha;
41741
+ }
41742
+ /*
41743
+ * Restricts drawing to the polygon, so nothing is ruled over ground it does not cover.
41744
+ */
41745
+ applyRingClip() {
41746
+ const ring = this.clipRing;
41747
+ if (!ring || ring.length < 3) {
41748
+ return;
41749
+ }
41750
+ const outline = ClipOutline(ring.map(point => this.project(point.lon, point.lat)));
41751
+ if (!outline) {
41752
+ return;
41753
+ }
41754
+ const path = new Path2D();
41755
+ path.moveTo(outline[0].x, outline[0].y);
41756
+ for (let i = 1; i < outline.length; i++) {
41757
+ path.lineTo(outline[i].x, outline[i].y);
41758
+ }
41759
+ path.closePath();
41760
+ this.ctx.clip(path);
41761
+ }
41762
+ /*
41763
+ * The cell of whichever tile covers a point, as screen corners.
41764
+ */
41765
+ tileCellOutline(lon, lat) {
41766
+ for (const tile of this.tiles) {
41767
+ if (lon < tile.West || lon > tile.East || lat < tile.South || lat > tile.North) {
41768
+ continue;
41769
+ }
41770
+ const c = (Math.min(tile.ResolutionX - 1, Math.max(0, Math.floor((lon - tile.West) / (tile.East - tile.West) * tile.ResolutionX))));
41771
+ const r = (Math.min(tile.ResolutionY - 1, Math.max(0, Math.floor((tile.North - lat) / (tile.North - tile.South) * tile.ResolutionY))));
41772
+ const w0 = tile.West + (c / tile.ResolutionX) * (tile.East - tile.West);
41773
+ const w1 = tile.West + ((c + 1) / tile.ResolutionX) * (tile.East - tile.West);
41774
+ const n0 = tile.North - (r / tile.ResolutionY) * (tile.North - tile.South);
41775
+ const n1 = tile.North - ((r + 1) / tile.ResolutionY) * (tile.North - tile.South);
41776
+ const corners = [
41777
+ this.project(w0, n0), this.project(w1, n0),
41778
+ this.project(w1, n1), this.project(w0, n1)
41779
+ ];
41780
+ return corners.every((q) => q) ? corners : null;
41781
+ }
41782
+ return null;
41783
+ }
41784
+ /*
41785
+ * Outlines each tile's own cells, so cell size still shows where the mesh is refined.
41786
+ */
41787
+ drawTileGrid(color) {
41788
+ var _a, _b, _c;
41789
+ const view = this.viewer.camera.computeViewRectangle(Ellipsoid.WGS84);
41790
+ if (!view) {
41791
+ return;
41792
+ }
41793
+ const west = Math$1.toDegrees(view.west);
41794
+ const east = Math$1.toDegrees(view.east);
41795
+ const south = Math$1.toDegrees(view.south);
41796
+ const north = Math$1.toDegrees(view.north);
41797
+ const minCell = (_a = this.settings.minGridCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_GRID_CELL_PIXELS;
41798
+ const focus = this.groundAtScreenCentre();
41799
+ const path = new Path2D();
41800
+ // A tile too fine to rule, or one the budget cannot afford, contributes its own outline
41801
+ // instead of vanishing. Dropping such tiles entirely is what made whole regions blink out.
41802
+ const coarse = new Path2D();
41803
+ let drew = false;
41804
+ let drewCoarse = false;
41805
+ const outlineTile = (tile) => {
41806
+ const tl = this.project(tile.West, tile.North);
41807
+ const tr = this.project(tile.East, tile.North);
41808
+ const br = this.project(tile.East, tile.South);
41809
+ const bl = this.project(tile.West, tile.South);
41810
+ if (!tl || !tr || !br || !bl) {
41811
+ return;
41812
+ }
41813
+ coarse.moveTo(tl.x, tl.y);
41814
+ coarse.lineTo(tr.x, tr.y);
41815
+ coarse.lineTo(br.x, br.y);
41816
+ coarse.lineTo(bl.x, bl.y);
41817
+ coarse.closePath();
41818
+ drewCoarse = true;
41819
+ };
41820
+ // Planned for every visible tile BEFORE any is drawn.
41821
+ const planned = [];
41822
+ for (const tile of this.tiles) {
41823
+ if (tile.East < west || tile.West > east || tile.North < south || tile.South > north) {
41824
+ continue;
41825
+ }
41826
+ const c0 = Math.max(0, Math.floor((Math.max(west, tile.West) - tile.West)
41827
+ / (tile.East - tile.West) * tile.ResolutionX));
41828
+ const c1 = Math.min(tile.ResolutionX, Math.ceil((Math.min(east, tile.East) - tile.West)
41829
+ / (tile.East - tile.West) * tile.ResolutionX));
41830
+ const r0 = Math.max(0, Math.floor((tile.North - Math.min(north, tile.North))
41831
+ / (tile.North - tile.South) * tile.ResolutionY));
41832
+ const r1 = Math.min(tile.ResolutionY, Math.ceil((tile.North - Math.max(south, tile.South))
41833
+ / (tile.North - tile.South) * tile.ResolutionY));
41834
+ if (c1 <= c0 || r1 <= r0) {
41835
+ continue;
41836
+ }
41837
+ const focusC = focus
41838
+ ? Math.round((focus.lon - tile.West) / (tile.East - tile.West) * tile.ResolutionX)
41839
+ : Math.floor((c0 + c1) / 2);
41840
+ const focusR = focus
41841
+ ? Math.round((tile.North - focus.lat) / (tile.North - tile.South) * tile.ResolutionY)
41842
+ : Math.floor((r0 + r1) / 2);
41843
+ const midC = Math.min(c1 - 1, Math.max(c0, focusC));
41844
+ const midR = Math.min(r1 - 1, Math.max(r0, focusR));
41845
+ const cellLon = (tile.East - tile.West) / tile.ResolutionX;
41846
+ const cellLat = (tile.North - tile.South) / tile.ResolutionY;
41847
+ const at = this.project(tile.West + midC * cellLon, tile.North - midR * cellLat);
41848
+ const next = (this.project(tile.West + (midC + 1) * cellLon, tile.North - (midR + 1) * cellLat));
41849
+ if (!at || !next) {
41850
+ outlineTile(tile);
41851
+ continue;
41852
+ }
41853
+ const onScreen = Math.min(Math.abs(next.x - at.x), Math.abs(next.y - at.y));
41854
+ let step = 1;
41855
+ while (step < MAX_GRID_STRIDE && onScreen * step < minCell) {
41856
+ step *= 2;
41857
+ }
41858
+ if (onScreen * step < minCell) {
41859
+ outlineTile(tile);
41860
+ continue;
41861
+ }
41862
+ planned.push({ tile, c0, c1, r0, r1, step });
41863
+ }
41864
+ // Coarsened together rather than dropped one by one, so a busy view thins out evenly.
41865
+ const blocksOf = (p) => Math.ceil((p.c1 - p.c0) / p.step) * Math.ceil((p.r1 - p.r0) / p.step);
41866
+ let total = planned.reduce((sum, p) => sum + blocksOf(p), 0);
41867
+ while (total > MAX_GRID_CELLS && planned.some((p) => p.step < MAX_GRID_STRIDE)) {
41868
+ for (const p of planned) {
41869
+ p.step = Math.min(MAX_GRID_STRIDE, p.step * 2);
41870
+ }
41871
+ total = planned.reduce((sum, p) => sum + blocksOf(p), 0);
41872
+ }
41873
+ for (const { tile, c0, c1, r0, r1, step } of planned) {
41874
+ // Anchored to the tile's own origin so lines stay on the same boundaries as the camera
41875
+ // moves, and so each step up is a subset of the one below it.
41876
+ const firstR = Math.floor(r0 / step) * step;
41877
+ const firstC = Math.floor(c0 / step) * step;
41878
+ for (let r = firstR; r < r1; r += step) {
41879
+ const n0 = tile.North - (r / tile.ResolutionY) * (tile.North - tile.South);
41880
+ const n1 = tile.North
41881
+ - (Math.min(r + step, tile.ResolutionY) / tile.ResolutionY) * (tile.North - tile.South);
41882
+ for (let c = firstC; c < c1; c += step) {
41883
+ const w0 = tile.West + (c / tile.ResolutionX) * (tile.East - tile.West);
41884
+ const w1 = tile.West
41885
+ + (Math.min(c + step, tile.ResolutionX) / tile.ResolutionX) * (tile.East - tile.West);
41886
+ if (!this.coveredAt((w0 + w1) / 2, (n0 + n1) / 2)) {
41887
+ continue;
41888
+ }
41889
+ const tl = this.project(w0, n0);
41890
+ const tr = this.project(w1, n0);
41891
+ const br = this.project(w1, n1);
41892
+ const bl = this.project(w0, n1);
41893
+ if (!tl || !tr || !br || !bl) {
41894
+ continue;
41895
+ }
41896
+ path.moveTo(tl.x, tl.y);
41897
+ path.lineTo(tr.x, tr.y);
41898
+ path.lineTo(br.x, br.y);
41899
+ path.lineTo(bl.x, bl.y);
41900
+ path.closePath();
41901
+ drew = true;
41902
+ }
41903
+ }
41904
+ }
41905
+ const gridWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
41906
+ if (drewCoarse) {
41907
+ this.ctx.save();
41908
+ // Dimmer than a cell edge, so a tile boundary reads as "there is finer data here"
41909
+ // rather than as a cell in its own right.
41910
+ this.ctx.globalAlpha = TILE_OUTLINE_ALPHA;
41911
+ this.ctx.strokeStyle = color;
41912
+ this.ctx.lineWidth = gridWidth;
41913
+ this.ctx.stroke(coarse);
41914
+ this.ctx.restore();
41915
+ }
41916
+ if (!drew) {
41917
+ return;
41918
+ }
41919
+ this.ctx.strokeStyle = color;
41920
+ this.ctx.lineWidth = (_c = this.settings.gridWidthPixels) !== null && _c !== void 0 ? _c : DEFAULT_GRID_WIDTH_PIXELS;
41921
+ this.ctx.stroke(path);
41922
+ }
41923
+ /*
41924
+ * Lon/lat of the ground at the centre of the view, or null when the centre misses the globe.
41925
+ */
41926
+ groundAtScreenCentre() {
41927
+ const scene = this.viewer.scene;
41928
+ const centre = new Cartesian2(this.viewer.canvas.clientWidth / 2, this.viewer.canvas.clientHeight / 2);
41929
+ const hit = scene.camera.pickEllipsoid(centre, Ellipsoid.WGS84);
41930
+ if (!hit) {
41931
+ return null;
41932
+ }
41933
+ const carto = Cartographic.fromCartesian(hit);
41934
+ return {
41935
+ lon: Math$1.toDegrees(carto.longitude),
41936
+ lat: Math$1.toDegrees(carto.latitude)
41937
+ };
41938
+ }
41939
+ /*
41940
+ * Whether the composited raster holds data at a lon/lat.
41941
+ */
41942
+ coveredAt(lon, lat) {
41943
+ if (!this.source) {
41944
+ return false;
41945
+ }
41946
+ const cols = this.source.width;
41947
+ const rows = this.source.height;
41948
+ const col = Math.floor((lon - this.extent.West) / (this.extent.East - this.extent.West) * cols);
41949
+ const row = Math.floor((this.extent.North - lat) / (this.extent.North - this.extent.South) * rows);
41950
+ if (col < 0 || row < 0 || col >= cols || row >= rows) {
41951
+ return false;
41952
+ }
41953
+ const at = (row * cols + col) * 4 + 3;
41954
+ // Painted, not merely covered: a dry cell holds a reading the ramp deliberately hides, and
41955
+ // ruling a grid over it draws detail where the picture shows none.
41956
+ if (this.painted && this.painted.length === cols * rows * 4) {
41957
+ return this.painted[at] > 0;
41958
+ }
41959
+ return this.source.data[at] >= 128;
41960
+ }
41961
+ isCovered(col, row, cols) {
41962
+ return Boolean(this.source && this.source.data[(row * cols + col) * 4 + 3] >= 128);
41963
+ }
41964
+ /*
41965
+ * Width of the widest label this overlay could draw, in pixels.
41966
+ */
41967
+ widestLabel(parts) {
41968
+ const sample = [];
41969
+ if (parts.indexOf("value") >= 0) {
41970
+ sample.push("-000.00");
41971
+ }
41972
+ const range = [];
41973
+ if (parts.indexOf("floor") >= 0) {
41974
+ range.push("min: -000.00");
41975
+ }
41976
+ if (parts.indexOf("ceiling") >= 0) {
41977
+ range.push("max: -000.00");
41978
+ }
41979
+ if (range.length) {
41980
+ sample.push(range.join(" - "));
41981
+ }
41982
+ if (!sample.length) {
41983
+ return 0;
41984
+ }
41985
+ const widest = Math.max(...sample.map((line) => this.ctx.measureText(line).width));
41986
+ return widest + LABEL_GAP_PIXELS;
41987
+ }
41988
+ /*
41989
+ * The four screen corners of one cell, or null if any of them will not project.
41990
+ */
41991
+ cellOutline(col, row, cols, rows) {
41992
+ // On a pyramid the composited texel is not a cell. Outlining it would draw a second,
41993
+ // uniform lattice over the tile grid, misaligned with it everywhere.
41994
+ if (this.tiles && this.tiles.length) {
41995
+ return this.tileCellOutline(this.lonAt(col, cols), this.latAt(row, rows));
41996
+ }
41997
+ const corners = [
41998
+ this.project(this.lonAt(col - 0.5, cols), this.latAt(row - 0.5, rows)),
41999
+ this.project(this.lonAt(col + 0.5, cols), this.latAt(row - 0.5, rows)),
42000
+ this.project(this.lonAt(col + 0.5, cols), this.latAt(row + 0.5, rows)),
42001
+ this.project(this.lonAt(col - 0.5, cols), this.latAt(row + 0.5, rows))
42002
+ ];
42003
+ return corners.every((c) => c) ? corners : null;
42004
+ }
42005
+ drawLabel(point, reading, parts, outline) {
42006
+ var _a;
42007
+ const dp = (_a = this.settings.decimals) !== null && _a !== void 0 ? _a : (Math.abs(reading.value) < 10 ? 2 : 1);
42008
+ const lines = [];
42009
+ if (parts.indexOf("value") >= 0) {
42010
+ lines.push(reading.value.toFixed(dp));
42011
+ }
42012
+ // Named rather than a bare pair: two numbers under a third say nothing about which is
42013
+ // the series low and which is its high.
42014
+ const range = [];
42015
+ if (parts.indexOf("floor") >= 0 && reading.floor !== undefined) {
42016
+ range.push(`min: ${reading.floor.toFixed(dp)}`);
42017
+ }
42018
+ if (parts.indexOf("ceiling") >= 0 && reading.ceiling !== undefined) {
42019
+ range.push(`max: ${reading.ceiling.toFixed(dp)}`);
42020
+ }
42021
+ if (range.length) {
42022
+ lines.push(range.join(" - "));
42023
+ }
42024
+ if (!lines.length) {
42025
+ return;
42026
+ }
42027
+ if (outline) {
42028
+ this.ctx.strokeStyle = LABEL_ANCHOR_COLOR;
42029
+ this.ctx.lineWidth = 1.5;
42030
+ this.ctx.beginPath();
42031
+ this.ctx.moveTo(outline[0].x, outline[0].y);
42032
+ for (let i = 1; i < outline.length; i++) {
42033
+ this.ctx.lineTo(outline[i].x, outline[i].y);
42034
+ }
42035
+ this.ctx.closePath();
42036
+ this.ctx.stroke();
42037
+ }
42038
+ const width = Math.max(...lines.map((l) => this.ctx.measureText(l).width)) + 10;
42039
+ const height = lines.length * LINE_HEIGHT + 6;
42040
+ const top = point.y - height / 2;
42041
+ this.ctx.fillStyle = "rgba(16,16,14,0.72)";
42042
+ this.ctx.beginPath();
42043
+ this.ctx.roundRect(point.x - width / 2, top, width, height, LABEL_RADIUS);
42044
+ this.ctx.fill();
42045
+ lines.forEach((line, i) => {
42046
+ this.ctx.fillStyle = i === 0 ? "#ffffff" : "#a8a89f";
42047
+ this.ctx.fillText(line, point.x, top + LINE_HEIGHT * (i + 1) - 1);
42048
+ });
42049
+ this.drawn++;
42050
+ }
42051
+ /*
42052
+ * The first cell with a reading at or near a lattice point, searched within its own block.
42053
+ */
42054
+ readNear(col, row, cols, rows, stride) {
42055
+ const direct = this.readAt(col, row, cols);
42056
+ if (direct && this.paintedAt(col, row, cols)) {
42057
+ return { col, row, reading: direct };
42058
+ }
42059
+ if (stride <= 1) {
42060
+ return null;
42061
+ }
42062
+ // Bounded, and stepped rather than exhaustive, so a large stride cannot make this the
42063
+ // expensive part of a frame.
42064
+ const step = Math.max(1, Math.floor(stride / MAX_LABEL_PROBES_PER_SIDE));
42065
+ for (let r = row; r < Math.min(rows, row + stride); r += step) {
42066
+ for (let c = col; c < Math.min(cols, col + stride); c += step) {
42067
+ const reading = this.readAt(c, r, cols);
42068
+ if (reading && this.paintedAt(c, r, cols)) {
42069
+ return { col: c, row: r, reading };
42070
+ }
42071
+ }
42072
+ }
42073
+ return null;
42074
+ }
42075
+ paintedAt(col, row, cols) {
42076
+ if (!this.painted) {
42077
+ return true;
42078
+ }
42079
+ const at = (row * cols + col) * 4 + 3;
42080
+ return this.painted[at] > 0;
42081
+ }
42082
+ // Decodes one cell, skipping anything the texture marks as having no data.
42083
+ readAt(col, row, cols) {
42084
+ var _a, _b;
42085
+ const i = (row * cols + col) * 4;
42086
+ if (!this.source || this.source.data[i + 3] < 128) {
42087
+ return null;
42088
+ }
42089
+ const lo = this.metadata && typeof this.metadata.ValueMin === "number" ? this.metadata.ValueMin : 0;
42090
+ const hi = this.metadata && typeof this.metadata.ValueMax === "number" ? this.metadata.ValueMax : 1;
42091
+ const value = lo + (this.source.data[i] / 255) * (hi - lo);
42092
+ const reading = { value };
42093
+ const eLo = (_a = this.metadata) === null || _a === void 0 ? void 0 : _a.ExtremesValueMin;
42094
+ const eHi = (_b = this.metadata) === null || _b === void 0 ? void 0 : _b.ExtremesValueMax;
42095
+ if (this.floor && typeof eLo === "number" && typeof eHi === "number") {
42096
+ reading.floor = eLo + decode16(this.floor.data, i) * (eHi - eLo);
42097
+ // An anomaly archive's frames are already floor-relative, so the reading a user
42098
+ // wants is the absolute one. This is the whole reason the floor is published.
42099
+ if (TextureFrameSeriesAnimator.IsAnomalyMeasure(this.metadata)) {
42100
+ reading.value += reading.floor;
42101
+ }
42102
+ }
42103
+ if (this.ceiling && typeof eLo === "number" && typeof eHi === "number") {
42104
+ reading.ceiling = eLo + decode16(this.ceiling.data, i) * (eHi - eLo);
42105
+ }
42106
+ return reading;
42107
+ }
42108
+ lonAt(col, cols) {
42109
+ return this.extent.West + ((col + 0.5) / cols) * (this.extent.East - this.extent.West);
42110
+ }
42111
+ latAt(row, rows) {
42112
+ return this.extent.North - ((row + 0.5) / rows) * (this.extent.North - this.extent.South);
42113
+ }
42114
+ // The texel range overlapping the camera's view, so an off-screen texture costs nothing.
42115
+ visibleTexels(cols, rows) {
42116
+ const view = this.viewer.camera.computeViewRectangle(Ellipsoid.WGS84);
42117
+ if (!view) {
42118
+ return null;
42119
+ }
42120
+ const west = Math.max(this.extent.West, Math$1.toDegrees(view.west));
42121
+ const east = Math.min(this.extent.East, Math$1.toDegrees(view.east));
42122
+ const south = Math.max(this.extent.South, Math$1.toDegrees(view.south));
42123
+ const north = Math.min(this.extent.North, Math$1.toDegrees(view.north));
42124
+ if (west >= east || south >= north) {
42125
+ return null;
42126
+ }
42127
+ const spanLon = this.extent.East - this.extent.West;
42128
+ const spanLat = this.extent.North - this.extent.South;
42129
+ return {
42130
+ col0: Math.max(0, Math.floor((west - this.extent.West) / spanLon * cols) - 1),
42131
+ col1: Math.min(cols - 1, Math.ceil((east - this.extent.West) / spanLon * cols) + 1),
42132
+ row0: Math.max(0, Math.floor((this.extent.North - north) / spanLat * rows) - 1),
42133
+ row1: Math.min(rows - 1, Math.ceil((this.extent.North - south) / spanLat * rows) + 1)
42134
+ };
42135
+ }
42136
+ project(lon, lat) {
42137
+ const world = Cartesian3.fromDegrees(lon, lat, 0);
42138
+ const transforms = SceneTransforms;
42139
+ // Renamed in newer Cesium, and this library runs on whatever the host app supplies.
42140
+ const fn = transforms.worldToWindowCoordinates || transforms.wgs84ToWindowCoordinates;
42141
+ return fn ? fn.call(transforms, this.viewer.scene, world) : null;
42142
+ }
42143
+ }
42144
+ TextureValueLabels.Labels = Labels;
42145
+ function decode16(data, i) {
42146
+ return (data[i] * 256 + data[i + 1]) / 65535;
42147
+ }
42148
+ })(TextureValueLabels || (TextureValueLabels = {}));
42149
+
40475
42150
  /**
40476
42151
  * Renders a value texture as a displaced, coloured surface.
40477
42152
  * The source canvas is expected as RGBA: value in R, coverage in A.
@@ -40494,6 +42169,9 @@ var DisplacedSurfacePrimitive;
40494
42169
  const COVERAGE_CUTOFF = 0.35;
40495
42170
  // With no exaggeration stated, the full value range is drawn as this fraction of the extent's shorter side.
40496
42171
  const AUTO_RELIEF_FRACTION = 0.02;
42172
+ // Ceiling on the derived relief. Sizing purely off the extent is fine for a parcel and absurd for
42173
+ // a whole estuary: 4 m of water over an 82 km extent came out 1.6 km tall.
42174
+ const AUTO_MAX_RELIEF_METRES = 250.0;
40497
42175
  // Terrain is sampled on this grid and interpolated between samples.
40498
42176
  // Per-texel sampling would be hundreds of thousands of lookups for a surface whose own data is far coarser than that.
40499
42177
  const GROUND_SAMPLES_PER_SIDE = 64;
@@ -40572,8 +42250,7 @@ void main() {
40572
42250
  `;
40573
42251
  const FRAGMENT_SHADER_GLSL300 = `
40574
42252
  uniform sampler2D u_valueTexture;
40575
- uniform vec4 u_lowColor;
40576
- uniform vec4 u_highColor;
42253
+ uniform sampler2D u_rampTexture;
40577
42254
  uniform float u_coverageCutoff;
40578
42255
  uniform vec2 u_texelSize;
40579
42256
  uniform float u_valueRange;
@@ -40588,7 +42265,13 @@ void main() {
40588
42265
  if (v_coverage < u_coverageCutoff) {
40589
42266
  discard;
40590
42267
  }
40591
- vec4 color = mix(u_lowColor, u_highColor, v_value);
42268
+ // The ramp is baked to a lookup rather than lerped between two colours, so a banded style with a
42269
+ // hidden floor reads the same here as it does on the flat drape.
42270
+ // Without it this surface painted water the style hides, and the cell grid then correctly refused to outline any of it.
42271
+ vec4 color = texture(u_rampTexture, vec2(clamp(v_value, 0.0, 1.0), 0.5));
42272
+ if (color.a <= 0.0) {
42273
+ discard;
42274
+ }
40592
42275
 
40593
42276
  // Relief shading from the value gradient, so the displacement reads at a distance without
40594
42277
  // recomputing vertex normals every time the texture changes.
@@ -40787,6 +42470,7 @@ void main() {
40787
42470
  this.show = true;
40788
42471
  this.valueSource = null;
40789
42472
  this.lastPresentVersion = -1;
42473
+ this.rampTexture = null;
40790
42474
  this.texture = null;
40791
42475
  this.textureDirty = true;
40792
42476
  this.shaderProgram = null;
@@ -40808,8 +42492,7 @@ void main() {
40808
42492
  this.baseHeight = (_d = options.baseHeight) !== null && _d !== void 0 ? _d : 0;
40809
42493
  this.pixelPlacement = options.pixelPlacement;
40810
42494
  this.tileSkirts = Boolean(options.tileSkirts);
40811
- this.lowColor = toCesiumColor(options.lowColor || DEFAULT_LOW_COLOR);
40812
- this.highColor = toCesiumColor(options.highColor || DEFAULT_HIGH_COLOR);
42495
+ this.rampPixels = RampLookup(options.rampStops || null, options.lowColor || DEFAULT_LOW_COLOR, options.highColor || DEFAULT_HIGH_COLOR);
40813
42496
  this.tiles = buildTiles(hasCoverage);
40814
42497
  }
40815
42498
  GetFollowsGround() {
@@ -40868,6 +42551,7 @@ void main() {
40868
42551
  return;
40869
42552
  }
40870
42553
  this.syncTexture(context);
42554
+ this.syncRampTexture(context);
40871
42555
  this.requestGround();
40872
42556
  this.syncGroundTexture(context);
40873
42557
  // A tile needs one mesh before it has a bounding sphere to measure against.
@@ -40906,6 +42590,10 @@ void main() {
40906
42590
  this.texture.destroy();
40907
42591
  this.texture = null;
40908
42592
  }
42593
+ if (this.rampTexture) {
42594
+ this.rampTexture.destroy();
42595
+ this.rampTexture = null;
42596
+ }
40909
42597
  if (this.groundTexture) {
40910
42598
  this.groundTexture.destroy();
40911
42599
  this.groundTexture = null;
@@ -40999,6 +42687,25 @@ void main() {
40999
42687
  const width = Cartesian3.distance(southWest, southEast);
41000
42688
  return Math.max(1, width / Math.max(1, this.source ? this.source.width : 1));
41001
42689
  }
42690
+ syncRampTexture(context) {
42691
+ if (this.rampTexture) {
42692
+ return;
42693
+ }
42694
+ this.rampTexture = new Texture({
42695
+ context,
42696
+ pixelFormat: PixelFormat.RGBA,
42697
+ source: { width: this.rampPixels.length / 4, height: 1,
42698
+ arrayBufferView: this.rampPixels },
42699
+ flipY: false,
42700
+ sampler: new Sampler({
42701
+ wrapS: TextureWrap.CLAMP_TO_EDGE,
42702
+ wrapT: TextureWrap.CLAMP_TO_EDGE,
42703
+ // Linear across the ramp, so a gradient between stops stays smooth.
42704
+ minificationFilter: TextureMinificationFilter.LINEAR,
42705
+ magnificationFilter: TextureMagnificationFilter.LINEAR
42706
+ })
42707
+ });
42708
+ }
41002
42709
  syncTexture(context) {
41003
42710
  const sizeChanged = this.texture
41004
42711
  && (this.texture.width !== this.source.width || this.texture.height !== this.source.height);
@@ -41129,8 +42836,7 @@ void main() {
41129
42836
  ? Math.max(2, Math.abs(self.valueMax - self.valueMin) * self.exaggeration * SKIRT_FRACTION)
41130
42837
  : 0,
41131
42838
  u_coverageCutoff: () => COVERAGE_CUTOFF,
41132
- u_lowColor: () => self.lowColor,
41133
- u_highColor: () => self.highColor,
42839
+ u_rampTexture: () => self.rampTexture,
41134
42840
  u_texelSize: () => new Cartesian2(1 / self.source.width, 1 / self.source.height)
41135
42841
  }
41136
42842
  });
@@ -41191,12 +42897,10 @@ void main() {
41191
42897
  if (!(shorterSide > 0)) {
41192
42898
  return 1;
41193
42899
  }
41194
- return (shorterSide * AUTO_RELIEF_FRACTION) / range;
42900
+ const relief = Math.min(shorterSide * AUTO_RELIEF_FRACTION, AUTO_MAX_RELIEF_METRES);
42901
+ return relief / range;
41195
42902
  }
41196
42903
  DisplacedSurfacePrimitive.autoExaggeration = autoExaggeration;
41197
- function toCesiumColor(color) {
41198
- return new Color(color.red / 255, color.green / 255, color.blue / 255, color.alpha);
41199
- }
41200
42904
  function now() {
41201
42905
  return typeof performance !== "undefined" ? performance.now() : Date.now();
41202
42906
  }
@@ -41204,11 +42908,12 @@ void main() {
41204
42908
 
41205
42909
  const TEXTURE_FRAME_SERIES_ANIMATOR_KEY = "TextureFrameSeriesAnimator.Animator";
41206
42910
  const EXTRUSION_ANIMATOR_KEY = "TextureFrameSeriesAnimator.ExtrusionAnimator";
42911
+ const TEXTURE_VALUE_LABELS_KEY = "TextureValueLabels.Overlay";
41207
42912
  const DISPLACED_SURFACE_KEY = "DisplacedSurfacePrimitive.Surface";
41208
42913
  var EntityRenderEnginePolygon;
41209
42914
  (function (EntityRenderEnginePolygon) {
41210
42915
  async function Render(params) {
41211
- var _a, _b, _c, _d;
42916
+ var _a, _b, _c, _d, _e;
41212
42917
  const entity = params.entity;
41213
42918
  const style = params.style;
41214
42919
  const pRings = Entity$1.GetValue({
@@ -41259,18 +42964,22 @@ var EntityRenderEnginePolygon;
41259
42964
  console.error(`Polygon.Render: failed to resolve extrusion texture for entity ${((_b = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _b === void 0 ? void 0 : _b.ID) || "<NONE>"}:`, e);
41260
42965
  }
41261
42966
  }
42967
+ const retained = params.offline
42968
+ ? (_c = params.rendered) === null || _c === void 0 ? void 0 : _c[TEXTURE_FRAME_SERIES_ANIMATOR_KEY]
42969
+ : null;
42970
+ const keepsAnimator = Boolean(retained && !retained.IsDisposed());
41262
42971
  const drawsDisplacedSurface = Boolean(extrusionArchive);
41263
42972
  // The displaced surface IS the entity's fill, drawn as its own primitive at the value's height.
41264
42973
  // Leaving the flat polygon filled as well shows a second copy of the parcel on the ground under it.
41265
42974
  const hasFill = (drawsDisplacedSurface ? true
41266
42975
  : fillType === Style.EPolygonFillType.Texture
41267
- ? Boolean(textureDataUri || frameArchive || cFillColor.alpha > 0)
42976
+ ? Boolean(textureDataUri || frameArchive || keepsAnimator || cFillColor.alpha > 0)
41268
42977
  : cFillColor.alpha > 0);
41269
42978
  const fillMaterial = drawsDisplacedSurface
41270
42979
  ? Color.WHITE.withAlpha(0)
41271
42980
  : textureDataUri
41272
42981
  ? new ImageMaterialProperty({ image: textureDataUri, transparent: true })
41273
- : frameArchive
42982
+ : (frameArchive || keepsAnimator)
41274
42983
  ? Color.WHITE.withAlpha(0)
41275
42984
  : cFillColor;
41276
42985
  const lineColorTrace = style.lineColor ? Calculator.TraceGetColor(style.lineColor, entity, params.tags) : { value: null, effective: null };
@@ -41440,7 +43149,7 @@ var EntityRenderEnginePolygon;
41440
43149
  }
41441
43150
  // Must run before any material is baked below: the Animator takes over polygon.material, so a bake
41442
43151
  // has to know whether it is baking over a live animation or over a disposed one's restored material.
41443
- const animator = syncTextureFrameArchive(cEntity, drawsDisplacedSurface ? null : frameArchive, params.viewer, style.textureColorMask, style.maskTextureBaseline);
43152
+ const animator = syncTextureFrameArchive(cEntity, drawsDisplacedSurface ? null : frameArchive, params.viewer, style.textureColorMask, style.maskTextureBaseline, undefined, undefined, wantsLabels(style), params.offline, polygonBbox(posses));
41444
43153
  syncDisplacedSurface({
41445
43154
  cEntity,
41446
43155
  extrusionArchive,
@@ -41448,7 +43157,30 @@ var EntityRenderEnginePolygon;
41448
43157
  style,
41449
43158
  entity,
41450
43159
  heightRef,
41451
- outerRingPosses: posses
43160
+ outerRingPosses: posses,
43161
+ offline: params.offline
43162
+ });
43163
+ const drapeExtent = polygonBbox(posses);
43164
+ const labelArchive = drawsDisplacedSurface ? extrusionArchive : frameArchive;
43165
+ syncTextureValueLabels({
43166
+ cEntity,
43167
+ viewer: params.viewer,
43168
+ settings: overlaySettings(style, entity, params.tags),
43169
+ extent: labelArchive
43170
+ ? (drawsDisplacedSurface ? archiveExtent(labelArchive.metadata, posses) : drapeExtent)
43171
+ : null,
43172
+ animator: drawsDisplacedSurface
43173
+ ? cEntity[EXTRUSION_ANIMATOR_KEY]
43174
+ : animator,
43175
+ metadata: labelArchive ? labelArchive.metadata : undefined,
43176
+ offline: params.offline,
43177
+ clipRing: posses.map((pos) => {
43178
+ const carto = Cartographic.fromCartesian(pos);
43179
+ return {
43180
+ lon: Math$1.toDegrees(carto.longitude),
43181
+ lat: Math$1.toDegrees(carto.latitude)
43182
+ };
43183
+ })
41452
43184
  });
41453
43185
  if (animator) {
41454
43186
  CesiumEntityStyler.SetDefaultTextureImage({
@@ -41509,7 +43241,7 @@ var EntityRenderEnginePolygon;
41509
43241
  }
41510
43242
  }
41511
43243
  let borderPosses = posses.map(x => x.clone ? x.clone() : { ...x });
41512
- let cEntityBorder = (_d = (_c = params.rendered) === null || _c === void 0 ? void 0 : _c._siblingGraphics) === null || _d === void 0 ? void 0 : _d[0];
43244
+ let cEntityBorder = (_e = (_d = params.rendered) === null || _d === void 0 ? void 0 : _d._siblingGraphics) === null || _e === void 0 ? void 0 : _e[0];
41513
43245
  cEntity._siblingGraphics = [];
41514
43246
  if (!cEntityBorder || ((!cEntityBorder.polyline && units == "px") ||
41515
43247
  (!cEntityBorder.corridor && units == "m"))) {
@@ -41760,6 +43492,11 @@ var EntityRenderEnginePolygon;
41760
43492
  * Disposes a cEntity's TextureFrameSeriesAnimator.Animator (if any).
41761
43493
  */
41762
43494
  function DisposeTextureFrameSeriesAnimator(cEntity, viewer) {
43495
+ const labelOverlay = cEntity === null || cEntity === void 0 ? void 0 : cEntity[TEXTURE_VALUE_LABELS_KEY];
43496
+ if (labelOverlay) {
43497
+ labelOverlay.remove();
43498
+ cEntity[TEXTURE_VALUE_LABELS_KEY] = null;
43499
+ }
41763
43500
  const existing = cEntity === null || cEntity === void 0 ? void 0 : cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY];
41764
43501
  if (existing && !existing.IsDisposed()) {
41765
43502
  existing.Dispose();
@@ -41885,10 +43622,13 @@ const _textureCache = new LRUCache(50);
41885
43622
  * @param params
41886
43623
  */
41887
43624
  function syncDisplacedSurface(params) {
41888
- var _a;
41889
43625
  const { cEntity, extrusionArchive, viewer, style, entity, heightRef, outerRingPosses } = params;
41890
43626
  const existingAnimator = cEntity[EXTRUSION_ANIMATOR_KEY];
41891
43627
  const existingSurface = cEntity[DISPLACED_SURFACE_KEY];
43628
+ // Same reason as the fill animator: an unresolvable archive is not a removed one.
43629
+ if (params.offline && existingSurface && !existingSurface.isDestroyed()) {
43630
+ return;
43631
+ }
41892
43632
  // Nothing to draw and nothing left over, which is every polygon that does not use this feature.
41893
43633
  if (!extrusionArchive && !existingAnimator && !existingSurface) {
41894
43634
  return;
@@ -41904,7 +43644,7 @@ function syncDisplacedSurface(params) {
41904
43644
  // to rebuild rather than update in place.
41905
43645
  if (reusable && existingSurface.GetFollowsGround() === placement.followGround) {
41906
43646
  existingSurface.SetBaseHeight(placement.baseHeight);
41907
- existingSurface.SetExaggeration((_a = style.extrusionExaggeration) !== null && _a !== void 0 ? _a : DisplacedSurfacePrimitive.autoExaggeration(existingSurface.GetExtent(), extrusionArchive.metadata.ValueMin, extrusionArchive.metadata.ValueMax));
43647
+ existingSurface.SetExaggeration(exaggerationFor(style, extrusionArchive.metadata, existingSurface.GetExtent()));
41908
43648
  return;
41909
43649
  }
41910
43650
  disposeDisplacedSurface(cEntity, viewer);
@@ -41926,6 +43666,8 @@ function syncDisplacedSurface(params) {
41926
43666
  textureColorMask: style.textureColorMask,
41927
43667
  driveMaterial: false,
41928
43668
  produceValueCanvas: true,
43669
+ metadata: extrusionArchive.metadata,
43670
+ cellTexels: 1,
41929
43671
  baselineMask: extrusionArchive.metadata.BaselineMask,
41930
43672
  // Off-type for the same reason as the fill path: the pinned bruce-models predates the field.
41931
43673
  maskBaseline: style.maskTextureBaseline
@@ -41942,9 +43684,15 @@ function syncDisplacedSurface(params) {
41942
43684
  terrainProvider: viewer && viewer.terrainProvider,
41943
43685
  valueMin: extrusionArchive.metadata.ValueMin,
41944
43686
  valueMax: extrusionArchive.metadata.ValueMax,
41945
- exaggeration: style.extrusionExaggeration,
43687
+ exaggeration: exaggerationFor(style, extrusionArchive.metadata, extent),
41946
43688
  lowColor: mask ? Color$1.ColorFromStr(mask.minColor) : null,
41947
- highColor: mask ? Color$1.ColorFromStr(mask.maxColor) : null
43689
+ highColor: mask ? Color$1.ColorFromStr(mask.maxColor) : null,
43690
+ // Normalised the same way the animator normalises them, so the surface and the flat drape
43691
+ // agree about which values the style hides.
43692
+ rampStops: (mask && mask.points || []).map((stop) => ({
43693
+ position: TextureFrameSeriesAnimator.NormalisePosition(extrusionArchive.metadata, stop.position),
43694
+ color: Color$1.ColorFromStr(stop.color)
43695
+ })).filter((stop) => Boolean(stop.color))
41948
43696
  });
41949
43697
  surface.BindAnimator(animator);
41950
43698
  viewer.scene.primitives.add(surface);
@@ -41977,6 +43725,40 @@ function surfacePlacement(entity, heightRef) {
41977
43725
  * @param metadata
41978
43726
  * @param posses
41979
43727
  */
43728
+ /*
43729
+ * The factor a displaced surface should use, honouring an explicit style setting first.
43730
+ *
43731
+ * An elevation is a real height against a datum, so scaling it is not a display choice: it lifts the
43732
+ * surface off the datum it belongs to. Only a thickness is safe to exaggerate, which is why an
43733
+ * elevation archive defaults to 1 rather than to a derived factor.
43734
+ */
43735
+ function exaggerationFor(style, metadata, extent) {
43736
+ if (style.extrusionExaggeration != null) {
43737
+ return style.extrusionExaggeration;
43738
+ }
43739
+ if (metadata.Quantity === TextureFrameSeriesAnimator.QUANTITY_ELEVATION) {
43740
+ return 1;
43741
+ }
43742
+ if (!extent) {
43743
+ return undefined;
43744
+ }
43745
+ return DisplacedSurfacePrimitive.autoExaggeration(extent, metadata.ValueMin, metadata.ValueMax);
43746
+ }
43747
+ /*
43748
+ * The lon/lat bounding rectangle of a ring, which is the frame Cesium drapes an image material in.
43749
+ */
43750
+ function polygonBbox(posses) {
43751
+ if (!posses || posses.length === 0) {
43752
+ return undefined;
43753
+ }
43754
+ const rectangle = Rectangle.fromCartesianArray(posses);
43755
+ return {
43756
+ West: Math$1.toDegrees(rectangle.west),
43757
+ East: Math$1.toDegrees(rectangle.east),
43758
+ South: Math$1.toDegrees(rectangle.south),
43759
+ North: Math$1.toDegrees(rectangle.north)
43760
+ };
43761
+ }
41980
43762
  function archiveExtent(metadata, posses) {
41981
43763
  if (metadata.West != null && metadata.East != null && metadata.South != null && metadata.North != null) {
41982
43764
  return { West: metadata.West, East: metadata.East, South: metadata.South, North: metadata.North };
@@ -42027,9 +43809,22 @@ function disposeDisplacedSurface(cEntity, viewer) {
42027
43809
  * @param viewer
42028
43810
  * @param textureColorMask
42029
43811
  */
42030
- function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask, maskTextureBaseline) {
43812
+ function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask, maskTextureBaseline, cellBorder, cellTexels, produceValueCanvas, offline, drapeExtent) {
42031
43813
  const existing = cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY];
42032
- if (frameArchive && existing && !existing.IsDisposed() && existing.GetArchiveUrl() === frameArchive.url) {
43814
+ // Offline says nothing about whether the archive changed, only that it could not be asked.
43815
+ if (offline && existing && !existing.IsDisposed()) {
43816
+ return existing;
43817
+ }
43818
+ // Appearance is part of the reuse test, not just the archive URL. Recolouring a texture-driven
43819
+ // polygon keeps the same archive, so matching on the URL alone kept the old Animator and the
43820
+ // style edit appeared to do nothing at all.
43821
+ const appearance = TextureFrameSeriesAnimator.AppearanceSignature({
43822
+ textureColorMask, cellBorder, cellTexels, maskBaseline: maskTextureBaseline,
43823
+ valueCanvas: produceValueCanvas, drape: drapeExtent
43824
+ });
43825
+ if (frameArchive && existing && !existing.IsDisposed()
43826
+ && existing.GetArchiveUrl() === frameArchive.url
43827
+ && existing.GetAppearanceSignature() === appearance) {
42033
43828
  return existing;
42034
43829
  }
42035
43830
  if (existing && !existing.IsDisposed()) {
@@ -42045,12 +43840,118 @@ function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask
42045
43840
  archiveUrl: frameArchive.url,
42046
43841
  frames: frameArchive.metadata.Frames,
42047
43842
  textureColorMask,
43843
+ metadata: frameArchive.metadata,
43844
+ cellBorder,
43845
+ cellTexels,
42048
43846
  baselineMask: frameArchive.metadata.BaselineMask,
42049
- maskBaseline: maskTextureBaseline
43847
+ maskBaseline: maskTextureBaseline,
43848
+ produceValueCanvas,
43849
+ drapeExtent
42050
43850
  });
42051
43851
  cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY] = animator;
42052
43852
  return animator;
42053
43853
  }
43854
+ /**
43855
+ * Folds the cell-border and value-label style into the single overlay that draws both.
43856
+ *
43857
+ * Returns undefined when neither is asked for, which is what keeps the overlay off entirely.
43858
+ */
43859
+ function wantsLabels(style) {
43860
+ const texture = style.texture;
43861
+ return Boolean(texture && !Array.isArray(texture) && texture.label);
43862
+ }
43863
+ /*
43864
+ * How the cell grid and its values are drawn, which the style does not get a say in.
43865
+ *
43866
+ * A white hairline reads over every ramp colour without competing with it, and the spacing and cap
43867
+ * are what a parcel-sized archive needs to stay readable while the camera moves. Exposing them
43868
+ * bought nothing except thresholds an author could get wrong.
43869
+ */
43870
+ const LABEL_GRID_COLOR = "rgba(255,255,255,0.55)";
43871
+ const LABEL_GRID_WIDTH_PIXELS = 1;
43872
+ const LABEL_MIN_SPACING_PIXELS = 78;
43873
+ const LABEL_MAX_LABELS = 240;
43874
+ function overlaySettings(style, entity, tags) {
43875
+ if (!wantsLabels(style)) {
43876
+ return undefined;
43877
+ }
43878
+ return {
43879
+ gridColor: LABEL_GRID_COLOR,
43880
+ gridWidthPixels: LABEL_GRID_WIDTH_PIXELS,
43881
+ minSpacingPixels: LABEL_MIN_SPACING_PIXELS,
43882
+ maxLabels: LABEL_MAX_LABELS
43883
+ };
43884
+ }
43885
+ /**
43886
+ * Keeps a value-label overlay in step with whichever animator is driving the polygon.
43887
+ *
43888
+ * Redrawn on preRender rather than on frame changes: labels are screen-space, so panning and zooming
43889
+ * move them even when the data has not advanced at all.
43890
+ */
43891
+ function syncTextureValueLabels(params) {
43892
+ const { cEntity, viewer, settings, extent, animator, metadata } = params;
43893
+ const existing = cEntity[TEXTURE_VALUE_LABELS_KEY];
43894
+ // Offline has no extent to rebuild the overlay from, so an existing one is left running.
43895
+ if (params.offline && existing) {
43896
+ return;
43897
+ }
43898
+ if (existing) {
43899
+ existing.remove();
43900
+ cEntity[TEXTURE_VALUE_LABELS_KEY] = null;
43901
+ }
43902
+ if (!settings || !extent || !animator) {
43903
+ return;
43904
+ }
43905
+ const labels = new TextureValueLabels.Labels({
43906
+ viewer, extent, settings, metadata,
43907
+ tiles: TextureFrameSeriesAnimator.IsTiledLayout(metadata)
43908
+ ? metadata.Tiles.map((tile) => ({
43909
+ West: tile.West, East: tile.East, South: tile.South, North: tile.North,
43910
+ ResolutionX: tile.ResolutionX, ResolutionY: tile.ResolutionY
43911
+ }))
43912
+ : undefined,
43913
+ clipRing: params.clipRing
43914
+ });
43915
+ let lastVersion = -1;
43916
+ let appliedFloor = null;
43917
+ let appliedCeiling = null;
43918
+ // The extremes are what turn an anomaly frame back into a reading, so they are worth two extra
43919
+ // Range GETs here even though nothing else in the render path wants them.
43920
+ animator.EnsureExtremes();
43921
+ const remove = viewer.scene.preRender.addEventListener(() => {
43922
+ if (animator.IsDisposed()) {
43923
+ return;
43924
+ }
43925
+ const canvas = animator.GetValueCanvas();
43926
+ if (!canvas) {
43927
+ return;
43928
+ }
43929
+ const version = animator.GetPresentVersion();
43930
+ if (version !== lastVersion) {
43931
+ lastVersion = version;
43932
+ const ctx = canvas.getContext("2d");
43933
+ labels.SetSource(ctx.getImageData(0, 0, canvas.width, canvas.height));
43934
+ labels.SetPainted(animator.GetDisplayedPixels());
43935
+ }
43936
+ // Tracked by reference rather than latched on the first one to arrive: a tiled archive
43937
+ // composites the floor and the ceiling separately, so latching once would leave every label
43938
+ // showing a minimum and no maximum.
43939
+ const extremes = animator.GetExtremes();
43940
+ if (extremes.floor !== appliedFloor || extremes.ceiling !== appliedCeiling) {
43941
+ appliedFloor = extremes.floor;
43942
+ appliedCeiling = extremes.ceiling;
43943
+ labels.SetExtremes(appliedFloor, appliedCeiling);
43944
+ }
43945
+ labels.Render();
43946
+ });
43947
+ cEntity[TEXTURE_VALUE_LABELS_KEY] = {
43948
+ labels,
43949
+ remove: () => {
43950
+ remove();
43951
+ labels.Dispose();
43952
+ }
43953
+ };
43954
+ }
42054
43955
  /**
42055
43956
  * Derives a min/max/label time-range segment from a resolved frame archive's Frames metadata,
42056
43957
  * for reporting up to whoever owns the menu item this entity belongs to (see IParams.onSeriesDiscovered).
@@ -43658,7 +45559,7 @@ var StyleUtils;
43658
45559
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
43659
45560
  })(StyleUtils || (StyleUtils = {}));
43660
45561
 
43661
- const VERSION = "7.2.0";
45562
+ const VERSION = "7.2.2";
43662
45563
  /**
43663
45564
  * Updates the environment instance used by bruce-cesium to one specified.
43664
45565
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.
@@ -43675,5 +45576,5 @@ const getENVIRONMENT = () => {
43675
45576
  return ENVIRONMENT;
43676
45577
  };
43677
45578
 
43678
- export { VERSION, setENVIRONMENT, getENVIRONMENT, CesiumAnimatedInOut, DisplacedSurfacePrimitive, CesiumAnimatedProperty, isStyleChanged, isOutlineChanged$1 as isOutlineChanged, StyleEffective, EntityRenderEngine, EntityRenderEngineModel3d, EntityRenderEnginePoint, EntityRenderEnginePolygon, EntityRenderEnginePolyline, MenuItemCreator, MenuItemManager, CesiumParabola, EntityLabel, LiveCursor, SharedGetters, DataSourceStaticKmlManager, DataLabRenderManager, EntitiesIdsRenderManager, EntitiesLoadedRenderManager, EntitiesRenderManager, EntityRenderManager, AssemblyRenderManager, GoogleSearchRenderManager, RelationsRenderManager, RenderManager, TilesetArbRenderManager, TilesetCadRenderManager, TilesetEntitiesRenderManager, TilesetGooglePhotosRenderManager, TilesetOsmRenderManager, TilesetPointcloudRenderManager, TileRenderEngine, createTileset, TilesetRenderEngine, ViewRenderEngine, VisualRegisterCuller, VisualsRegister, XGridsRenderEngine, CesiumEntityStyler, DrawingUtils, EntityUtils, MeasureUtils, StyleUtils, CESIUM_INSPECTOR_KEY, CESIUM_TIMELINE_KEY, CESIUM_TIMELINE_LIVE_KEY, CESIUM_TIMELINE_LIVE_PADDING_KEY, CESIUM_TIMELINE_INTERVAL_KEY, CESIUM_MODEL_SPACE_KEY, DEFAULT_LIVE_PADDING_SECONDS, DEFAULT_BRIGHTNESS_SHIFT, DEFAULT_LIGHT_INTENSITY, LIGHTING_SETTINGS_VERSION, LIGHT_INTENSITY_COMPENSATION, MAX_COMPENSATED_LIGHT_INTENSITY, ViewUtils, CesiumViewMonitor, ViewGroundArea, ViewerEventTracker, ViewerUtils, Draw3dPolygon, Draw3dPolyline, MeasureCreator, Walkthrough, WidgetControlViewBar, WidgetControlViewBarSearch, WidgetLeftPanelTab, WidgetLeftPanelTabBookmarks, WidgetLeftPanelTabDashboard, EMenuItemState, WidgetLeftPanelTabMenuItems, Widget, VIEWER_BOOKMARKS_WIDGET_KEY, WidgetBookmarks, WidgetBranding, WidgetCursorBar, WidgetEmbeddedInfoView, WidgetInfoView, VIEWER_LEFT_PANEL_WIDGET_KEY, VIEWER_LEFT_PANEL_CSS_VAR_LEFT, WidgetLeftPanel, WidgetNavCompass$$1 as WidgetNavCompass, VIEWER_VIEW_BAR_WIDGET_KEY, WidgetViewBar };
45579
+ export { VERSION, setENVIRONMENT, getENVIRONMENT, CesiumAnimatedInOut, DisplacedSurfacePrimitive, CesiumAnimatedProperty, isStyleChanged, isOutlineChanged$1 as isOutlineChanged, StyleEffective, EntityRenderEngine, EntityRenderEngineModel3d, EntityRenderEnginePoint, EntityRenderEnginePolygon, EntityRenderEnginePolyline, TextureValueLabels, MenuItemCreator, MenuItemManager, CesiumParabola, EntityLabel, LiveCursor, SharedGetters, DataSourceStaticKmlManager, DataLabRenderManager, EntitiesIdsRenderManager, EntitiesLoadedRenderManager, EntitiesRenderManager, EntityRenderManager, AssemblyRenderManager, GoogleSearchRenderManager, RelationsRenderManager, RenderManager, TilesetArbRenderManager, TilesetCadRenderManager, TilesetEntitiesRenderManager, TilesetGooglePhotosRenderManager, TilesetOsmRenderManager, TilesetPointcloudRenderManager, TileRenderEngine, createTileset, TilesetRenderEngine, ViewRenderEngine, VisualRegisterCuller, VisualsRegister, XGridsRenderEngine, CesiumEntityStyler, DrawingUtils, EntityUtils, MeasureUtils, StyleUtils, CESIUM_INSPECTOR_KEY, CESIUM_TIMELINE_KEY, CESIUM_TIMELINE_LIVE_KEY, CESIUM_TIMELINE_LIVE_PADDING_KEY, CESIUM_TIMELINE_INTERVAL_KEY, CESIUM_MODEL_SPACE_KEY, DEFAULT_LIVE_PADDING_SECONDS, DEFAULT_BRIGHTNESS_SHIFT, DEFAULT_LIGHT_INTENSITY, LIGHTING_SETTINGS_VERSION, LIGHT_INTENSITY_COMPENSATION, MAX_COMPENSATED_LIGHT_INTENSITY, ViewUtils, CesiumViewMonitor, ViewGroundArea, ViewerEventTracker, ViewerUtils, Draw3dPolygon, Draw3dPolyline, MeasureCreator, Walkthrough, WidgetControlViewBar, WidgetControlViewBarSearch, WidgetLeftPanelTab, WidgetLeftPanelTabBookmarks, WidgetLeftPanelTabDashboard, EMenuItemState, WidgetLeftPanelTabMenuItems, Widget, VIEWER_BOOKMARKS_WIDGET_KEY, WidgetBookmarks, WidgetBranding, WidgetCursorBar, WidgetEmbeddedInfoView, WidgetInfoView, VIEWER_LEFT_PANEL_WIDGET_KEY, VIEWER_LEFT_PANEL_CSS_VAR_LEFT, WidgetLeftPanel, WidgetNavCompass$$1 as WidgetNavCompass, VIEWER_VIEW_BAR_WIDGET_KEY, WidgetViewBar };
43679
45580
  //# sourceMappingURL=bruce-cesium.es5.js.map