bruce-cesium 7.5.8 → 7.5.9

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.
@@ -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, Cartesian2, Cartesian4, sampleTerrain, Texture, PixelFormat, Sampler, TextureWrap, TextureMinificationFilter, TextureMagnificationFilter, VertexArray, BufferUsage, ShaderProgram, RenderState, BlendingState, DrawCommand, Pass, ConstantPositionProperty, HeightReference, DistanceDisplayCondition, JulianDate, Quaternion, HeadingPitchRoll, ColorBlendMode, ShadowMode, ClassificationType, Model, ArcType, CornerType, ImageMaterialProperty, PolygonHierarchy, PolylineGraphics, Rectangle, HorizontalOrigin, VerticalOrigin, SceneTransforms, FeatureDetection, SceneMode, Matrix3, NearFarScalar, KmlDataSource, GeoJsonDataSource, Cesium3DTileStyle, HeadingPitchRange, Cesium3DTileColorBlendMode, Ion, IonResource, Cesium3DTileset, OrthographicFrustum, EasingFunction, Intersect, ModelGraphics, PolygonGraphics, CorridorGraphics, PointGraphics, BillboardGraphics, EllipseGraphics, PolylineDashMaterialProperty, EllipsoidGeodesic, EllipsoidTerrainProvider, sampleTerrainMostDetailed, defined, GeometryInstance, PolygonPipeline, CustomDataSource, IonImageryProvider, createWorldImagery, createWorldImageryAsync, BingMapsImageryProvider, BingMapsStyle, MapboxImageryProvider, MapboxStyleImageryProvider, ArcGisMapServerImageryProvider, OpenStreetMapImageryProvider, UrlTemplateImageryProvider, GridImageryProvider, GeographicTilingScheme, ImageryLayer, TileMapServiceImageryProvider, CesiumTerrainProvider, CesiumInspector, ClockRange, DynamicEnvironmentMapManager, ScreenSpaceEventHandler, ScreenSpaceEventType, Fullscreen, CzmlDataSource } from 'cesium';
3
- import { Cartes, LRUCache, ProjectViewTile, ENVIRONMENT, Calculator, ClientFile, Entity as Entity$1, EntityLod, EntityTag, EntityType, ObjectUtils, Style, UrlUtils, Carto, Geometry as Geometry$1, ClientFileValueMap, Color as Color$1, Bounds, Api, MenuItem, ProjectView, ProjectViewBookmark, Tileset, ZoomControl, BruceEvent, EntityCoords, DataLab, DelayQueue, AccountConcept, RecordChangeFeed, BruceApi, EntityRelation, Camera, ProgramKey, EntitySource, ProjectViewLegacyTile, Account, AccountLimits, Session, EntityAttachment, EntityAttachmentType, EntityAttribute, MathUtils, ProjectViewBookmarkGroup, EntityRelationType, EntityHistoricData } from 'bruce-models';
2
+ import { Cartographic, ShaderSource, Cartesian3, Math as Math$1, Ellipsoid, Transforms, Matrix4, Geometry, GeometryAttribute, ComponentDatatype, PrimitiveType, BoundingSphere, GeometryPipeline, Cartesian2, Cartesian4, sampleTerrain, Texture, PixelFormat, Sampler, TextureWrap, TextureMinificationFilter, TextureMagnificationFilter, PixelDatatype, VertexArray, BufferUsage, ShaderProgram, RenderState, BlendingState, DrawCommand, Pass, Entity, ConstantPositionProperty, HeightReference, Color, DistanceDisplayCondition, ColorMaterialProperty, JulianDate, Quaternion, HeadingPitchRoll, CallbackProperty, HorizontalOrigin, VerticalOrigin, ConstantProperty, ClassificationType, Primitive, Cesium3DTileFeature, ColorBlendMode, ShadowMode, Model, ArcType, CornerType, ImageMaterialProperty, PolygonHierarchy, PolylineGraphics, Rectangle, SceneMode, IntersectionTests, Ray, SceneTransforms, FeatureDetection, Matrix3, NearFarScalar, KmlDataSource, GeoJsonDataSource, Cesium3DTileStyle, HeadingPitchRange, Ion, Cesium3DTileColorBlendMode, IonResource, Cesium3DTileset, Intersect, EllipsoidTerrainProvider, IonImageryProvider, createWorldImagery, createWorldImageryAsync, BingMapsImageryProvider, BingMapsStyle, MapboxImageryProvider, MapboxStyleImageryProvider, ArcGisMapServerImageryProvider, OpenStreetMapImageryProvider, UrlTemplateImageryProvider, GridImageryProvider, GeographicTilingScheme, ImageryLayer, TileMapServiceImageryProvider, CesiumTerrainProvider, OrthographicFrustum, EasingFunction, ModelGraphics, PolygonGraphics, CorridorGraphics, PointGraphics, BillboardGraphics, EllipseGraphics, PolylineDashMaterialProperty, EllipsoidGeodesic, sampleTerrainMostDetailed, defined, PolygonPipeline, GeometryInstance, CesiumInspector, ClockRange, DynamicEnvironmentMapManager, CustomDataSource, ScreenSpaceEventHandler, ScreenSpaceEventType, Fullscreen, CzmlDataSource } from 'cesium';
3
+ import { Cartes, LRUCache, ProjectViewTile, ENVIRONMENT, Api, Calculator, ClientFile, Entity as Entity$1, EntityTag, EntityType, ObjectUtils, Style, EntityLod, UrlUtils, Carto, Geometry as Geometry$1, MenuItem, ProjectView, ProjectViewBookmark, Tileset, ZoomControl, ClientFileValueMap, Color as Color$1, Bounds, BruceEvent, EntityCoords, DataLab, DelayQueue, AccountConcept, BruceApi, RecordChangeFeed, EntityRelation, EntitySource, ProgramKey, ProjectViewLegacyTile, Camera, Account, AccountLimits, Session, ProjectViewBookmarkGroup, EntityAttachment, EntityAttachmentType, EntityAttribute, MathUtils, EntityRelationType, EntityHistoricData } from 'bruce-models';
4
4
 
5
5
  /**
6
6
  * Ensures a number is returned from a given value.
@@ -7311,7 +7311,8 @@ var EntityLabel;
7311
7311
  this.groupId = groupId;
7312
7312
  this.additionalTexts = this.options.text != null ? [] : null;
7313
7313
  this.photo = null;
7314
- this.createLabel();
7314
+ // A label given its position keeps it, or the rebuild goes looking for the entity's own.
7315
+ this.createLabel(this.prePositioned ? this._pos3d : undefined);
7315
7316
  }
7316
7317
  /**
7317
7318
  * Creates cesium entity for the label if one does not exist.
@@ -13811,7 +13812,9 @@ var VisualsRegister;
13811
13812
  entityId: params.entityId,
13812
13813
  menuItemId: params.menuItemId,
13813
13814
  position: params.position ? { ...params.position } : undefined,
13814
- createIfMissing: params.createIfMissing !== false
13815
+ createIfMissing: params.createIfMissing !== false,
13816
+ text: params.text,
13817
+ pos3d: params.pos3d ? Cartesian3.clone(params.pos3d) : undefined
13815
13818
  };
13816
13819
  const doc = this.viewer.container.ownerDocument;
13817
13820
  // Only a label following the cursor should go when the cursor leaves the canvas.
@@ -13827,6 +13830,13 @@ var VisualsRegister;
13827
13830
  this.SyncLabelHover();
13828
13831
  this.viewer.scene.requestRender();
13829
13832
  }
13833
+ /*
13834
+ * Which Entity holds the label hover, if any, so a caller can leave another's hover alone.
13835
+ */
13836
+ GetLabelHoverTarget() {
13837
+ const hover = this.labelHover;
13838
+ return hover ? { entityId: hover.entityId, menuItemId: hover.menuItemId } : undefined;
13839
+ }
13830
13840
  /*
13831
13841
  * Whether the label hover is on this Entity, with or without a cursor position.
13832
13842
  */
@@ -13839,7 +13849,7 @@ var VisualsRegister;
13839
13849
  return (hover === null || hover === void 0 ? void 0 : hover.entityId) === entityId && (hover.menuItemId == null || hover.menuItemId === menuItemId) ? hover.position : undefined;
13840
13850
  }
13841
13851
  SyncLabelHover() {
13842
- var _a, _b, _c;
13852
+ var _a, _b, _c, _d;
13843
13853
  const hover = this.labelHover;
13844
13854
  if (!hover || this.disposed) {
13845
13855
  return;
@@ -13871,12 +13881,24 @@ var VisualsRegister;
13871
13881
  this.ReleaseHoverLabel(true);
13872
13882
  return;
13873
13883
  }
13884
+ const hoverOptions = {
13885
+ presentation: "text",
13886
+ text: (_b = hover.text) !== null && _b !== void 0 ? _b : group === null || group === void 0 ? void 0 : group.options.text,
13887
+ selectOnClick: false
13888
+ };
13874
13889
  if (this.hoverLabel && !this.hoverLabel.Disposed) {
13890
+ // Rebuilds only when the text changed, as it does when the pointer crosses a cell.
13891
+ if (hover.pos3d && !Cartesian3.equals(hover.pos3d, this.hoverLabel.pos3d)) {
13892
+ this.hoverLabel.pos3d = hover.pos3d;
13893
+ }
13894
+ if (hover.text != null) {
13895
+ this.hoverLabel.Configure(hoverOptions);
13896
+ }
13875
13897
  this.hoverLabel.SetHoverVisible(true);
13876
13898
  return;
13877
13899
  }
13878
13900
  // Query-only icon groups have coordinates but no registered scene visual to attach to.
13879
- if (!rego && group && [group.item.longitude, group.item.latitude, (_b = group.item.altitude) !== null && _b !== void 0 ? _b : 0].every(Number.isFinite)) {
13901
+ if (!rego && group && [group.item.longitude, group.item.latitude, (_c = group.item.altitude) !== null && _c !== void 0 ? _c : 0].every(Number.isFinite)) {
13880
13902
  rego = {
13881
13903
  entityId: hover.entityId,
13882
13904
  menuItemId: hover.menuItemId,
@@ -13898,15 +13920,12 @@ var VisualsRegister;
13898
13920
  rego,
13899
13921
  viewer: this.viewer,
13900
13922
  visualRegister: this,
13901
- api: (_c = this.ApiGetters) === null || _c === void 0 ? void 0 : _c.GetBruceApi(),
13923
+ api: (_d = this.ApiGetters) === null || _d === void 0 ? void 0 : _d.GetBruceApi(),
13902
13924
  persistent: false,
13903
13925
  transient: true,
13904
13926
  hoverRingState: this.hoverRingState,
13905
- options: {
13906
- presentation: "text",
13907
- text: group === null || group === void 0 ? void 0 : group.options.text,
13908
- selectOnClick: false
13909
- },
13927
+ pos3d: hover.pos3d,
13928
+ options: hoverOptions,
13910
13929
  visible: () => {
13911
13930
  var _a;
13912
13931
  const current = this.GetState(hover);
@@ -46823,6 +46842,10 @@ var TextureFrameSeriesAnimator;
46823
46842
  TextureFrameSeriesAnimator.AppearanceSignature = AppearanceSignature;
46824
46843
  const MAX_COMPOSITE_TEXELS = 2048;
46825
46844
  const VALUE_DILATE_PASSES = 2;
46845
+ // Decoded frame bytes kept at once. A 2048 square composite is 16 MB, so a long series cannot all stay.
46846
+ const MAX_CACHED_FRAME_BYTES = 256 * 1024 * 1024;
46847
+ // Fetched bytes kept for decoding again, a small fraction of what they decode to.
46848
+ const MAX_RAW_FRAME_BYTES = 96 * 1024 * 1024;
46826
46849
  /*
46827
46850
  * Rewrites a packed 16 bit value as the 8 bit grey the colour ramps index on.
46828
46851
  *
@@ -46843,19 +46866,33 @@ var TextureFrameSeriesAnimator;
46843
46866
  /*
46844
46867
  * Bleeds covered values outward into uncovered texels, leaving alpha untouched.
46845
46868
  */
46846
- function dilateValues(value, width, height, passes, packed) {
46869
+ function DilateValues(value, width, height, passes, packed) {
46847
46870
  const texels = width * height;
46848
- const filled = new Uint8Array(texels);
46871
+ // 255 for a texel that had data to begin with, otherwise the 1 based pass that filled it, so a pass
46872
+ // reads only texels filled before it and can work in place.
46873
+ const stamp = new Uint8Array(texels);
46849
46874
  for (let p = 0; p < texels; p++) {
46850
- filled[p] = value[p * 4 + 3] !== 0 ? 1 : 0;
46875
+ stamp[p] = value[p * 4 + 3] !== 0 ? 255 : 0;
46851
46876
  }
46877
+ // Whether a texel or a horizontal neighbour had data, so empty space is skipped in three reads.
46878
+ const across = new Uint8Array(texels);
46852
46879
  for (let pass = 0; pass < passes; pass++) {
46853
- const source = value.slice();
46854
- const wasFilled = filled.slice();
46880
+ const current = pass + 1;
46881
+ const had = (p) => stamp[p] !== 0 && stamp[p] !== current;
46882
+ for (let y = 0; y < height; y++) {
46883
+ const row = y * width;
46884
+ for (let x = 0; x < width; x++) {
46885
+ const p = row + x;
46886
+ across[p] = (had(p) || (x > 0 && had(p - 1)) || (x + 1 < width && had(p + 1))) ? 1 : 0;
46887
+ }
46888
+ }
46855
46889
  for (let y = 0; y < height; y++) {
46856
46890
  for (let x = 0; x < width; x++) {
46857
46891
  const p = y * width + x;
46858
- if (wasFilled[p]) {
46892
+ if (stamp[p] !== 0) {
46893
+ continue;
46894
+ }
46895
+ if (!across[p] && !(y > 0 && across[p - width]) && !(y + 1 < height && across[p + width])) {
46859
46896
  continue;
46860
46897
  }
46861
46898
  let sum = 0;
@@ -46864,13 +46901,13 @@ var TextureFrameSeriesAnimator;
46864
46901
  for (let dx = -1; dx <= 1; dx++) {
46865
46902
  const nx = x + dx;
46866
46903
  const ny = y + dy;
46867
- if (nx < 0 || ny < 0 || nx >= width || ny >= height || !wasFilled[ny * width + nx]) {
46904
+ if (nx < 0 || ny < 0 || nx >= width || ny >= height || !had(ny * width + nx)) {
46868
46905
  continue;
46869
46906
  }
46870
46907
  const at = (ny * width + nx) * 4;
46871
46908
  // Averaged as a value rather than per byte: averaging a packed low byte
46872
46909
  // on its own wraps at every 256th step and speckles the bled edge.
46873
- sum += packed ? source[at] * 256 + source[at + 1] : source[at];
46910
+ sum += packed ? value[at] * 256 + value[at + 1] : value[at];
46874
46911
  hits++;
46875
46912
  }
46876
46913
  }
@@ -46886,18 +46923,25 @@ var TextureFrameSeriesAnimator;
46886
46923
  value[p * 4 + 1] = v;
46887
46924
  value[p * 4 + 2] = v;
46888
46925
  }
46889
- filled[p] = 1;
46926
+ stamp[p] = current;
46890
46927
  }
46891
46928
  }
46892
46929
  }
46893
46930
  }
46894
46931
  }
46932
+ TextureFrameSeriesAnimator.DilateValues = DilateValues;
46933
+ /*
46934
+ * Bytes one cached frame holds, counting a buffer shared between colour and value once.
46935
+ */
46936
+ function frameBytes(pixels, value) {
46937
+ return (pixels ? pixels.byteLength : 0) + (value && value !== pixels ? value.byteLength : 0);
46938
+ }
46895
46939
  const DEFAULT_CROSSFADE_MS = 900;
46896
46940
  const DEFAULT_LOW_COLOR = { red: 255, green: 255, blue: 255, alpha: 0.12 };
46897
46941
  const DEFAULT_HIGH_COLOR = { red: 21, green: 96, blue: 196, alpha: 0.92 };
46898
46942
  class Animator {
46899
46943
  constructor(options) {
46900
- var _a, _b, _c;
46944
+ var _a, _b, _c, _d;
46901
46945
  this.removeOnTick = null;
46902
46946
  this.removeCrossfadeTick = null;
46903
46947
  this.disposed = false;
@@ -46928,6 +46972,15 @@ var TextureFrameSeriesAnimator;
46928
46972
  this.extremesLoad = null;
46929
46973
  this.floorPixels = null;
46930
46974
  this.ceilingPixels = null;
46975
+ // How far through the fade from the previous value frame to the presented one, 1 when settled.
46976
+ this.fadeMix = 1;
46977
+ // Where the current fade started from, above zero when it took over from one still running.
46978
+ this.fadeMixStart = 0;
46979
+ // Version the value canvas was last written at, since in value only mode it is written on demand.
46980
+ this.valueCanvasVersion = -1;
46981
+ this.rawBytes = 0;
46982
+ this.cachedBytes = 0;
46983
+ this.lastFrameBytes = 0;
46931
46984
  if (!options.entity.polygon) {
46932
46985
  throw new Error("TextureFrameSeriesAnimator requires an entity with polygon graphics.");
46933
46986
  }
@@ -46975,6 +47028,9 @@ var TextureFrameSeriesAnimator;
46975
47028
  this.maskBaseline = (_c = options.maskBaseline) !== null && _c !== void 0 ? _c : this.tiles.some((tile) => Boolean(tile.BaselineMask));
46976
47029
  this.valueCanvas = this.produceValueCanvas ? document.createElement("canvas") : null;
46977
47030
  this.driveMaterial = options.driveMaterial !== false;
47031
+ this.valueOnly = this.produceValueCanvas && !this.driveMaterial;
47032
+ this.rawCache = new Array(this.frames.length).fill(null);
47033
+ this.cacheBudget = (_d = options.cacheBytes) !== null && _d !== void 0 ? _d : MAX_CACHED_FRAME_BYTES;
46978
47034
  this.originalMaterial = options.entity.polygon.material;
46979
47035
  this.imageProperty = new CallbackProperty(() => this.pool[this.poolIdx], false);
46980
47036
  if (this.driveMaterial) {
@@ -47040,7 +47096,40 @@ var TextureFrameSeriesAnimator;
47040
47096
  * or null unless the instance was constructed with produceValueCanvas.
47041
47097
  */
47042
47098
  GetValueCanvas() {
47043
- return this.valueDims ? this.valueCanvas : null;
47099
+ if (!this.valueDims) {
47100
+ return null;
47101
+ }
47102
+ if (this.valueOnly && this.valueCanvasVersion !== this.presentVersion && this.displayedValuePixels) {
47103
+ this.writeInto(this.valueCanvas, this.displayedValuePixels, this.valueDims);
47104
+ this.valueCanvasVersion = this.presentVersion;
47105
+ }
47106
+ return this.valueCanvas;
47107
+ }
47108
+ /**
47109
+ * The presented frame's value pixels, or null before one. Shares the cached array, so never write to it.
47110
+ */
47111
+ GetValueRaster() {
47112
+ if (!this.valueDims || !this.displayedValuePixels) {
47113
+ return null;
47114
+ }
47115
+ return { data: this.displayedValuePixels, width: this.valueDims.width, height: this.valueDims.height };
47116
+ }
47117
+ /**
47118
+ * The two value frames being faded between and the mix, or null unless the material is not driven.
47119
+ */
47120
+ GetValueFade() {
47121
+ if (!this.valueOnly || !this.valueDims || !this.displayedValuePixels) {
47122
+ return null;
47123
+ }
47124
+ const previous = this.fadeFromValuePixels;
47125
+ const from = previous && previous.length === this.displayedValuePixels.length ? previous : null;
47126
+ return {
47127
+ from,
47128
+ to: this.displayedValuePixels,
47129
+ width: this.valueDims.width,
47130
+ height: this.valueDims.height,
47131
+ mix: from ? this.fadeMix : 1
47132
+ };
47044
47133
  }
47045
47134
  /**
47046
47135
  * Increments every time a frame is presented, so a consumer holding the value canvas can tell
@@ -47107,7 +47196,11 @@ var TextureFrameSeriesAnimator;
47107
47196
  if (this.currentFrameIndex < 0) {
47108
47197
  return this.frameCache[0] ? -1 : 0;
47109
47198
  }
47110
- for (let d = 0; d < n; d++) {
47199
+ // Full once another frame will not fit, so a prefetch only displaces a frame further from the
47200
+ // playhead than itself and never trades the two ends of the window back and forth.
47201
+ const full = this.cachedBytes + this.lastFrameBytes > this.cacheBudget;
47202
+ const reach = full ? this.farthestCachedDistance() : n;
47203
+ for (let d = 0; d < Math.min(n, reach); d++) {
47111
47204
  const forward = this.currentFrameIndex + d;
47112
47205
  if (forward < n && !this.frameCache[forward]) {
47113
47206
  return forward;
@@ -47119,6 +47212,68 @@ var TextureFrameSeriesAnimator;
47119
47212
  }
47120
47213
  return -1;
47121
47214
  }
47215
+ farthestCachedDistance() {
47216
+ let farthest = 0;
47217
+ for (let i = 0; i < this.frameCache.length; i++) {
47218
+ if (this.frameCache[i]) {
47219
+ farthest = Math.max(farthest, Math.abs(i - this.currentFrameIndex));
47220
+ }
47221
+ }
47222
+ return farthest;
47223
+ }
47224
+ /*
47225
+ * Drops decoded frames furthest from the playhead until the cache fits its budget again.
47226
+ */
47227
+ evictDecoded(keep) {
47228
+ // A tinted fade reads its target from the cache each tick, where a value fade holds the arrays.
47229
+ const fading = !this.valueOnly && this.fadeStartMs !== null ? this.fadeToIndex : -1;
47230
+ while (this.cachedBytes > this.cacheBudget) {
47231
+ let victim = -1;
47232
+ let distance = -1;
47233
+ for (let i = 0; i < this.frameCache.length; i++) {
47234
+ if (!this.frameCache[i] || i === keep || i === this.currentFrameIndex || i === fading) {
47235
+ continue;
47236
+ }
47237
+ const d = Math.abs(i - this.currentFrameIndex);
47238
+ if (d > distance) {
47239
+ distance = d;
47240
+ victim = i;
47241
+ }
47242
+ }
47243
+ if (victim < 0) {
47244
+ return;
47245
+ }
47246
+ this.cachedBytes -= frameBytes(this.frameCache[victim], this.valueCache[victim]);
47247
+ this.frameCache[victim] = null;
47248
+ this.valueCache[victim] = null;
47249
+ }
47250
+ }
47251
+ /*
47252
+ * Keeps a frame's fetched bytes unless they are large, as a tile major archive's are.
47253
+ */
47254
+ keepRaw(idx, buffer, base) {
47255
+ if (buffer.byteLength > MAX_RAW_FRAME_BYTES / 16) {
47256
+ return;
47257
+ }
47258
+ this.rawCache[idx] = { buffer, base };
47259
+ this.rawBytes += buffer.byteLength;
47260
+ while (this.rawBytes > MAX_RAW_FRAME_BYTES) {
47261
+ let victim = -1;
47262
+ let distance = -1;
47263
+ for (let i = 0; i < this.rawCache.length; i++) {
47264
+ const d = Math.abs(i - this.currentFrameIndex);
47265
+ if (this.rawCache[i] && i !== idx && d > distance) {
47266
+ distance = d;
47267
+ victim = i;
47268
+ }
47269
+ }
47270
+ if (victim < 0) {
47271
+ return;
47272
+ }
47273
+ this.rawBytes -= this.rawCache[victim].buffer.byteLength;
47274
+ this.rawCache[victim] = null;
47275
+ }
47276
+ }
47122
47277
  pumpFetchQueue() {
47123
47278
  if (this.disposed || this.inFlightIndex !== -1) {
47124
47279
  return;
@@ -47162,10 +47317,18 @@ var TextureFrameSeriesAnimator;
47162
47317
  lo = Math.min(lo, at.ByteOffset);
47163
47318
  hi = Math.max(hi, at.ByteOffset + at.ByteLength);
47164
47319
  }
47165
- const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
47166
- const buffer = await response.arrayBuffer();
47167
- if (this.disposed) {
47168
- return;
47320
+ let buffer;
47321
+ const raw = this.rawCache[idx];
47322
+ if (raw && raw.base === lo) {
47323
+ buffer = raw.buffer;
47324
+ }
47325
+ else {
47326
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
47327
+ buffer = await response.arrayBuffer();
47328
+ if (this.disposed) {
47329
+ return;
47330
+ }
47331
+ this.keepRaw(idx, buffer, lo);
47169
47332
  }
47170
47333
  // Awaited before decoding rather than applied later, so no frame is ever cached unmasked and
47171
47334
  // then corrected once the mask lands.
@@ -47177,9 +47340,15 @@ var TextureFrameSeriesAnimator;
47177
47340
  if (this.disposed) {
47178
47341
  return;
47179
47342
  }
47343
+ if (this.frameCache[idx]) {
47344
+ this.cachedBytes -= frameBytes(this.frameCache[idx], this.valueCache[idx]);
47345
+ }
47180
47346
  this.frameCache[idx] = decoded.pixels;
47181
47347
  this.frameDims[idx] = { width: decoded.width, height: decoded.height };
47182
47348
  this.valueCache[idx] = decoded.valuePixels || null;
47349
+ this.lastFrameBytes = frameBytes(decoded.pixels, decoded.valuePixels || null);
47350
+ this.cachedBytes += this.lastFrameBytes;
47351
+ this.evictDecoded(idx);
47183
47352
  if (this.shouldPresent(idx, isTarget)) {
47184
47353
  this.beginCrossfadeTo(idx);
47185
47354
  }
@@ -47249,6 +47418,13 @@ var TextureFrameSeriesAnimator;
47249
47418
  */
47250
47419
  tint(imageData) {
47251
47420
  const canvas = { width: imageData.width, height: imageData.height };
47421
+ if (this.valueOnly) {
47422
+ // One array serves as both, since nothing will ever display the colour.
47423
+ const values = imageData.data;
47424
+ this.applyBaselineMask(values, undefined, canvas.width, canvas.height);
47425
+ DilateValues(values, canvas.width, canvas.height, VALUE_DILATE_PASSES, IsPackedValue(this.metadata));
47426
+ return { pixels: values, width: canvas.width, height: canvas.height, valuePixels: values };
47427
+ }
47252
47428
  // Copied before tinting: the ramp is a lerp between two colours, so the value cannot be
47253
47429
  // recovered from the tinted pixels afterwards.
47254
47430
  const valuePixels = this.produceValueCanvas ? new Uint8ClampedArray(imageData.data) : undefined;
@@ -47269,7 +47445,7 @@ var TextureFrameSeriesAnimator;
47269
47445
  }
47270
47446
  this.applyBaselineMask(imageData.data, valuePixels, canvas.width, canvas.height);
47271
47447
  if (valuePixels) {
47272
- dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES, IsPackedValue(this.metadata));
47448
+ DilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES, IsPackedValue(this.metadata));
47273
47449
  }
47274
47450
  return { pixels: imageData.data, width: canvas.width, height: canvas.height, valuePixels };
47275
47451
  }
@@ -47527,6 +47703,10 @@ var TextureFrameSeriesAnimator;
47527
47703
  }
47528
47704
  }
47529
47705
  beginCrossfadeTo(idx) {
47706
+ if (this.valueOnly) {
47707
+ this.beginValueFadeTo(idx);
47708
+ return;
47709
+ }
47530
47710
  const to = this.frameCache[idx];
47531
47711
  this.fadeFromValuePixels = this.displayedValuePixels === null && this.valueCache[idx]
47532
47712
  ? new Uint8ClampedArray(this.valueCache[idx].length)
@@ -47536,12 +47716,53 @@ var TextureFrameSeriesAnimator;
47536
47716
  this.fadeToDims = this.frameDims[idx];
47537
47717
  this.fadeStartMs = Now();
47538
47718
  }
47719
+ /*
47720
+ * Presents a value frame at once and leaves the fade to the consumer, which blends on the GPU.
47721
+ */
47722
+ beginValueFadeTo(idx) {
47723
+ const to = this.valueCache[idx];
47724
+ if (!to || to === this.displayedValuePixels) {
47725
+ return;
47726
+ }
47727
+ // A new target mid fade keeps whichever frame is mostly on screen and carries on from the current
47728
+ // mix, so frames landing in quick succession do not jump the sheet back.
47729
+ const midFade = this.fadeStartMs !== null && Boolean(this.fadeFromValuePixels);
47730
+ if (midFade && this.fadeMix < 0.5) {
47731
+ this.fadeMixStart = this.fadeMix;
47732
+ }
47733
+ else {
47734
+ this.fadeFromValuePixels = this.displayedValuePixels;
47735
+ this.fadeMixStart = 0;
47736
+ }
47737
+ this.displayedValuePixels = to;
47738
+ this.valueDims = this.frameDims[idx];
47739
+ this.fadeToIndex = idx;
47740
+ this.fadeMix = this.fadeFromValuePixels ? this.fadeMixStart : 1;
47741
+ this.fadeStartMs = this.fadeFromValuePixels ? Now() : null;
47742
+ this.presentVersion++;
47743
+ this.requestRender();
47744
+ }
47745
+ requestRender() {
47746
+ if (this.viewer && this.viewer.scene && this.viewer.scene.requestRenderMode) {
47747
+ this.viewer.scene.requestRender();
47748
+ }
47749
+ }
47539
47750
  tickCrossfade() {
47540
47751
  if (this.disposed || this.fadeStartMs === null) {
47541
47752
  return;
47542
47753
  }
47543
47754
  const t = Math.min(1, (Now() - this.fadeStartMs) / this.crossfadeMs);
47544
47755
  const eased = easeInOutQuad(t);
47756
+ if (this.valueOnly) {
47757
+ this.fadeMix = this.fadeMixStart + (1 - this.fadeMixStart) * eased;
47758
+ if (t >= 1) {
47759
+ this.fadeStartMs = null;
47760
+ this.fadeFromValuePixels = null;
47761
+ this.fadeMix = 1;
47762
+ }
47763
+ this.requestRender();
47764
+ return;
47765
+ }
47545
47766
  const from = this.fadeFromPixels;
47546
47767
  const to = this.frameCache[this.fadeToIndex];
47547
47768
  const n = Math.min(from.length, to.length);
@@ -47581,9 +47802,7 @@ var TextureFrameSeriesAnimator;
47581
47802
  this.valueDims = dims;
47582
47803
  }
47583
47804
  this.presentVersion++;
47584
- if (this.viewer && this.viewer.scene && this.viewer.scene.requestRenderMode) {
47585
- this.viewer.scene.requestRender();
47586
- }
47805
+ this.requestRender();
47587
47806
  }
47588
47807
  }
47589
47808
  TextureFrameSeriesAnimator.Animator = Animator;
@@ -47606,13 +47825,15 @@ var TextureValueLabels;
47606
47825
  const MIN_VISIBLE_PIXELS = 60;
47607
47826
  const MAX_STRIDE = 512;
47608
47827
  const STRIDE_HYSTERESIS = 1.4;
47609
- const DEFAULT_MIN_SPACING_PIXELS = 78;
47610
- const DEFAULT_MAX_LABELS = 240;
47611
- const LABEL_RADIUS = 4;
47612
- const LABEL_FONT = "600 11px ui-monospace, SFMono-Regular, Menlo, monospace";
47613
- // Clear space either side of a label box, so neighbours read as separate.
47614
- const LABEL_GAP_PIXELS = 16;
47615
- const LABEL_ANCHOR_COLOR = "rgba(255,255,255,0.85)";
47828
+ // Scene labels carry a leader and are wider than a value, so they want room and not many of them.
47829
+ const DEFAULT_MIN_SPACING_PIXELS = 140;
47830
+ const DEFAULT_MAX_LABELS = 60;
47831
+ // How far past the edge of the view an anchor may sit before its label is dropped.
47832
+ const LABEL_EDGE_MARGIN_PIXELS = 120;
47833
+ // Distance discount for a label already up when the cap forces a choice.
47834
+ const LABEL_KEEP_BIAS = 0.6;
47835
+ // How long a label outlives its cell leaving the wanted set, so one back a frame later does not blink.
47836
+ const LABEL_LINGER_MS = 300;
47616
47837
  // 8 rather than 5: below this a cell reads as texture rather than as a boundary, and it also
47617
47838
  // bounds the worst case, since a full screen of 8 px cells fits inside MAX_GRID_CELLS.
47618
47839
  const DEFAULT_MIN_GRID_CELL_PIXELS = 8;
@@ -47626,7 +47847,8 @@ var TextureValueLabels;
47626
47847
  const TILE_OUTLINE_ALPHA = 0.45;
47627
47848
  // Cells per grid square at the coarsest. Beyond this a tile is outlined instead.
47628
47849
  const MAX_GRID_STRIDE = 256;
47629
- const LINE_HEIGHT = 13;
47850
+ // Times the pointer's ground hit is moved onto the sheet's own height before reading a cell.
47851
+ const HOVER_REFINE_STEPS = 3;
47630
47852
  /**
47631
47853
  * A clip outline from a projected ring, or null when the ring cannot be drawn.
47632
47854
  * @param projected one entry per ring vertex, null where the vertex has no window coordinate
@@ -47720,11 +47942,25 @@ var TextureValueLabels;
47720
47942
  this.sourceMap = null;
47721
47943
  this.source = null;
47722
47944
  this.painted = null;
47945
+ this.paintedRamp = null;
47723
47946
  this.floor = null;
47724
47947
  this.ceiling = null;
47725
47948
  this.disposed = false;
47726
- this.drawn = 0;
47727
47949
  this.stride = 0;
47950
+ this.cellLabels = new Map();
47951
+ this.lingerTimer = null;
47952
+ // What the last draw was made from, so a frame that changes none of it draws nothing.
47953
+ this.drawnView = null;
47954
+ this.drawnInputs = [];
47955
+ this.scratchWorld = new Cartesian3();
47956
+ this.scratchWindow = new Cartesian2();
47957
+ this.scratchClip = new Cartesian4();
47958
+ // The camera's view and projection as one matrix, rebuilt per draw, or null outside 3D.
47959
+ this.viewProjection = null;
47960
+ this.viewProjectionScratch = new Matrix4();
47961
+ this.hovering = false;
47962
+ this.hoverFrame = 0;
47963
+ this.hoverAt = null;
47728
47964
  this.viewer = options.viewer;
47729
47965
  this.extent = options.extent;
47730
47966
  this.settings = options.settings || {};
@@ -47734,6 +47970,9 @@ var TextureValueLabels;
47734
47970
  this.sourceMap = TextureFrameSeriesAnimator.SourceMap(options.metadata);
47735
47971
  this.tiles = options.tiles;
47736
47972
  this.clipRing = options.clipRing;
47973
+ this.heightAt = options.heightAt;
47974
+ this.heightKey = options.heightKey;
47975
+ this.hover = options.hover;
47737
47976
  this.canvas = document.createElement("canvas");
47738
47977
  this.canvas.style.position = "absolute";
47739
47978
  this.canvas.style.inset = "0";
@@ -47743,12 +47982,31 @@ var TextureValueLabels;
47743
47982
  parent.appendChild(this.canvas);
47744
47983
  }
47745
47984
  this.ctx = this.canvas.getContext("2d");
47985
+ this.onPointerMove = (event) => {
47986
+ const bounds = this.viewer.canvas.getBoundingClientRect();
47987
+ this.hoverAt = { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
47988
+ // At most once a frame, since a pointer reports far faster than anything is drawn.
47989
+ if (!this.hoverFrame) {
47990
+ this.hoverFrame = requestAnimationFrame(() => {
47991
+ this.hoverFrame = 0;
47992
+ this.updateHover();
47993
+ });
47994
+ }
47995
+ };
47996
+ this.onPointerLeave = () => {
47997
+ this.hoverAt = null;
47998
+ this.releaseHover();
47999
+ };
48000
+ if (this.hover && this.hover.register) {
48001
+ this.viewer.canvas.addEventListener("pointermove", this.onPointerMove, { passive: true });
48002
+ this.viewer.canvas.addEventListener("pointerleave", this.onPointerLeave);
48003
+ }
47746
48004
  }
47747
48005
  SetExtent(extent) {
47748
48006
  this.extent = extent;
47749
48007
  }
47750
48008
  /**
47751
- * The current frame's decoded values, as the animator's value canvas already provides them.
48009
+ * The current frame's decoded values.
47752
48010
  * @param source value pixels, one texel per cell
47753
48011
  */
47754
48012
  SetSource(source) {
@@ -47756,11 +48014,17 @@ var TextureValueLabels;
47756
48014
  }
47757
48015
  /**
47758
48016
  * The tinted pixels of the same frame, so the grid can skip cells the ramp painted nothing in.
47759
- * @param painted RGBA of the presented frame, or null to fall back to coverage alone
48017
+ * @param painted RGBA of the presented frame, or null to fall back to the ramp or coverage alone
47760
48018
  */
47761
48019
  SetPainted(painted) {
47762
48020
  this.painted = painted;
47763
48021
  }
48022
+ /**
48023
+ * The surface's 256 step RGBA ramp, for telling whether a cell was painted without a tinted frame.
48024
+ */
48025
+ SetPaintedRamp(ramp) {
48026
+ this.paintedRamp = ramp;
48027
+ }
47764
48028
  /**
47765
48029
  * The archive's per-cell extremes, when it publishes them. Without these a label can still
47766
48030
  * print the current reading, just not the range behind it.
@@ -47770,21 +48034,28 @@ var TextureValueLabels;
47770
48034
  this.ceiling = ceiling;
47771
48035
  }
47772
48036
  GetDrawnCount() {
47773
- return this.drawn;
48037
+ return this.cellLabels.size;
47774
48038
  }
47775
48039
  Clear() {
47776
48040
  this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
47777
- this.drawn = 0;
48041
+ this.drawnView = null;
47778
48042
  }
47779
48043
  Dispose() {
47780
48044
  this.disposed = true;
48045
+ this.clearCellLabels();
48046
+ this.releaseHover();
48047
+ if (this.hoverFrame) {
48048
+ cancelAnimationFrame(this.hoverFrame);
48049
+ this.hoverFrame = 0;
48050
+ }
48051
+ this.viewer.canvas.removeEventListener("pointermove", this.onPointerMove);
48052
+ this.viewer.canvas.removeEventListener("pointerleave", this.onPointerLeave);
47781
48053
  if (this.canvas.parentElement) {
47782
48054
  this.canvas.parentElement.removeChild(this.canvas);
47783
48055
  }
47784
48056
  }
47785
48057
  /**
47786
- * Redraws every visible label. Cheap enough to call per rendered frame, because the cell
47787
- * count it considers is bounded by the view rather than by the texture.
48058
+ * Redraws the grid and syncs the value labels, when anything they are drawn from has changed.
47788
48059
  */
47789
48060
  Render() {
47790
48061
  var _a, _b, _c;
@@ -47792,67 +48063,296 @@ var TextureValueLabels;
47792
48063
  return;
47793
48064
  }
47794
48065
  const view = this.viewer.canvas;
48066
+ const inputs = [this.source, this.painted, this.paintedRamp, this.floor, this.ceiling,
48067
+ view.clientWidth, view.clientHeight, this.heightKey ? this.heightKey() : null];
48068
+ const camera = this.viewer.camera.viewMatrix;
48069
+ if (this.drawnView &&
48070
+ Matrix4.equals(this.drawnView, camera) &&
48071
+ inputs.every((input, i) => input === this.drawnInputs[i])) {
48072
+ return;
48073
+ }
47795
48074
  if (this.canvas.width !== view.clientWidth || this.canvas.height !== view.clientHeight) {
47796
48075
  this.canvas.width = view.clientWidth;
47797
48076
  this.canvas.height = view.clientHeight;
47798
48077
  }
47799
48078
  this.Clear();
48079
+ this.drawnView = Matrix4.clone(camera, this.drawnView || undefined);
48080
+ this.drawnInputs = inputs;
48081
+ const projection = this.viewer.camera.frustum.projectionMatrix;
48082
+ this.viewProjection = this.viewer.scene.mode === SceneMode.SCENE3D
48083
+ ? Matrix4.multiply(projection, camera, this.viewProjectionScratch)
48084
+ : null;
47800
48085
  const cols = this.source.width;
47801
48086
  const rows = this.source.height;
47802
48087
  const window = this.visibleTexels(cols, rows);
47803
48088
  if (!window) {
48089
+ this.syncCellLabels([]);
47804
48090
  return;
47805
48091
  }
47806
- const midCol = (window.col0 + window.col1) / 2;
47807
- const midRow = (window.row0 + window.row1) / 2;
47808
- const a = this.project(this.lonAt(midCol, cols), this.latAt(midRow, rows));
47809
- const b = this.project(this.lonAt(midCol + 1, cols), this.latAt(midRow + 1, rows));
48092
+ // Measured where the view is looking, since in an oblique view up close the middle of the visible
48093
+ // texels sits out toward the horizon, where cells are a fraction of the size.
48094
+ const focus = this.groundAtScreenCentre();
48095
+ const at = focus ? this.texelAt(focus.lon, focus.lat) : null;
48096
+ const focused = (Boolean(at) &&
48097
+ at.col >= window.col0 &&
48098
+ at.col <= window.col1 &&
48099
+ at.row >= window.row0 &&
48100
+ at.row <= window.row1);
48101
+ const midCol = focused ? at.col : (window.col0 + window.col1) / 2;
48102
+ const midRow = focused ? at.row : (window.row0 + window.row1) / 2;
48103
+ const a = this.projectCell(midCol, midRow, cols, rows);
48104
+ const b = this.projectCell(midCol + 1, midRow + 1, cols, rows);
47810
48105
  if (!a || !b) {
48106
+ // Left as they are, since one frame where the focus will not project is not the data leaving.
47811
48107
  return;
47812
48108
  }
47813
48109
  const cellPixels = Math.min(Math.abs(b.x - a.x), Math.abs(b.y - a.y));
47814
48110
  // Ahead of the label threshold on purpose: a grid still reads at a cell size where a
47815
48111
  // number no longer fits.
47816
48112
  this.drawGrid(window, cols, rows, cellPixels);
47817
- if (cellPixels < ((_a = this.settings.minCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_CELL_PIXELS)) {
48113
+ // By area rather than the shorter side, which an oblique view foreshortens to a fraction of the other.
48114
+ const across = this.projectCell(midCol + 1, midRow, cols, rows);
48115
+ const down = this.projectCell(midCol, midRow + 1, cols, rows);
48116
+ const cellSize = across && down
48117
+ ? Math.sqrt(Math.abs((across.x - a.x) * (down.y - a.y) - (across.y - a.y) * (down.x - a.x)))
48118
+ : cellPixels;
48119
+ const visibleWidth = cellSize * (window.col1 - window.col0 + 1);
48120
+ const visibleHeight = cellSize * (window.row1 - window.row0 + 1);
48121
+ if (cellSize < ((_a = this.settings.minCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_CELL_PIXELS) ||
48122
+ Math.max(visibleWidth, visibleHeight) < MIN_VISIBLE_PIXELS) {
48123
+ this.syncCellLabels([]);
47818
48124
  return;
47819
48125
  }
47820
- const visibleWidth = cellPixels * (window.col1 - window.col0 + 1);
47821
- const visibleHeight = cellPixels * (window.row1 - window.row0 + 1);
47822
- if (Math.max(visibleWidth, visibleHeight) < MIN_VISIBLE_PIXELS) {
47823
- return;
47824
- }
47825
- const parts = this.settings.parts || ["value", "floor", "ceiling"];
47826
- this.ctx.font = LABEL_FONT;
47827
- this.ctx.textAlign = "center";
47828
48126
  const maxLabels = (_b = this.settings.maxLabels) !== null && _b !== void 0 ? _b : DEFAULT_MAX_LABELS;
47829
48127
  this.stride = ChooseLabelStride({
47830
- cellPixels,
48128
+ cellPixels: cellSize,
47831
48129
  visibleCols: window.col1 - window.col0 + 1,
47832
48130
  visibleRows: window.row1 - window.row0 + 1,
47833
48131
  maxLabels,
47834
- // Measured, not assumed: the spacing that keeps labels apart is whatever the widest
47835
- // one is, so changing what a label says cannot quietly start overlapping them.
47836
- minSpacingPixels: Math.max((_c = this.settings.minSpacingPixels) !== null && _c !== void 0 ? _c : DEFAULT_MIN_SPACING_PIXELS, this.widestLabel(parts)),
48132
+ minSpacingPixels: (_c = this.settings.minSpacingPixels) !== null && _c !== void 0 ? _c : DEFAULT_MIN_SPACING_PIXELS,
47837
48133
  held: this.stride
47838
48134
  });
47839
- const stride = this.stride;
48135
+ const parts = this.settings.parts || ["value", "floor", "ceiling"];
48136
+ this.syncCellLabels(this.labelCells(window, cols, rows, this.stride, maxLabels, parts));
48137
+ }
48138
+ /*
48139
+ * One labelled cell per lattice block, keyed by the block's origin so it survives frame and stride changes.
48140
+ */
48141
+ labelCells(window, cols, rows, stride, max, parts) {
48142
+ const width = this.canvas.width;
48143
+ const height = this.canvas.height;
47840
48144
  const firstRow = Math.floor(window.row0 / stride) * stride;
47841
48145
  const firstCol = Math.floor(window.col0 / stride) * stride;
47842
- for (let row = firstRow; row <= window.row1 && this.drawn < maxLabels; row += stride) {
47843
- for (let col = firstCol; col <= window.col1 && this.drawn < maxLabels; col += stride) {
48146
+ const candidates = [];
48147
+ for (let row = firstRow; row <= window.row1; row += stride) {
48148
+ for (let col = firstCol; col <= window.col1; col += stride) {
47844
48149
  const found = this.readNear(col, row, cols, rows, stride);
47845
48150
  if (!found) {
47846
48151
  continue;
47847
48152
  }
47848
- const point = this.project(this.lonAt(found.col, cols), this.latAt(found.row, rows));
47849
- if (!point || point.x < 0 || point.y < 0
47850
- || point.x > this.canvas.width || point.y > this.canvas.height) {
48153
+ const point = this.projectCell(found.col, found.row, cols, rows);
48154
+ // A margin past the edge, so a label does not drop the moment its anchor does.
48155
+ const margin = LABEL_EDGE_MARGIN_PIXELS;
48156
+ if (!point ||
48157
+ point.x < -margin ||
48158
+ point.y < -margin ||
48159
+ point.x > width + margin ||
48160
+ point.y > height + margin) {
47851
48161
  continue;
47852
48162
  }
47853
- this.drawLabel(point, found.reading, parts, this.cellOutline(found.col, found.row, cols, rows));
48163
+ const text = this.labelLines(found.reading, parts).join("\n");
48164
+ if (!text) {
48165
+ continue;
48166
+ }
48167
+ const key = col + "," + row;
48168
+ const distance = Math.hypot(point.x - width / 2, point.y - height / 2);
48169
+ candidates.push({
48170
+ key,
48171
+ lon: this.lonAt(found.col, cols),
48172
+ lat: this.latAt(found.row, rows),
48173
+ text,
48174
+ // Labels already up outrank new ones, so the capped set does not shuffle as the view pans.
48175
+ rank: this.cellLabels.has(key) ? distance * LABEL_KEEP_BIAS : distance
48176
+ });
48177
+ }
48178
+ }
48179
+ if (candidates.length > max) {
48180
+ candidates.sort((x, y) => x.rank - y.rank);
48181
+ candidates.length = max;
48182
+ }
48183
+ return candidates;
48184
+ }
48185
+ /*
48186
+ * Keeps one scene label per wanted cell, reusing those that are still wanted.
48187
+ */
48188
+ syncCellLabels(wanted) {
48189
+ var _a;
48190
+ const keep = new Set(wanted.map((w) => w.key));
48191
+ const now = Date.now();
48192
+ let lingering = false;
48193
+ for (const [key, entry] of this.cellLabels) {
48194
+ if (keep.has(key)) {
48195
+ entry.missingSince = undefined;
48196
+ continue;
48197
+ }
48198
+ entry.missingSince = (_a = entry.missingSince) !== null && _a !== void 0 ? _a : now;
48199
+ if (now - entry.missingSince >= LABEL_LINGER_MS) {
48200
+ entry.label.Dispose();
48201
+ this.cellLabels.delete(key);
48202
+ }
48203
+ else {
48204
+ lingering = true;
48205
+ }
48206
+ }
48207
+ // A still camera draws nothing more, so a lingering label is given its own later look.
48208
+ if (lingering && !this.lingerTimer) {
48209
+ this.lingerTimer = setTimeout(() => {
48210
+ this.lingerTimer = null;
48211
+ if (!this.disposed) {
48212
+ this.drawnView = null;
48213
+ this.viewer.scene.requestRender();
48214
+ }
48215
+ }, LABEL_LINGER_MS);
48216
+ }
48217
+ for (const want of wanted) {
48218
+ const pos3d = Cartesian3.fromDegrees(want.lon, want.lat, this.surfaceHeight(want.lon, want.lat));
48219
+ const options = {
48220
+ presentation: "text",
48221
+ text: want.text,
48222
+ leader: true,
48223
+ selectOnClick: false
48224
+ };
48225
+ const existing = this.cellLabels.get(want.key);
48226
+ if (existing) {
48227
+ if (!Cartesian3.equalsEpsilon(existing.label.pos3d, pos3d, 0, 0.01)) {
48228
+ existing.label.pos3d = pos3d;
48229
+ }
48230
+ if (existing.text !== want.text) {
48231
+ existing.label.Configure(options);
48232
+ existing.text = want.text;
48233
+ }
48234
+ continue;
48235
+ }
48236
+ const id = (this.hover ? this.hover.entityId : "texture-value") + "#cell:" + want.key;
48237
+ const label = new EntityLabel.Label({
48238
+ rego: {
48239
+ entityId: id,
48240
+ menuItemId: this.hover ? this.hover.menuItemId : undefined,
48241
+ name: want.text,
48242
+ priority: 0,
48243
+ // No graphic, since a point or billboard visual pulls a label back to itself on rebuild.
48244
+ visual: new Entity({ position: pos3d })
48245
+ },
48246
+ viewer: this.viewer,
48247
+ pos3d,
48248
+ options,
48249
+ persistent: true,
48250
+ visible: () => !this.disposed
48251
+ });
48252
+ this.cellLabels.set(want.key, { label, text: want.text });
48253
+ }
48254
+ }
48255
+ clearCellLabels() {
48256
+ if (this.lingerTimer) {
48257
+ clearTimeout(this.lingerTimer);
48258
+ this.lingerTimer = null;
48259
+ }
48260
+ for (const entry of this.cellLabels.values()) {
48261
+ entry.label.Dispose();
48262
+ }
48263
+ this.cellLabels.clear();
48264
+ }
48265
+ /*
48266
+ * Shows the cell under the pointer through the register's hover label, unless something else holds it.
48267
+ */
48268
+ updateHover() {
48269
+ const hover = this.hover;
48270
+ if (this.disposed || !hover || !hover.register || !this.source || !this.hoverAt) {
48271
+ return;
48272
+ }
48273
+ const register = hover.register;
48274
+ const target = register.GetLabelHoverTarget();
48275
+ const ours = Boolean(target) && register.IsLabelHovered(hover.entityId, hover.menuItemId);
48276
+ if (target && !ours) {
48277
+ this.hovering = false;
48278
+ return;
48279
+ }
48280
+ const under = this.readingUnder(this.hoverAt.x, this.hoverAt.y);
48281
+ if (!under) {
48282
+ this.releaseHover();
48283
+ return;
48284
+ }
48285
+ register.SetLabelHover({
48286
+ entityId: hover.entityId,
48287
+ menuItemId: hover.menuItemId,
48288
+ position: { x: this.hoverAt.x, y: this.hoverAt.y },
48289
+ text: under.text,
48290
+ pos3d: under.pos3d
48291
+ });
48292
+ this.hovering = true;
48293
+ }
48294
+ releaseHover() {
48295
+ const hover = this.hover;
48296
+ if (!this.hovering || !hover || !hover.register) {
48297
+ return;
48298
+ }
48299
+ this.hovering = false;
48300
+ if (hover.register.IsLabelHovered(hover.entityId, hover.menuItemId)) {
48301
+ hover.register.ClearLabelHover();
48302
+ }
48303
+ }
48304
+ /*
48305
+ * The text for the painted cell under a window position and its centre on the sheet, or null.
48306
+ */
48307
+ readingUnder(x, y) {
48308
+ const at = this.cellUnder(x, y);
48309
+ if (!at) {
48310
+ return null;
48311
+ }
48312
+ const cols = this.source.width;
48313
+ const found = this.readAt(at.col, at.row, cols);
48314
+ if (!found || !this.paintedAt(at.col, at.row, cols)) {
48315
+ return null;
48316
+ }
48317
+ const text = this.labelLines(found, this.settings.parts || ["value", "floor", "ceiling"]).join("\n");
48318
+ if (!text) {
48319
+ return null;
48320
+ }
48321
+ const lon = this.lonAt(at.col, cols);
48322
+ const lat = this.latAt(at.row, this.source.height);
48323
+ return { text, pos3d: Cartesian3.fromDegrees(lon, lat, this.surfaceHeight(lon, lat)) };
48324
+ }
48325
+ /*
48326
+ * The cell under a window position, found on the sheet rather than on the ground beneath it.
48327
+ */
48328
+ cellUnder(x, y) {
48329
+ const scene = this.viewer.scene;
48330
+ const camera = this.viewer.camera;
48331
+ const screen = new Cartesian2(x, y);
48332
+ const ray = camera.getPickRay(screen);
48333
+ if (!ray) {
48334
+ return null;
48335
+ }
48336
+ let hit = (scene.globe && scene.globe.pick(ray, scene)) || camera.pickEllipsoid(screen);
48337
+ if (!hit) {
48338
+ return null;
48339
+ }
48340
+ const radii = Ellipsoid.WGS84.radii;
48341
+ for (let i = 0; i < HOVER_REFINE_STEPS && this.heightAt; i++) {
48342
+ const carto = Cartographic.fromCartesian(hit);
48343
+ const height = this.heightAt(Math$1.toDegrees(carto.longitude), Math$1.toDegrees(carto.latitude));
48344
+ if (height == null) {
48345
+ break;
47854
48346
  }
48347
+ const inflated = new Ellipsoid(radii.x + height, radii.y + height, radii.z + height);
48348
+ const interval = IntersectionTests.rayEllipsoid(ray, inflated);
48349
+ if (!interval) {
48350
+ break;
48351
+ }
48352
+ hit = Ray.getPoint(ray, interval.start);
47855
48353
  }
48354
+ const carto = Cartographic.fromCartesian(hit);
48355
+ return this.texelAt(Math$1.toDegrees(carto.longitude), Math$1.toDegrees(carto.latitude));
47856
48356
  }
47857
48357
  /*
47858
48358
  * Outlines each covered cell, from a projected lattice of its corners.
@@ -47880,44 +48380,25 @@ var TextureValueLabels;
47880
48380
  if (wide * high > MAX_GRID_CELLS) {
47881
48381
  return;
47882
48382
  }
47883
- const lattice = new Array((wide + 1) * (high + 1));
47884
- for (let r = 0; r <= high; r++) {
47885
- for (let c = 0; c <= wide; c++) {
47886
- // Corners sit half a texel out from the centres lonAt/latAt return.
47887
- const lon = this.lonAt(window.col0 + c - 0.5, cols);
47888
- const lat = this.latAt(window.row0 + r - 0.5, rows);
47889
- lattice[r * (wide + 1) + c] = this.project(lon, lat);
47890
- }
47891
- }
47892
- const path = new Path2D();
47893
- let drew = false;
48383
+ const lattice = new Lattice(wide + 1, high + 1, (c, r) => {
48384
+ // Corners sit half a texel out from the centres lonAt/latAt return.
48385
+ const lon = this.lonAt(window.col0 + c - 0.5, cols);
48386
+ const lat = this.latAt(window.row0 + r - 0.5, rows);
48387
+ return this.projectInto(lon, lat);
48388
+ });
48389
+ const covered = new Uint8Array(wide * high);
47894
48390
  for (let r = 0; r < high; r++) {
47895
48391
  for (let c = 0; c < wide; c++) {
47896
- if (!this.isCovered(window.col0 + c, window.row0 + r, cols)) {
47897
- continue;
47898
- }
47899
- const tl = lattice[r * (wide + 1) + c];
47900
- const tr = lattice[r * (wide + 1) + c + 1];
47901
- const bl = lattice[(r + 1) * (wide + 1) + c];
47902
- const br = lattice[(r + 1) * (wide + 1) + c + 1];
47903
- if (!tl || !tr || !bl || !br) {
47904
- continue;
47905
- }
47906
- path.moveTo(tl.x, tl.y);
47907
- path.lineTo(tr.x, tr.y);
47908
- path.lineTo(br.x, br.y);
47909
- path.lineTo(bl.x, bl.y);
47910
- path.closePath();
47911
- drew = true;
48392
+ covered[r * wide + c] = this.isCovered(window.col0 + c, window.row0 + r, cols) ? 1 : 0;
47912
48393
  }
47913
48394
  }
47914
- if (!drew) {
48395
+ const path = new Path2D();
48396
+ if (!lattice.Trace(path, covered, wide, high)) {
47915
48397
  return;
47916
48398
  }
47917
48399
  this.ctx.strokeStyle = color;
47918
48400
  this.ctx.lineWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
47919
48401
  this.ctx.stroke(path);
47920
- +" alpha" + this.ctx.globalAlpha;
47921
48402
  }
47922
48403
  /*
47923
48404
  * Restricts drawing to the polygon, so nothing is ruled over ground it does not cover.
@@ -47939,33 +48420,11 @@ var TextureValueLabels;
47939
48420
  path.closePath();
47940
48421
  this.ctx.clip(path);
47941
48422
  }
47942
- /*
47943
- * The cell of whichever tile covers a point, as screen corners.
47944
- */
47945
- tileCellOutline(lon, lat) {
47946
- for (const tile of this.tiles) {
47947
- if (lon < tile.West || lon > tile.East || lat < tile.South || lat > tile.North) {
47948
- continue;
47949
- }
47950
- const c = (Math.min(tile.ResolutionX - 1, Math.max(0, Math.floor((lon - tile.West) / (tile.East - tile.West) * tile.ResolutionX))));
47951
- const r = (Math.min(tile.ResolutionY - 1, Math.max(0, Math.floor((tile.North - lat) / (tile.North - tile.South) * tile.ResolutionY))));
47952
- const w0 = tile.West + (c / tile.ResolutionX) * (tile.East - tile.West);
47953
- const w1 = tile.West + ((c + 1) / tile.ResolutionX) * (tile.East - tile.West);
47954
- const n0 = tile.North - (r / tile.ResolutionY) * (tile.North - tile.South);
47955
- const n1 = tile.North - ((r + 1) / tile.ResolutionY) * (tile.North - tile.South);
47956
- const corners = [
47957
- this.project(w0, n0), this.project(w1, n0),
47958
- this.project(w1, n1), this.project(w0, n1)
47959
- ];
47960
- return corners.every((q) => q) ? corners : null;
47961
- }
47962
- return null;
47963
- }
47964
48423
  /*
47965
48424
  * Outlines each tile's own cells, so cell size still shows where the mesh is refined.
47966
48425
  */
47967
48426
  drawTileGrid(color) {
47968
- var _a, _b, _c;
48427
+ var _a, _b;
47969
48428
  const view = this.viewer.camera.computeViewRectangle(Ellipsoid.WGS84);
47970
48429
  if (!view) {
47971
48430
  return;
@@ -47994,7 +48453,7 @@ var TextureValueLabels;
47994
48453
  coarse.lineTo(tr.x, tr.y);
47995
48454
  coarse.lineTo(br.x, br.y);
47996
48455
  coarse.lineTo(bl.x, bl.y);
47997
- coarse.closePath();
48456
+ coarse.lineTo(tl.x, tl.y);
47998
48457
  drewCoarse = true;
47999
48458
  };
48000
48459
  // Planned for every visible tile BEFORE any is drawn.
@@ -48055,31 +48514,29 @@ var TextureValueLabels;
48055
48514
  // moves, and so each step up is a subset of the one below it.
48056
48515
  const firstR = Math.floor(r0 / step) * step;
48057
48516
  const firstC = Math.floor(c0 / step) * step;
48058
- for (let r = firstR; r < r1; r += step) {
48059
- const n0 = tile.North - (r / tile.ResolutionY) * (tile.North - tile.South);
48060
- const n1 = tile.North
48061
- - (Math.min(r + step, tile.ResolutionY) / tile.ResolutionY) * (tile.North - tile.South);
48062
- for (let c = firstC; c < c1; c += step) {
48063
- const w0 = tile.West + (c / tile.ResolutionX) * (tile.East - tile.West);
48064
- const w1 = tile.West
48065
- + (Math.min(c + step, tile.ResolutionX) / tile.ResolutionX) * (tile.East - tile.West);
48066
- if (!this.coveredAt((w0 + w1) / 2, (n0 + n1) / 2)) {
48067
- continue;
48068
- }
48069
- const tl = this.project(w0, n0);
48070
- const tr = this.project(w1, n0);
48071
- const br = this.project(w1, n1);
48072
- const bl = this.project(w0, n1);
48073
- if (!tl || !tr || !br || !bl) {
48074
- continue;
48075
- }
48076
- path.moveTo(tl.x, tl.y);
48077
- path.lineTo(tr.x, tr.y);
48078
- path.lineTo(br.x, br.y);
48079
- path.lineTo(bl.x, bl.y);
48080
- path.closePath();
48081
- drew = true;
48082
- }
48517
+ const wide = Math.ceil((c1 - firstC) / step);
48518
+ const high = Math.ceil((r1 - firstR) / step);
48519
+ const lonOf = (c) => tile.West
48520
+ + (Math.min(firstC + c * step, tile.ResolutionX) / tile.ResolutionX) * (tile.East - tile.West);
48521
+ const latOf = (r) => tile.North
48522
+ - (Math.min(firstR + r * step, tile.ResolutionY) / tile.ResolutionY) * (tile.North - tile.South);
48523
+ const covered = new Uint8Array(wide * high);
48524
+ let any = false;
48525
+ for (let r = 0; r < high; r++) {
48526
+ for (let c = 0; c < wide; c++) {
48527
+ if (this.coveredAt((lonOf(c) + lonOf(c + 1)) / 2, (latOf(r) + latOf(r + 1)) / 2)) {
48528
+ covered[r * wide + c] = 1;
48529
+ any = true;
48530
+ }
48531
+ }
48532
+ }
48533
+ if (!any) {
48534
+ continue;
48535
+ }
48536
+ // Projected on demand, since a sparse raster leaves most corners belonging to no cell.
48537
+ const lattice = new Lattice(wide + 1, high + 1, (c, r) => this.projectInto(lonOf(c), latOf(r)));
48538
+ if (lattice.Trace(path, covered, wide, high)) {
48539
+ drew = true;
48083
48540
  }
48084
48541
  }
48085
48542
  const gridWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
@@ -48097,7 +48554,7 @@ var TextureValueLabels;
48097
48554
  return;
48098
48555
  }
48099
48556
  this.ctx.strokeStyle = color;
48100
- this.ctx.lineWidth = (_c = this.settings.gridWidthPixels) !== null && _c !== void 0 ? _c : DEFAULT_GRID_WIDTH_PIXELS;
48557
+ this.ctx.lineWidth = gridWidth;
48101
48558
  this.ctx.stroke(path);
48102
48559
  }
48103
48560
  /*
@@ -48117,7 +48574,7 @@ var TextureValueLabels;
48117
48574
  };
48118
48575
  }
48119
48576
  /*
48120
- * Whether the composited raster holds data at a lon/lat.
48577
+ * Whether the composited raster holds painted data at a lon/lat.
48121
48578
  */
48122
48579
  coveredAt(lon, lat) {
48123
48580
  if (!this.source) {
@@ -48130,60 +48587,20 @@ var TextureValueLabels;
48130
48587
  if (col < 0 || row < 0 || col >= cols || row >= rows) {
48131
48588
  return false;
48132
48589
  }
48133
- const at = (row * cols + col) * 4 + 3;
48134
48590
  // Painted, not merely covered: a dry cell holds a reading the ramp deliberately hides, and
48135
48591
  // ruling a grid over it draws detail where the picture shows none.
48136
- if (this.painted && this.painted.length === cols * rows * 4) {
48137
- return this.painted[at] > 0;
48592
+ if (this.painted || this.paintedRamp) {
48593
+ return this.paintedAt(col, row, cols);
48138
48594
  }
48139
- return this.source.data[at] >= 128;
48595
+ return this.source.data[(row * cols + col) * 4 + 3] >= 128;
48140
48596
  }
48141
48597
  isCovered(col, row, cols) {
48142
48598
  return Boolean(this.source && this.source.data[(row * cols + col) * 4 + 3] >= 128);
48143
48599
  }
48144
48600
  /*
48145
- * Width of the widest label this overlay could draw, in pixels.
48146
- */
48147
- widestLabel(parts) {
48148
- const sample = [];
48149
- if (parts.indexOf("value") >= 0) {
48150
- sample.push(valueLine("-000.00", LabelUnits(this.metadata)));
48151
- }
48152
- const range = [];
48153
- const showsRange = !TextureFrameSeriesAnimator.IsTransformed(this.metadata);
48154
- if (showsRange && parts.indexOf("floor") >= 0) {
48155
- range.push("min: -000.00");
48156
- }
48157
- if (showsRange && parts.indexOf("ceiling") >= 0) {
48158
- range.push("max: -000.00");
48159
- }
48160
- if (range.length) {
48161
- sample.push(range.join(" - "));
48162
- }
48163
- if (!sample.length) {
48164
- return 0;
48165
- }
48166
- const widest = Math.max(...sample.map((line) => this.ctx.measureText(line).width));
48167
- return widest + LABEL_GAP_PIXELS;
48168
- }
48169
- /*
48170
- * The four screen corners of one cell, or null if any of them will not project.
48601
+ * What a label says about one reading, a line at a time.
48171
48602
  */
48172
- cellOutline(col, row, cols, rows) {
48173
- // On a pyramid the composited texel is not a cell. Outlining it would draw a second,
48174
- // uniform lattice over the tile grid, misaligned with it everywhere.
48175
- if (this.tiles && this.tiles.length) {
48176
- return this.tileCellOutline(this.lonAt(col, cols), this.latAt(row, rows));
48177
- }
48178
- const corners = [
48179
- this.project(this.lonAt(col - 0.5, cols), this.latAt(row - 0.5, rows)),
48180
- this.project(this.lonAt(col + 0.5, cols), this.latAt(row - 0.5, rows)),
48181
- this.project(this.lonAt(col + 0.5, cols), this.latAt(row + 0.5, rows)),
48182
- this.project(this.lonAt(col - 0.5, cols), this.latAt(row + 0.5, rows))
48183
- ];
48184
- return corners.every((c) => c) ? corners : null;
48185
- }
48186
- drawLabel(point, reading, parts, outline) {
48603
+ labelLines(reading, parts) {
48187
48604
  var _a;
48188
48605
  const dp = (_a = this.settings.decimals) !== null && _a !== void 0 ? _a : (Math.abs(reading.value) < 10 ? 2 : 1);
48189
48606
  const lines = [];
@@ -48202,32 +48619,7 @@ var TextureValueLabels;
48202
48619
  if (range.length) {
48203
48620
  lines.push(range.join(" - "));
48204
48621
  }
48205
- if (!lines.length) {
48206
- return;
48207
- }
48208
- if (outline) {
48209
- this.ctx.strokeStyle = LABEL_ANCHOR_COLOR;
48210
- this.ctx.lineWidth = 1.5;
48211
- this.ctx.beginPath();
48212
- this.ctx.moveTo(outline[0].x, outline[0].y);
48213
- for (let i = 1; i < outline.length; i++) {
48214
- this.ctx.lineTo(outline[i].x, outline[i].y);
48215
- }
48216
- this.ctx.closePath();
48217
- this.ctx.stroke();
48218
- }
48219
- const width = Math.max(...lines.map((l) => this.ctx.measureText(l).width)) + 10;
48220
- const height = lines.length * LINE_HEIGHT + 6;
48221
- const top = point.y - height / 2;
48222
- this.ctx.fillStyle = "rgba(16,16,14,0.72)";
48223
- this.ctx.beginPath();
48224
- this.ctx.roundRect(point.x - width / 2, top, width, height, LABEL_RADIUS);
48225
- this.ctx.fill();
48226
- lines.forEach((line, i) => {
48227
- this.ctx.fillStyle = i === 0 ? "#ffffff" : "#a8a89f";
48228
- this.ctx.fillText(line, point.x, top + LINE_HEIGHT * (i + 1) - 1);
48229
- });
48230
- this.drawn++;
48622
+ return lines;
48231
48623
  }
48232
48624
  /*
48233
48625
  * The first cell with a reading at or near a lattice point, searched within its own block.
@@ -48254,11 +48646,24 @@ var TextureValueLabels;
48254
48646
  return null;
48255
48647
  }
48256
48648
  paintedAt(col, row, cols) {
48257
- if (!this.painted) {
48258
- return true;
48649
+ const at = (row * cols + col) * 4;
48650
+ if (this.painted && this.painted.length === this.source.data.length) {
48651
+ return this.painted[at + 3] > 0;
48259
48652
  }
48260
- const at = (row * cols + col) * 4 + 3;
48261
- return this.painted[at] > 0;
48653
+ if (this.paintedRamp) {
48654
+ const data = this.source.data;
48655
+ if (data[at + 3] === 0) {
48656
+ return false;
48657
+ }
48658
+ // The 8 bit grey the ramp is indexed on, which is what a tinted frame would have used.
48659
+ const grey = TextureFrameSeriesAnimator.IsPackedValue(this.metadata)
48660
+ ? Math.round((data[at] * 256 + data[at + 1]) / 65535 * 255)
48661
+ : data[at];
48662
+ const steps = this.paintedRamp.length / 4;
48663
+ const index = Math.min(steps - 1, Math.round(grey / 255 * (steps - 1)));
48664
+ return this.paintedRamp[index * 4 + 3] > 0;
48665
+ }
48666
+ return true;
48262
48667
  }
48263
48668
  // Decodes one cell, skipping anything the texture marks as having no data.
48264
48669
  readAt(col, row, cols) {
@@ -48274,6 +48679,16 @@ var TextureValueLabels;
48274
48679
  sourceMap: this.sourceMap
48275
48680
  });
48276
48681
  }
48682
+ /*
48683
+ * The texel holding a lon/lat, or null outside the raster.
48684
+ */
48685
+ texelAt(lon, lat) {
48686
+ const cols = this.source.width;
48687
+ const rows = this.source.height;
48688
+ const col = Math.floor((lon - this.extent.West) / (this.extent.East - this.extent.West) * cols);
48689
+ const row = Math.floor((this.extent.North - lat) / (this.extent.North - this.extent.South) * rows);
48690
+ return col < 0 || row < 0 || col >= cols || row >= rows ? null : { col, row };
48691
+ }
48277
48692
  lonAt(col, cols) {
48278
48693
  return this.extent.West + ((col + 0.5) / cols) * (this.extent.East - this.extent.West);
48279
48694
  }
@@ -48302,15 +48717,102 @@ var TextureValueLabels;
48302
48717
  row1: Math.min(rows - 1, Math.ceil((this.extent.North - south) / spanLat * rows) + 1)
48303
48718
  };
48304
48719
  }
48720
+ // Metres the sheet sits at over a point, or the ellipsoid when nothing says otherwise.
48721
+ surfaceHeight(lon, lat) {
48722
+ const height = this.heightAt ? this.heightAt(lon, lat) : null;
48723
+ return height == null || !isFinite(height) ? 0 : height;
48724
+ }
48725
+ projectCell(col, row, cols, rows) {
48726
+ return this.project(this.lonAt(col, cols), this.latAt(row, rows));
48727
+ }
48728
+ // Projected at the sheet's own height, since up close the ellipsoid is metres from where it is drawn.
48305
48729
  project(lon, lat) {
48306
- const world = Cartesian3.fromDegrees(lon, lat, 0);
48730
+ return this.projectInto(lon, lat) ? { x: this.scratchWindow.x, y: this.scratchWindow.y } : null;
48731
+ }
48732
+ /*
48733
+ * Projects into the shared scratch window position, returning whether it landed anywhere.
48734
+ */
48735
+ projectInto(lon, lat) {
48736
+ const world = Cartesian3.fromDegrees(lon, lat, this.surfaceHeight(lon, lat), Ellipsoid.WGS84, this.scratchWorld);
48737
+ // One matrix multiply in 3D, where the general path rederives the viewport and frustum per point.
48738
+ if (this.viewProjection) {
48739
+ const clip = Matrix4.multiplyByVector(this.viewProjection, Cartesian4.fromElements(world.x, world.y, world.z, 1, this.scratchClip), this.scratchClip);
48740
+ if (!(clip.w > 0)) {
48741
+ return null;
48742
+ }
48743
+ const view = this.viewer.canvas;
48744
+ this.scratchWindow.x = (clip.x / clip.w * 0.5 + 0.5) * view.clientWidth;
48745
+ this.scratchWindow.y = (0.5 - clip.y / clip.w * 0.5) * view.clientHeight;
48746
+ return this.scratchWindow;
48747
+ }
48307
48748
  const transforms = SceneTransforms;
48308
48749
  // Renamed in newer Cesium, and this library runs on whatever the host app supplies.
48309
48750
  const fn = transforms.worldToWindowCoordinates || transforms.wgs84ToWindowCoordinates;
48310
- return fn ? fn.call(transforms, this.viewer.scene, world) : null;
48751
+ return fn ? fn.call(transforms, this.viewer.scene, world, this.scratchWindow) || null : null;
48311
48752
  }
48312
48753
  }
48313
48754
  TextureValueLabels.Labels = Labels;
48755
+ /*
48756
+ * A grid of window positions projected at most once each, and the cell edges traced through them.
48757
+ */
48758
+ class Lattice {
48759
+ constructor(wide, high, projectAt) {
48760
+ this.wide = wide;
48761
+ this.projectAt = projectAt;
48762
+ this.xs = new Float64Array(wide * high);
48763
+ this.ys = new Float64Array(wide * high);
48764
+ this.state = new Uint8Array(wide * high);
48765
+ }
48766
+ at(c, r) {
48767
+ const i = r * this.wide + c;
48768
+ if (this.state[i] === 0) {
48769
+ const p = this.projectAt(c, r);
48770
+ if (p) {
48771
+ this.xs[i] = p.x;
48772
+ this.ys[i] = p.y;
48773
+ this.state[i] = 1;
48774
+ }
48775
+ else {
48776
+ this.state[i] = 2;
48777
+ }
48778
+ }
48779
+ return this.state[i] === 1 ? i : -1;
48780
+ }
48781
+ edge(path, a, b) {
48782
+ if (a < 0 || b < 0) {
48783
+ return false;
48784
+ }
48785
+ path.moveTo(this.xs[a], this.ys[a]);
48786
+ path.lineTo(this.xs[b], this.ys[b]);
48787
+ return true;
48788
+ }
48789
+ /*
48790
+ * Adds each covered cell's edges to a path, a shared edge once, and says whether any were added.
48791
+ */
48792
+ Trace(path, covered, cellsWide, cellsHigh) {
48793
+ let drew = false;
48794
+ for (let r = 0; r < cellsHigh; r++) {
48795
+ for (let c = 0; c < cellsWide; c++) {
48796
+ if (!covered[r * cellsWide + c]) {
48797
+ continue;
48798
+ }
48799
+ const tl = this.at(c, r);
48800
+ const tr = this.at(c + 1, r);
48801
+ const bl = this.at(c, r + 1);
48802
+ if (!(r > 0 && covered[(r - 1) * cellsWide + c])) {
48803
+ drew = this.edge(path, tl, tr) || drew;
48804
+ }
48805
+ if (!(c > 0 && covered[r * cellsWide + c - 1])) {
48806
+ drew = this.edge(path, tl, bl) || drew;
48807
+ }
48808
+ const br = this.at(c + 1, r + 1);
48809
+ drew = this.edge(path, tr, br) || drew;
48810
+ drew = this.edge(path, bl, br) || drew;
48811
+ }
48812
+ }
48813
+ return drew;
48814
+ }
48815
+ }
48314
48816
  /*
48315
48817
  * The value line. Units go on it alone: repeating them on the range underneath doubles the widest
48316
48818
  * label to say nothing, and the two lines are plainly the same quantity.
@@ -48411,6 +48913,9 @@ in vec2 st;
48411
48913
  in float skirt;
48412
48914
 
48413
48915
  uniform sampler2D u_valueTexture;
48916
+ // The frame being faded away from, and how far the fade has got. The same texture at 1 when settled.
48917
+ uniform sampler2D u_valueTextureFrom;
48918
+ uniform float u_valueFade;
48414
48919
  uniform sampler2D u_groundTexture;
48415
48920
  uniform float u_valuePacked;
48416
48921
  uniform float u_valueMin;
@@ -48531,18 +49036,22 @@ float decodeValue(vec4 texel) {
48531
49036
  }
48532
49037
 
48533
49038
  void main() {
48534
- vec4 texel = texture(u_valueTexture, st);
49039
+ vec4 texelTo = texture(u_valueTexture, st);
49040
+ vec4 texelFrom = texture(u_valueTextureFrom, st);
48535
49041
  v_st = st;
49042
+ // Decoded before mixing, since blending a packed pair byte by byte reads as some other value
49043
+ // halfway through the fade.
49044
+ float raw = mix(decodeValue(texelFrom), decodeValue(texelTo), u_valueFade);
49045
+ float alpha = mix(texelFrom.a, texelTo.a, u_valueFade);
48536
49046
 
48537
49047
  // Inside a region the scene knows is water, a texel with no value still has to paint, or the
48538
49048
  // hole the region exists to close is still a hole.
48539
49049
  // The value it paints is the shallowest the ramp is not transparent at.
48540
49050
  float inMask = u_maskWeight * texture(u_maskTexture, st).a;
48541
- float hasData = step(u_coverageCutoffVertex, texel.a);
48542
- float raw = decodeValue(texel);
49051
+ float hasData = step(u_coverageCutoffVertex, alpha);
48543
49052
  float filled = mix(u_maskFillValue, max(raw, u_maskFillValue), hasData);
48544
49053
  v_value = mix(raw, filled, inMask * u_maskFill);
48545
- v_coverage = max(texel.a, inMask * u_maskFill);
49054
+ v_coverage = max(alpha, inMask * u_maskFill);
48546
49055
 
48547
49056
  // Exaggeration stretches the span above the archive's minimum rather than the height itself. On an
48548
49057
  // elevation the height is measured from the ellipsoid, tens of metres from any of the data, so
@@ -48585,6 +49094,8 @@ void main() {
48585
49094
  `;
48586
49095
  const FRAGMENT_SHADER_GLSL300 = `
48587
49096
  uniform sampler2D u_valueTexture;
49097
+ uniform sampler2D u_valueTextureFrom;
49098
+ uniform float u_valueFade;
48588
49099
  uniform sampler2D u_rampTexture;
48589
49100
  uniform float u_coverageCutoff;
48590
49101
  uniform vec2 u_texelSize;
@@ -48621,12 +49132,15 @@ in vec2 v_waveMetres;
48621
49132
  * Scaled per channel for the same reason the vertex shader's decode is: reassembling the pair reaches
48622
49133
  * 65535, which mediump does not have to represent, and this is the shader that may not get highp.
48623
49134
  */
48624
- float valueAt(vec2 uv) {
48625
- vec4 texel = texture(u_valueTexture, uv);
49135
+ float decodeValue(vec4 texel) {
48626
49136
  float packed16 = texel.r * (255.0 * 256.0 / 65535.0) + texel.g * (255.0 / 65535.0);
48627
49137
  return mix(texel.r, packed16, u_valuePacked);
48628
49138
  }
48629
49139
 
49140
+ float valueAt(vec2 uv) {
49141
+ return mix(decodeValue(texture(u_valueTextureFrom, uv)), decodeValue(texture(u_valueTexture, uv)), u_valueFade);
49142
+ }
49143
+
48630
49144
  //#water
48631
49145
  /*
48632
49146
  * Slope of the ripple field at a point, in metres of rise per metre travelled.
@@ -49073,6 +49587,13 @@ void main() {
49073
49587
  this.rampTexture = null;
49074
49588
  this.texture = null;
49075
49589
  this.textureDirty = true;
49590
+ // Fed from a fading source: the frame being left, and which pixels each texture holds.
49591
+ this.fade = null;
49592
+ this.textureFrom = null;
49593
+ this.uploadedTo = null;
49594
+ this.uploadedFrom = null;
49595
+ // Size of whichever value raster is bound, canvas or pixels.
49596
+ this.sourceDims = null;
49076
49597
  this.shaderProgram = null;
49077
49598
  this.shaderProgramWater = false;
49078
49599
  this.renderState = null;
@@ -49243,6 +49764,65 @@ void main() {
49243
49764
  this.maskGroundTexture = null;
49244
49765
  }
49245
49766
  }
49767
+ /**
49768
+ * Metres above the ellipsoid the sheet sits at over a point, or null unless a fading source is bound.
49769
+ */
49770
+ HeightAt(lon, lat) {
49771
+ const pixels = this.fade ? this.fade.to : null;
49772
+ const dims = this.sourceDims;
49773
+ if (!pixels || !dims) {
49774
+ return null;
49775
+ }
49776
+ const fx = (lon - this.extent.West) / (this.extent.East - this.extent.West);
49777
+ const fy = (this.extent.North - lat) / (this.extent.North - this.extent.South);
49778
+ if (!(fx >= 0 && fx <= 1 && fy >= 0 && fy <= 1)) {
49779
+ return null;
49780
+ }
49781
+ const col = Math.min(dims.width - 1, Math.floor(fx * dims.width));
49782
+ const row = Math.min(dims.height - 1, Math.floor(fy * dims.height));
49783
+ const at = (row * dims.width + col) * 4;
49784
+ const coverage = pixels[at + 3] / 255;
49785
+ const normalised = this.packedValue
49786
+ ? (pixels[at] * 256 + pixels[at + 1]) / 65535
49787
+ : pixels[at] / 255;
49788
+ const metres = this.valueMin + normalised * (this.valueMax - this.valueMin) * this.exaggeration;
49789
+ const following = this.followGround && Boolean(this.groundPixels);
49790
+ const ground = following ? this.groundAt(fx, fy) : 0;
49791
+ return this.baseHeight + ground + metres * (following ? coverage : 1);
49792
+ }
49793
+ /**
49794
+ * Changes whenever HeightAt would answer differently for the same frame.
49795
+ */
49796
+ GetPlacementKey() {
49797
+ return [this.groundPixels ? 1 : 0, this.baseHeight, this.exaggeration, this.followGround ? 1 : 0].join("|");
49798
+ }
49799
+ /**
49800
+ * The ramp the sheet is painted with, as 256 RGBA steps across the value range.
49801
+ */
49802
+ GetRampPixels() {
49803
+ return this.rampPixels;
49804
+ }
49805
+ /*
49806
+ * The packed ground raster read the way sampleGround reads it, bilinear between texel centres.
49807
+ */
49808
+ groundAt(u, v) {
49809
+ const size = GROUND_SAMPLES_PER_SIDE;
49810
+ const pixels = this.groundPixels;
49811
+ const read = (x, y) => {
49812
+ const i = (Math.min(size - 1, Math.max(0, y)) * size + Math.min(size - 1, Math.max(0, x))) * 4;
49813
+ const packed = (pixels[i] << 16) + (pixels[i + 1] << 8) + pixels[i + 2];
49814
+ return GROUND_MIN_METRES + (packed / 16777215) * (GROUND_MAX_METRES - GROUND_MIN_METRES);
49815
+ };
49816
+ const x = u * size - 0.5;
49817
+ const y = v * size - 0.5;
49818
+ const x0 = Math.floor(x);
49819
+ const y0 = Math.floor(y);
49820
+ const fx = x - x0;
49821
+ const fy = y - y0;
49822
+ const top = read(x0, y0) + (read(x0 + 1, y0) - read(x0, y0)) * fx;
49823
+ const bottom = read(x0, y0 + 1) + (read(x0 + 1, y0 + 1) - read(x0, y0 + 1)) * fx;
49824
+ return top + (bottom - top) * fy;
49825
+ }
49246
49826
  /**
49247
49827
  * Feeds the surface from an animated frame series.
49248
49828
  * Height and colour advance with the clock without the caller having to push each frame in.
@@ -49261,23 +49841,39 @@ void main() {
49261
49841
  return;
49262
49842
  }
49263
49843
  const context = frameState.context;
49264
- if (this.valueSource) {
49265
- const canvas = this.valueSource.GetValueCanvas();
49266
- if (!canvas) {
49267
- // No frame decoded yet, so there is nothing to displace against.
49268
- return;
49269
- }
49270
- const version = this.valueSource.GetPresentVersion();
49271
- if (canvas !== this.source || version !== this.lastPresentVersion) {
49272
- this.source = canvas;
49273
- this.lastPresentVersion = version;
49274
- this.textureDirty = true;
49844
+ const fade = this.valueSource && this.valueSource.GetValueFade
49845
+ ? this.valueSource.GetValueFade()
49846
+ : null;
49847
+ if (fade) {
49848
+ this.fade = fade;
49849
+ this.sourceDims = { width: fade.width, height: fade.height };
49850
+ }
49851
+ else {
49852
+ this.fade = null;
49853
+ if (this.valueSource) {
49854
+ const canvas = this.valueSource.GetValueCanvas();
49855
+ if (!canvas) {
49856
+ // No frame decoded yet, so there is nothing to displace against.
49857
+ return;
49858
+ }
49859
+ const version = this.valueSource.GetPresentVersion();
49860
+ if (canvas !== this.source || version !== this.lastPresentVersion) {
49861
+ this.source = canvas;
49862
+ this.lastPresentVersion = version;
49863
+ this.textureDirty = true;
49864
+ }
49275
49865
  }
49866
+ this.sourceDims = this.source ? { width: this.source.width, height: this.source.height } : null;
49276
49867
  }
49277
- if (!this.source || !this.source.width || !this.source.height) {
49868
+ if (!this.sourceDims || !this.sourceDims.width || !this.sourceDims.height) {
49278
49869
  return;
49279
49870
  }
49280
- this.syncTexture(context);
49871
+ if (this.fade) {
49872
+ this.syncFadeTextures(context);
49873
+ }
49874
+ else {
49875
+ this.syncTexture(context);
49876
+ }
49281
49877
  this.syncRampTexture(context);
49282
49878
  this.requestGround();
49283
49879
  this.syncGroundTexture(context);
@@ -49325,6 +49921,10 @@ void main() {
49325
49921
  this.texture.destroy();
49326
49922
  this.texture = null;
49327
49923
  }
49924
+ if (this.textureFrom) {
49925
+ this.textureFrom.destroy();
49926
+ this.textureFrom = null;
49927
+ }
49328
49928
  if (this.rampTexture) {
49329
49929
  this.rampTexture.destroy();
49330
49930
  this.rampTexture = null;
@@ -49592,7 +50192,7 @@ void main() {
49592
50192
  const southWest = Cartesian3.fromDegrees(this.extent.West, this.extent.South, 0);
49593
50193
  const southEast = Cartesian3.fromDegrees(this.extent.East, this.extent.South, 0);
49594
50194
  const width = Cartesian3.distance(southWest, southEast);
49595
- return Math.max(1, width / Math.max(1, this.source ? this.source.width : 1));
50195
+ return Math.max(1, width / Math.max(1, this.sourceDims ? this.sourceDims.width : 1));
49596
50196
  }
49597
50197
  syncRampTexture(context) {
49598
50198
  if (this.rampTexture) {
@@ -49613,6 +50213,67 @@ void main() {
49613
50213
  })
49614
50214
  });
49615
50215
  }
50216
+ valueSampler() {
50217
+ // Nearest for a packed value, because filtering would blend the two bytes of the
50218
+ // packing rather than the values they encode, the same trap sampleGround avoids.
50219
+ const minification = this.packedValue
50220
+ ? TextureMinificationFilter.NEAREST
50221
+ : TextureMinificationFilter.LINEAR;
50222
+ const magnification = this.packedValue
50223
+ ? TextureMagnificationFilter.NEAREST
50224
+ : TextureMagnificationFilter.LINEAR;
50225
+ return new Sampler({
50226
+ wrapS: TextureWrap.CLAMP_TO_EDGE,
50227
+ wrapT: TextureWrap.CLAMP_TO_EDGE,
50228
+ minificationFilter: minification,
50229
+ magnificationFilter: magnification
50230
+ });
50231
+ }
50232
+ /*
50233
+ * Uploads value pixels into a texture, reusing it when the size still fits.
50234
+ */
50235
+ uploadPixels(context, texture, pixels, width, height) {
50236
+ const view = new Uint8Array(pixels.buffer, pixels.byteOffset, pixels.byteLength);
50237
+ const source = { width, height, arrayBufferView: view };
50238
+ if (texture && texture.width === width && texture.height === height) {
50239
+ texture.copyFrom({ source });
50240
+ return texture;
50241
+ }
50242
+ if (texture) {
50243
+ texture.destroy();
50244
+ }
50245
+ return new Texture({
50246
+ context,
50247
+ width,
50248
+ height,
50249
+ pixelFormat: PixelFormat.RGBA,
50250
+ pixelDatatype: PixelDatatype.UNSIGNED_BYTE,
50251
+ source,
50252
+ // Row 0 is the archive's north edge, and has to stay at the top, as the canvas path does.
50253
+ flipY: false,
50254
+ sampler: this.valueSampler()
50255
+ });
50256
+ }
50257
+ /*
50258
+ * Keeps a texture each side of the fade, swapping them on a new frame so only that one is uploaded.
50259
+ */
50260
+ syncFadeTextures(context) {
50261
+ const fade = this.fade;
50262
+ if (fade.to !== this.uploadedTo) {
50263
+ if (fade.from && fade.from === this.uploadedTo && this.texture) {
50264
+ const previous = this.textureFrom;
50265
+ this.textureFrom = this.texture;
50266
+ this.uploadedFrom = this.uploadedTo;
50267
+ this.texture = previous;
50268
+ }
50269
+ this.texture = this.uploadPixels(context, this.texture, fade.to, fade.width, fade.height);
50270
+ this.uploadedTo = fade.to;
50271
+ }
50272
+ if (fade.from && fade.from !== this.uploadedFrom) {
50273
+ this.textureFrom = this.uploadPixels(context, this.textureFrom, fade.from, fade.width, fade.height);
50274
+ this.uploadedFrom = fade.from;
50275
+ }
50276
+ }
49616
50277
  syncTexture(context) {
49617
50278
  const sizeChanged = this.texture
49618
50279
  && (this.texture.width !== this.source.width || this.texture.height !== this.source.height);
@@ -49620,27 +50281,15 @@ void main() {
49620
50281
  if (this.texture) {
49621
50282
  this.texture.destroy();
49622
50283
  }
49623
- // Nearest for a packed value, because filtering would blend the two bytes of the
49624
- // packing rather than the values they encode, the same trap sampleGround avoids.
49625
- const minification = this.packedValue
49626
- ? TextureMinificationFilter.NEAREST
49627
- : TextureMinificationFilter.LINEAR;
49628
- const magnification = this.packedValue
49629
- ? TextureMagnificationFilter.NEAREST
49630
- : TextureMagnificationFilter.LINEAR;
49631
50284
  this.texture = new Texture({
49632
50285
  context,
49633
50286
  source: this.source,
49634
50287
  // Cesium flips canvas rows on upload by default, which would put the archive's
49635
50288
  // row 0 (its north edge) at the south edge and mirror the whole surface.
49636
50289
  flipY: false,
49637
- sampler: new Sampler({
49638
- wrapS: TextureWrap.CLAMP_TO_EDGE,
49639
- wrapT: TextureWrap.CLAMP_TO_EDGE,
49640
- minificationFilter: minification,
49641
- magnificationFilter: magnification
49642
- })
50290
+ sampler: this.valueSampler()
49643
50291
  });
50292
+ this.uploadedTo = null;
49644
50293
  this.textureDirty = false;
49645
50294
  return;
49646
50295
  }
@@ -49703,7 +50352,7 @@ void main() {
49703
50352
  * carry, so the ladder is capped one step above the texel count rather than run to its top.
49704
50353
  */
49705
50354
  tierCeiling() {
49706
- const source = this.source;
50355
+ const source = this.sourceDims;
49707
50356
  if (!source || !source.width || !source.height) {
49708
50357
  return TIERS[TIERS.length - 1];
49709
50358
  }
@@ -49781,6 +50430,10 @@ void main() {
49781
50430
  owner: this,
49782
50431
  uniformMap: {
49783
50432
  u_valueTexture: () => self.texture,
50433
+ u_valueTextureFrom: () => (self.fade && self.fade.from && self.textureFrom
50434
+ ? self.textureFrom
50435
+ : self.texture),
50436
+ u_valueFade: () => (self.fade && self.fade.from ? self.fade.mix : 1),
49784
50437
  u_groundTexture: () => self.groundTexture,
49785
50438
  u_valuePacked: () => (self.packedValue ? 1 : 0),
49786
50439
  u_groundWeight: () => (self.followGround && self.groundPixels ? 1 : 0),
@@ -49818,7 +50471,7 @@ void main() {
49818
50471
  : 0,
49819
50472
  u_coverageCutoff: () => COVERAGE_CUTOFF,
49820
50473
  u_rampTexture: () => self.rampTexture,
49821
- u_texelSize: () => new Cartesian2(1 / self.source.width, 1 / self.source.height),
50474
+ u_texelSize: () => new Cartesian2(1 / self.sourceDims.width, 1 / self.sourceDims.height),
49822
50475
  u_waterOn: () => (self.water ? 1 : 0),
49823
50476
  u_waterWavelength: () => (self.water && self.water.wavelength)
49824
50477
  || DEFAULT_WATER_WAVELENGTH_METRES,
@@ -50190,6 +50843,10 @@ var EntityRenderEnginePolygon;
50190
50843
  syncTextureValueLabels({
50191
50844
  cEntity,
50192
50845
  viewer: params.viewer,
50846
+ surface: drawsDisplacedSurface ? cEntity[DISPLACED_SURFACE_KEY] : null,
50847
+ hover: params.visualRegister
50848
+ ? { register: params.visualRegister, entityId: String(entity.Bruce.ID), menuItemId: params.menuItemId }
50849
+ : undefined,
50193
50850
  settings: overlaySettings(style, entity, params.tags),
50194
50851
  extent: labelArchive
50195
50852
  ? (drawsDisplacedSurface ? archiveExtent(labelArchive.metadata, posses) : drapeExtent)
@@ -50496,6 +51153,7 @@ var EntityRenderEnginePolygon;
50496
51153
  // rendered: null
50497
51154
  rendered: (_c = params.rendered) === null || _c === void 0 ? void 0 : _c.get(entity.Bruce.ID),
50498
51155
  menuItemId: params.menuItemId,
51156
+ visualRegister: params.visualRegister,
50499
51157
  onSeriesDiscovered: params.onSeriesDiscovered
50500
51158
  });
50501
51159
  }
@@ -50933,8 +51591,8 @@ function wantsLabels(style) {
50933
51591
  */
50934
51592
  const LABEL_GRID_COLOR = "rgba(255,255,255,0.55)";
50935
51593
  const LABEL_GRID_WIDTH_PIXELS = 1;
50936
- const LABEL_MIN_SPACING_PIXELS = 78;
50937
- const LABEL_MAX_LABELS = 240;
51594
+ const LABEL_MIN_SPACING_PIXELS = 140;
51595
+ const LABEL_MAX_LABELS = 60;
50938
51596
  function overlaySettings(style, entity, tags) {
50939
51597
  if (!wantsLabels(style)) {
50940
51598
  return undefined;
@@ -50954,6 +51612,7 @@ function overlaySettings(style, entity, tags) {
50954
51612
  */
50955
51613
  function syncTextureValueLabels(params) {
50956
51614
  const { cEntity, viewer, settings, extent, animator, metadata } = params;
51615
+ const surface = params.surface && !params.surface.isDestroyed() ? params.surface : null;
50957
51616
  const existing = cEntity[TEXTURE_VALUE_LABELS_KEY];
50958
51617
  // Offline has no extent to rebuild the overlay from, so an existing one is left running.
50959
51618
  if (params.offline && existing) {
@@ -50972,8 +51631,13 @@ function syncTextureValueLabels(params) {
50972
51631
  West: tile.West, East: tile.East, South: tile.South, North: tile.North,
50973
51632
  ResolutionX: tile.ResolutionX, ResolutionY: tile.ResolutionY
50974
51633
  })),
50975
- clipRing: params.clipRing
51634
+ clipRing: params.clipRing,
51635
+ heightAt: surface ? (lon, lat) => surface.HeightAt(lon, lat) : undefined,
51636
+ heightKey: surface ? () => surface.GetPlacementKey() : undefined,
51637
+ hover: params.hover
50976
51638
  });
51639
+ // A surface keeps no tinted frame, so whether a cell was painted is read off its ramp.
51640
+ labels.SetPaintedRamp(surface ? surface.GetRampPixels() : null);
50977
51641
  let lastVersion = -1;
50978
51642
  let appliedFloor = null;
50979
51643
  let appliedCeiling = null;
@@ -50985,15 +51649,15 @@ function syncTextureValueLabels(params) {
50985
51649
  if (animator.IsDisposed()) {
50986
51650
  return;
50987
51651
  }
50988
- const canvas = animator.GetValueCanvas();
50989
- if (!canvas) {
50990
- return;
50991
- }
50992
51652
  const version = animator.GetPresentVersion();
50993
51653
  if (version !== lastVersion) {
51654
+ // Read from the frame cache, since reading the value canvas back copies a whole frame.
51655
+ const raster = animator.GetValueRaster();
51656
+ if (!raster) {
51657
+ return;
51658
+ }
50994
51659
  lastVersion = version;
50995
- const ctx = canvas.getContext("2d");
50996
- labels.SetSource(ctx.getImageData(0, 0, canvas.width, canvas.height));
51660
+ labels.SetSource(raster);
50997
51661
  labels.SetPainted(animator.GetDisplayedPixels());
50998
51662
  }
50999
51663
  // Tracked by reference rather than latched on the first one to arrive: a tiled archive
@@ -52732,7 +53396,7 @@ var StyleUtils;
52732
53396
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
52733
53397
  })(StyleUtils || (StyleUtils = {}));
52734
53398
 
52735
- const VERSION = "7.5.8";
53399
+ const VERSION = "7.5.9";
52736
53400
  /**
52737
53401
  * Updates the environment instance used by bruce-cesium to one specified.
52738
53402
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.