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.
@@ -7295,7 +7295,8 @@
7295
7295
  this.groupId = groupId;
7296
7296
  this.additionalTexts = this.options.text != null ? [] : null;
7297
7297
  this.photo = null;
7298
- this.createLabel();
7298
+ // A label given its position keeps it, or the rebuild goes looking for the entity's own.
7299
+ this.createLabel(this.prePositioned ? this._pos3d : undefined);
7299
7300
  }
7300
7301
  /**
7301
7302
  * Creates cesium entity for the label if one does not exist.
@@ -13789,7 +13790,9 @@
13789
13790
  entityId: params.entityId,
13790
13791
  menuItemId: params.menuItemId,
13791
13792
  position: params.position ? { ...params.position } : undefined,
13792
- createIfMissing: params.createIfMissing !== false
13793
+ createIfMissing: params.createIfMissing !== false,
13794
+ text: params.text,
13795
+ pos3d: params.pos3d ? Cesium.Cartesian3.clone(params.pos3d) : undefined
13793
13796
  };
13794
13797
  const doc = this.viewer.container.ownerDocument;
13795
13798
  // Only a label following the cursor should go when the cursor leaves the canvas.
@@ -13805,6 +13808,13 @@
13805
13808
  this.SyncLabelHover();
13806
13809
  this.viewer.scene.requestRender();
13807
13810
  }
13811
+ /*
13812
+ * Which Entity holds the label hover, if any, so a caller can leave another's hover alone.
13813
+ */
13814
+ GetLabelHoverTarget() {
13815
+ const hover = this.labelHover;
13816
+ return hover ? { entityId: hover.entityId, menuItemId: hover.menuItemId } : undefined;
13817
+ }
13808
13818
  /*
13809
13819
  * Whether the label hover is on this Entity, with or without a cursor position.
13810
13820
  */
@@ -13817,7 +13827,7 @@
13817
13827
  return (hover === null || hover === void 0 ? void 0 : hover.entityId) === entityId && (hover.menuItemId == null || hover.menuItemId === menuItemId) ? hover.position : undefined;
13818
13828
  }
13819
13829
  SyncLabelHover() {
13820
- var _a, _b, _c;
13830
+ var _a, _b, _c, _d;
13821
13831
  const hover = this.labelHover;
13822
13832
  if (!hover || this.disposed) {
13823
13833
  return;
@@ -13849,12 +13859,24 @@
13849
13859
  this.ReleaseHoverLabel(true);
13850
13860
  return;
13851
13861
  }
13862
+ const hoverOptions = {
13863
+ presentation: "text",
13864
+ text: (_b = hover.text) !== null && _b !== void 0 ? _b : group === null || group === void 0 ? void 0 : group.options.text,
13865
+ selectOnClick: false
13866
+ };
13852
13867
  if (this.hoverLabel && !this.hoverLabel.Disposed) {
13868
+ // Rebuilds only when the text changed, as it does when the pointer crosses a cell.
13869
+ if (hover.pos3d && !Cesium.Cartesian3.equals(hover.pos3d, this.hoverLabel.pos3d)) {
13870
+ this.hoverLabel.pos3d = hover.pos3d;
13871
+ }
13872
+ if (hover.text != null) {
13873
+ this.hoverLabel.Configure(hoverOptions);
13874
+ }
13853
13875
  this.hoverLabel.SetHoverVisible(true);
13854
13876
  return;
13855
13877
  }
13856
13878
  // Query-only icon groups have coordinates but no registered scene visual to attach to.
13857
- if (!rego && group && [group.item.longitude, group.item.latitude, (_b = group.item.altitude) !== null && _b !== void 0 ? _b : 0].every(Number.isFinite)) {
13879
+ if (!rego && group && [group.item.longitude, group.item.latitude, (_c = group.item.altitude) !== null && _c !== void 0 ? _c : 0].every(Number.isFinite)) {
13858
13880
  rego = {
13859
13881
  entityId: hover.entityId,
13860
13882
  menuItemId: hover.menuItemId,
@@ -13876,15 +13898,12 @@
13876
13898
  rego,
13877
13899
  viewer: this.viewer,
13878
13900
  visualRegister: this,
13879
- api: (_c = this.ApiGetters) === null || _c === void 0 ? void 0 : _c.GetBruceApi(),
13901
+ api: (_d = this.ApiGetters) === null || _d === void 0 ? void 0 : _d.GetBruceApi(),
13880
13902
  persistent: false,
13881
13903
  transient: true,
13882
13904
  hoverRingState: this.hoverRingState,
13883
- options: {
13884
- presentation: "text",
13885
- text: group === null || group === void 0 ? void 0 : group.options.text,
13886
- selectOnClick: false
13887
- },
13905
+ pos3d: hover.pos3d,
13906
+ options: hoverOptions,
13888
13907
  visible: () => {
13889
13908
  var _a;
13890
13909
  const current = this.GetState(hover);
@@ -46708,6 +46727,10 @@
46708
46727
  TextureFrameSeriesAnimator.AppearanceSignature = AppearanceSignature;
46709
46728
  const MAX_COMPOSITE_TEXELS = 2048;
46710
46729
  const VALUE_DILATE_PASSES = 2;
46730
+ // Decoded frame bytes kept at once. A 2048 square composite is 16 MB, so a long series cannot all stay.
46731
+ const MAX_CACHED_FRAME_BYTES = 256 * 1024 * 1024;
46732
+ // Fetched bytes kept for decoding again, a small fraction of what they decode to.
46733
+ const MAX_RAW_FRAME_BYTES = 96 * 1024 * 1024;
46711
46734
  /*
46712
46735
  * Rewrites a packed 16 bit value as the 8 bit grey the colour ramps index on.
46713
46736
  *
@@ -46728,19 +46751,33 @@
46728
46751
  /*
46729
46752
  * Bleeds covered values outward into uncovered texels, leaving alpha untouched.
46730
46753
  */
46731
- function dilateValues(value, width, height, passes, packed) {
46754
+ function DilateValues(value, width, height, passes, packed) {
46732
46755
  const texels = width * height;
46733
- const filled = new Uint8Array(texels);
46756
+ // 255 for a texel that had data to begin with, otherwise the 1 based pass that filled it, so a pass
46757
+ // reads only texels filled before it and can work in place.
46758
+ const stamp = new Uint8Array(texels);
46734
46759
  for (let p = 0; p < texels; p++) {
46735
- filled[p] = value[p * 4 + 3] !== 0 ? 1 : 0;
46760
+ stamp[p] = value[p * 4 + 3] !== 0 ? 255 : 0;
46736
46761
  }
46762
+ // Whether a texel or a horizontal neighbour had data, so empty space is skipped in three reads.
46763
+ const across = new Uint8Array(texels);
46737
46764
  for (let pass = 0; pass < passes; pass++) {
46738
- const source = value.slice();
46739
- const wasFilled = filled.slice();
46765
+ const current = pass + 1;
46766
+ const had = (p) => stamp[p] !== 0 && stamp[p] !== current;
46767
+ for (let y = 0; y < height; y++) {
46768
+ const row = y * width;
46769
+ for (let x = 0; x < width; x++) {
46770
+ const p = row + x;
46771
+ across[p] = (had(p) || (x > 0 && had(p - 1)) || (x + 1 < width && had(p + 1))) ? 1 : 0;
46772
+ }
46773
+ }
46740
46774
  for (let y = 0; y < height; y++) {
46741
46775
  for (let x = 0; x < width; x++) {
46742
46776
  const p = y * width + x;
46743
- if (wasFilled[p]) {
46777
+ if (stamp[p] !== 0) {
46778
+ continue;
46779
+ }
46780
+ if (!across[p] && !(y > 0 && across[p - width]) && !(y + 1 < height && across[p + width])) {
46744
46781
  continue;
46745
46782
  }
46746
46783
  let sum = 0;
@@ -46749,13 +46786,13 @@
46749
46786
  for (let dx = -1; dx <= 1; dx++) {
46750
46787
  const nx = x + dx;
46751
46788
  const ny = y + dy;
46752
- if (nx < 0 || ny < 0 || nx >= width || ny >= height || !wasFilled[ny * width + nx]) {
46789
+ if (nx < 0 || ny < 0 || nx >= width || ny >= height || !had(ny * width + nx)) {
46753
46790
  continue;
46754
46791
  }
46755
46792
  const at = (ny * width + nx) * 4;
46756
46793
  // Averaged as a value rather than per byte: averaging a packed low byte
46757
46794
  // on its own wraps at every 256th step and speckles the bled edge.
46758
- sum += packed ? source[at] * 256 + source[at + 1] : source[at];
46795
+ sum += packed ? value[at] * 256 + value[at + 1] : value[at];
46759
46796
  hits++;
46760
46797
  }
46761
46798
  }
@@ -46771,18 +46808,25 @@
46771
46808
  value[p * 4 + 1] = v;
46772
46809
  value[p * 4 + 2] = v;
46773
46810
  }
46774
- filled[p] = 1;
46811
+ stamp[p] = current;
46775
46812
  }
46776
46813
  }
46777
46814
  }
46778
46815
  }
46779
46816
  }
46817
+ TextureFrameSeriesAnimator.DilateValues = DilateValues;
46818
+ /*
46819
+ * Bytes one cached frame holds, counting a buffer shared between colour and value once.
46820
+ */
46821
+ function frameBytes(pixels, value) {
46822
+ return (pixels ? pixels.byteLength : 0) + (value && value !== pixels ? value.byteLength : 0);
46823
+ }
46780
46824
  const DEFAULT_CROSSFADE_MS = 900;
46781
46825
  const DEFAULT_LOW_COLOR = { red: 255, green: 255, blue: 255, alpha: 0.12 };
46782
46826
  const DEFAULT_HIGH_COLOR = { red: 21, green: 96, blue: 196, alpha: 0.92 };
46783
46827
  class Animator {
46784
46828
  constructor(options) {
46785
- var _a, _b, _c;
46829
+ var _a, _b, _c, _d;
46786
46830
  this.removeOnTick = null;
46787
46831
  this.removeCrossfadeTick = null;
46788
46832
  this.disposed = false;
@@ -46813,6 +46857,15 @@
46813
46857
  this.extremesLoad = null;
46814
46858
  this.floorPixels = null;
46815
46859
  this.ceilingPixels = null;
46860
+ // How far through the fade from the previous value frame to the presented one, 1 when settled.
46861
+ this.fadeMix = 1;
46862
+ // Where the current fade started from, above zero when it took over from one still running.
46863
+ this.fadeMixStart = 0;
46864
+ // Version the value canvas was last written at, since in value only mode it is written on demand.
46865
+ this.valueCanvasVersion = -1;
46866
+ this.rawBytes = 0;
46867
+ this.cachedBytes = 0;
46868
+ this.lastFrameBytes = 0;
46816
46869
  if (!options.entity.polygon) {
46817
46870
  throw new Error("TextureFrameSeriesAnimator requires an entity with polygon graphics.");
46818
46871
  }
@@ -46860,6 +46913,9 @@
46860
46913
  this.maskBaseline = (_c = options.maskBaseline) !== null && _c !== void 0 ? _c : this.tiles.some((tile) => Boolean(tile.BaselineMask));
46861
46914
  this.valueCanvas = this.produceValueCanvas ? document.createElement("canvas") : null;
46862
46915
  this.driveMaterial = options.driveMaterial !== false;
46916
+ this.valueOnly = this.produceValueCanvas && !this.driveMaterial;
46917
+ this.rawCache = new Array(this.frames.length).fill(null);
46918
+ this.cacheBudget = (_d = options.cacheBytes) !== null && _d !== void 0 ? _d : MAX_CACHED_FRAME_BYTES;
46863
46919
  this.originalMaterial = options.entity.polygon.material;
46864
46920
  this.imageProperty = new Cesium.CallbackProperty(() => this.pool[this.poolIdx], false);
46865
46921
  if (this.driveMaterial) {
@@ -46925,7 +46981,40 @@
46925
46981
  * or null unless the instance was constructed with produceValueCanvas.
46926
46982
  */
46927
46983
  GetValueCanvas() {
46928
- return this.valueDims ? this.valueCanvas : null;
46984
+ if (!this.valueDims) {
46985
+ return null;
46986
+ }
46987
+ if (this.valueOnly && this.valueCanvasVersion !== this.presentVersion && this.displayedValuePixels) {
46988
+ this.writeInto(this.valueCanvas, this.displayedValuePixels, this.valueDims);
46989
+ this.valueCanvasVersion = this.presentVersion;
46990
+ }
46991
+ return this.valueCanvas;
46992
+ }
46993
+ /**
46994
+ * The presented frame's value pixels, or null before one. Shares the cached array, so never write to it.
46995
+ */
46996
+ GetValueRaster() {
46997
+ if (!this.valueDims || !this.displayedValuePixels) {
46998
+ return null;
46999
+ }
47000
+ return { data: this.displayedValuePixels, width: this.valueDims.width, height: this.valueDims.height };
47001
+ }
47002
+ /**
47003
+ * The two value frames being faded between and the mix, or null unless the material is not driven.
47004
+ */
47005
+ GetValueFade() {
47006
+ if (!this.valueOnly || !this.valueDims || !this.displayedValuePixels) {
47007
+ return null;
47008
+ }
47009
+ const previous = this.fadeFromValuePixels;
47010
+ const from = previous && previous.length === this.displayedValuePixels.length ? previous : null;
47011
+ return {
47012
+ from,
47013
+ to: this.displayedValuePixels,
47014
+ width: this.valueDims.width,
47015
+ height: this.valueDims.height,
47016
+ mix: from ? this.fadeMix : 1
47017
+ };
46929
47018
  }
46930
47019
  /**
46931
47020
  * Increments every time a frame is presented, so a consumer holding the value canvas can tell
@@ -46992,7 +47081,11 @@
46992
47081
  if (this.currentFrameIndex < 0) {
46993
47082
  return this.frameCache[0] ? -1 : 0;
46994
47083
  }
46995
- for (let d = 0; d < n; d++) {
47084
+ // Full once another frame will not fit, so a prefetch only displaces a frame further from the
47085
+ // playhead than itself and never trades the two ends of the window back and forth.
47086
+ const full = this.cachedBytes + this.lastFrameBytes > this.cacheBudget;
47087
+ const reach = full ? this.farthestCachedDistance() : n;
47088
+ for (let d = 0; d < Math.min(n, reach); d++) {
46996
47089
  const forward = this.currentFrameIndex + d;
46997
47090
  if (forward < n && !this.frameCache[forward]) {
46998
47091
  return forward;
@@ -47004,6 +47097,68 @@
47004
47097
  }
47005
47098
  return -1;
47006
47099
  }
47100
+ farthestCachedDistance() {
47101
+ let farthest = 0;
47102
+ for (let i = 0; i < this.frameCache.length; i++) {
47103
+ if (this.frameCache[i]) {
47104
+ farthest = Math.max(farthest, Math.abs(i - this.currentFrameIndex));
47105
+ }
47106
+ }
47107
+ return farthest;
47108
+ }
47109
+ /*
47110
+ * Drops decoded frames furthest from the playhead until the cache fits its budget again.
47111
+ */
47112
+ evictDecoded(keep) {
47113
+ // A tinted fade reads its target from the cache each tick, where a value fade holds the arrays.
47114
+ const fading = !this.valueOnly && this.fadeStartMs !== null ? this.fadeToIndex : -1;
47115
+ while (this.cachedBytes > this.cacheBudget) {
47116
+ let victim = -1;
47117
+ let distance = -1;
47118
+ for (let i = 0; i < this.frameCache.length; i++) {
47119
+ if (!this.frameCache[i] || i === keep || i === this.currentFrameIndex || i === fading) {
47120
+ continue;
47121
+ }
47122
+ const d = Math.abs(i - this.currentFrameIndex);
47123
+ if (d > distance) {
47124
+ distance = d;
47125
+ victim = i;
47126
+ }
47127
+ }
47128
+ if (victim < 0) {
47129
+ return;
47130
+ }
47131
+ this.cachedBytes -= frameBytes(this.frameCache[victim], this.valueCache[victim]);
47132
+ this.frameCache[victim] = null;
47133
+ this.valueCache[victim] = null;
47134
+ }
47135
+ }
47136
+ /*
47137
+ * Keeps a frame's fetched bytes unless they are large, as a tile major archive's are.
47138
+ */
47139
+ keepRaw(idx, buffer, base) {
47140
+ if (buffer.byteLength > MAX_RAW_FRAME_BYTES / 16) {
47141
+ return;
47142
+ }
47143
+ this.rawCache[idx] = { buffer, base };
47144
+ this.rawBytes += buffer.byteLength;
47145
+ while (this.rawBytes > MAX_RAW_FRAME_BYTES) {
47146
+ let victim = -1;
47147
+ let distance = -1;
47148
+ for (let i = 0; i < this.rawCache.length; i++) {
47149
+ const d = Math.abs(i - this.currentFrameIndex);
47150
+ if (this.rawCache[i] && i !== idx && d > distance) {
47151
+ distance = d;
47152
+ victim = i;
47153
+ }
47154
+ }
47155
+ if (victim < 0) {
47156
+ return;
47157
+ }
47158
+ this.rawBytes -= this.rawCache[victim].buffer.byteLength;
47159
+ this.rawCache[victim] = null;
47160
+ }
47161
+ }
47007
47162
  pumpFetchQueue() {
47008
47163
  if (this.disposed || this.inFlightIndex !== -1) {
47009
47164
  return;
@@ -47047,10 +47202,18 @@
47047
47202
  lo = Math.min(lo, at.ByteOffset);
47048
47203
  hi = Math.max(hi, at.ByteOffset + at.ByteLength);
47049
47204
  }
47050
- const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
47051
- const buffer = await response.arrayBuffer();
47052
- if (this.disposed) {
47053
- return;
47205
+ let buffer;
47206
+ const raw = this.rawCache[idx];
47207
+ if (raw && raw.base === lo) {
47208
+ buffer = raw.buffer;
47209
+ }
47210
+ else {
47211
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
47212
+ buffer = await response.arrayBuffer();
47213
+ if (this.disposed) {
47214
+ return;
47215
+ }
47216
+ this.keepRaw(idx, buffer, lo);
47054
47217
  }
47055
47218
  // Awaited before decoding rather than applied later, so no frame is ever cached unmasked and
47056
47219
  // then corrected once the mask lands.
@@ -47062,9 +47225,15 @@
47062
47225
  if (this.disposed) {
47063
47226
  return;
47064
47227
  }
47228
+ if (this.frameCache[idx]) {
47229
+ this.cachedBytes -= frameBytes(this.frameCache[idx], this.valueCache[idx]);
47230
+ }
47065
47231
  this.frameCache[idx] = decoded.pixels;
47066
47232
  this.frameDims[idx] = { width: decoded.width, height: decoded.height };
47067
47233
  this.valueCache[idx] = decoded.valuePixels || null;
47234
+ this.lastFrameBytes = frameBytes(decoded.pixels, decoded.valuePixels || null);
47235
+ this.cachedBytes += this.lastFrameBytes;
47236
+ this.evictDecoded(idx);
47068
47237
  if (this.shouldPresent(idx, isTarget)) {
47069
47238
  this.beginCrossfadeTo(idx);
47070
47239
  }
@@ -47134,6 +47303,13 @@
47134
47303
  */
47135
47304
  tint(imageData) {
47136
47305
  const canvas = { width: imageData.width, height: imageData.height };
47306
+ if (this.valueOnly) {
47307
+ // One array serves as both, since nothing will ever display the colour.
47308
+ const values = imageData.data;
47309
+ this.applyBaselineMask(values, undefined, canvas.width, canvas.height);
47310
+ DilateValues(values, canvas.width, canvas.height, VALUE_DILATE_PASSES, IsPackedValue(this.metadata));
47311
+ return { pixels: values, width: canvas.width, height: canvas.height, valuePixels: values };
47312
+ }
47137
47313
  // Copied before tinting: the ramp is a lerp between two colours, so the value cannot be
47138
47314
  // recovered from the tinted pixels afterwards.
47139
47315
  const valuePixels = this.produceValueCanvas ? new Uint8ClampedArray(imageData.data) : undefined;
@@ -47154,7 +47330,7 @@
47154
47330
  }
47155
47331
  this.applyBaselineMask(imageData.data, valuePixels, canvas.width, canvas.height);
47156
47332
  if (valuePixels) {
47157
- dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES, IsPackedValue(this.metadata));
47333
+ DilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES, IsPackedValue(this.metadata));
47158
47334
  }
47159
47335
  return { pixels: imageData.data, width: canvas.width, height: canvas.height, valuePixels };
47160
47336
  }
@@ -47412,6 +47588,10 @@
47412
47588
  }
47413
47589
  }
47414
47590
  beginCrossfadeTo(idx) {
47591
+ if (this.valueOnly) {
47592
+ this.beginValueFadeTo(idx);
47593
+ return;
47594
+ }
47415
47595
  const to = this.frameCache[idx];
47416
47596
  this.fadeFromValuePixels = this.displayedValuePixels === null && this.valueCache[idx]
47417
47597
  ? new Uint8ClampedArray(this.valueCache[idx].length)
@@ -47421,12 +47601,53 @@
47421
47601
  this.fadeToDims = this.frameDims[idx];
47422
47602
  this.fadeStartMs = Now();
47423
47603
  }
47604
+ /*
47605
+ * Presents a value frame at once and leaves the fade to the consumer, which blends on the GPU.
47606
+ */
47607
+ beginValueFadeTo(idx) {
47608
+ const to = this.valueCache[idx];
47609
+ if (!to || to === this.displayedValuePixels) {
47610
+ return;
47611
+ }
47612
+ // A new target mid fade keeps whichever frame is mostly on screen and carries on from the current
47613
+ // mix, so frames landing in quick succession do not jump the sheet back.
47614
+ const midFade = this.fadeStartMs !== null && Boolean(this.fadeFromValuePixels);
47615
+ if (midFade && this.fadeMix < 0.5) {
47616
+ this.fadeMixStart = this.fadeMix;
47617
+ }
47618
+ else {
47619
+ this.fadeFromValuePixels = this.displayedValuePixels;
47620
+ this.fadeMixStart = 0;
47621
+ }
47622
+ this.displayedValuePixels = to;
47623
+ this.valueDims = this.frameDims[idx];
47624
+ this.fadeToIndex = idx;
47625
+ this.fadeMix = this.fadeFromValuePixels ? this.fadeMixStart : 1;
47626
+ this.fadeStartMs = this.fadeFromValuePixels ? Now() : null;
47627
+ this.presentVersion++;
47628
+ this.requestRender();
47629
+ }
47630
+ requestRender() {
47631
+ if (this.viewer && this.viewer.scene && this.viewer.scene.requestRenderMode) {
47632
+ this.viewer.scene.requestRender();
47633
+ }
47634
+ }
47424
47635
  tickCrossfade() {
47425
47636
  if (this.disposed || this.fadeStartMs === null) {
47426
47637
  return;
47427
47638
  }
47428
47639
  const t = Math.min(1, (Now() - this.fadeStartMs) / this.crossfadeMs);
47429
47640
  const eased = easeInOutQuad(t);
47641
+ if (this.valueOnly) {
47642
+ this.fadeMix = this.fadeMixStart + (1 - this.fadeMixStart) * eased;
47643
+ if (t >= 1) {
47644
+ this.fadeStartMs = null;
47645
+ this.fadeFromValuePixels = null;
47646
+ this.fadeMix = 1;
47647
+ }
47648
+ this.requestRender();
47649
+ return;
47650
+ }
47430
47651
  const from = this.fadeFromPixels;
47431
47652
  const to = this.frameCache[this.fadeToIndex];
47432
47653
  const n = Math.min(from.length, to.length);
@@ -47466,9 +47687,7 @@
47466
47687
  this.valueDims = dims;
47467
47688
  }
47468
47689
  this.presentVersion++;
47469
- if (this.viewer && this.viewer.scene && this.viewer.scene.requestRenderMode) {
47470
- this.viewer.scene.requestRender();
47471
- }
47690
+ this.requestRender();
47472
47691
  }
47473
47692
  }
47474
47693
  TextureFrameSeriesAnimator.Animator = Animator;
@@ -47487,13 +47706,15 @@
47487
47706
  const MIN_VISIBLE_PIXELS = 60;
47488
47707
  const MAX_STRIDE = 512;
47489
47708
  const STRIDE_HYSTERESIS = 1.4;
47490
- const DEFAULT_MIN_SPACING_PIXELS = 78;
47491
- const DEFAULT_MAX_LABELS = 240;
47492
- const LABEL_RADIUS = 4;
47493
- const LABEL_FONT = "600 11px ui-monospace, SFMono-Regular, Menlo, monospace";
47494
- // Clear space either side of a label box, so neighbours read as separate.
47495
- const LABEL_GAP_PIXELS = 16;
47496
- const LABEL_ANCHOR_COLOR = "rgba(255,255,255,0.85)";
47709
+ // Scene labels carry a leader and are wider than a value, so they want room and not many of them.
47710
+ const DEFAULT_MIN_SPACING_PIXELS = 140;
47711
+ const DEFAULT_MAX_LABELS = 60;
47712
+ // How far past the edge of the view an anchor may sit before its label is dropped.
47713
+ const LABEL_EDGE_MARGIN_PIXELS = 120;
47714
+ // Distance discount for a label already up when the cap forces a choice.
47715
+ const LABEL_KEEP_BIAS = 0.6;
47716
+ // How long a label outlives its cell leaving the wanted set, so one back a frame later does not blink.
47717
+ const LABEL_LINGER_MS = 300;
47497
47718
  // 8 rather than 5: below this a cell reads as texture rather than as a boundary, and it also
47498
47719
  // bounds the worst case, since a full screen of 8 px cells fits inside MAX_GRID_CELLS.
47499
47720
  const DEFAULT_MIN_GRID_CELL_PIXELS = 8;
@@ -47507,7 +47728,8 @@
47507
47728
  const TILE_OUTLINE_ALPHA = 0.45;
47508
47729
  // Cells per grid square at the coarsest. Beyond this a tile is outlined instead.
47509
47730
  const MAX_GRID_STRIDE = 256;
47510
- const LINE_HEIGHT = 13;
47731
+ // Times the pointer's ground hit is moved onto the sheet's own height before reading a cell.
47732
+ const HOVER_REFINE_STEPS = 3;
47511
47733
  /**
47512
47734
  * A clip outline from a projected ring, or null when the ring cannot be drawn.
47513
47735
  * @param projected one entry per ring vertex, null where the vertex has no window coordinate
@@ -47601,11 +47823,25 @@
47601
47823
  this.sourceMap = null;
47602
47824
  this.source = null;
47603
47825
  this.painted = null;
47826
+ this.paintedRamp = null;
47604
47827
  this.floor = null;
47605
47828
  this.ceiling = null;
47606
47829
  this.disposed = false;
47607
- this.drawn = 0;
47608
47830
  this.stride = 0;
47831
+ this.cellLabels = new Map();
47832
+ this.lingerTimer = null;
47833
+ // What the last draw was made from, so a frame that changes none of it draws nothing.
47834
+ this.drawnView = null;
47835
+ this.drawnInputs = [];
47836
+ this.scratchWorld = new Cesium.Cartesian3();
47837
+ this.scratchWindow = new Cesium.Cartesian2();
47838
+ this.scratchClip = new Cesium.Cartesian4();
47839
+ // The camera's view and projection as one matrix, rebuilt per draw, or null outside 3D.
47840
+ this.viewProjection = null;
47841
+ this.viewProjectionScratch = new Cesium.Matrix4();
47842
+ this.hovering = false;
47843
+ this.hoverFrame = 0;
47844
+ this.hoverAt = null;
47609
47845
  this.viewer = options.viewer;
47610
47846
  this.extent = options.extent;
47611
47847
  this.settings = options.settings || {};
@@ -47615,6 +47851,9 @@
47615
47851
  this.sourceMap = exports.TextureFrameSeriesAnimator.SourceMap(options.metadata);
47616
47852
  this.tiles = options.tiles;
47617
47853
  this.clipRing = options.clipRing;
47854
+ this.heightAt = options.heightAt;
47855
+ this.heightKey = options.heightKey;
47856
+ this.hover = options.hover;
47618
47857
  this.canvas = document.createElement("canvas");
47619
47858
  this.canvas.style.position = "absolute";
47620
47859
  this.canvas.style.inset = "0";
@@ -47624,12 +47863,31 @@
47624
47863
  parent.appendChild(this.canvas);
47625
47864
  }
47626
47865
  this.ctx = this.canvas.getContext("2d");
47866
+ this.onPointerMove = (event) => {
47867
+ const bounds = this.viewer.canvas.getBoundingClientRect();
47868
+ this.hoverAt = { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
47869
+ // At most once a frame, since a pointer reports far faster than anything is drawn.
47870
+ if (!this.hoverFrame) {
47871
+ this.hoverFrame = requestAnimationFrame(() => {
47872
+ this.hoverFrame = 0;
47873
+ this.updateHover();
47874
+ });
47875
+ }
47876
+ };
47877
+ this.onPointerLeave = () => {
47878
+ this.hoverAt = null;
47879
+ this.releaseHover();
47880
+ };
47881
+ if (this.hover && this.hover.register) {
47882
+ this.viewer.canvas.addEventListener("pointermove", this.onPointerMove, { passive: true });
47883
+ this.viewer.canvas.addEventListener("pointerleave", this.onPointerLeave);
47884
+ }
47627
47885
  }
47628
47886
  SetExtent(extent) {
47629
47887
  this.extent = extent;
47630
47888
  }
47631
47889
  /**
47632
- * The current frame's decoded values, as the animator's value canvas already provides them.
47890
+ * The current frame's decoded values.
47633
47891
  * @param source value pixels, one texel per cell
47634
47892
  */
47635
47893
  SetSource(source) {
@@ -47637,11 +47895,17 @@
47637
47895
  }
47638
47896
  /**
47639
47897
  * The tinted pixels of the same frame, so the grid can skip cells the ramp painted nothing in.
47640
- * @param painted RGBA of the presented frame, or null to fall back to coverage alone
47898
+ * @param painted RGBA of the presented frame, or null to fall back to the ramp or coverage alone
47641
47899
  */
47642
47900
  SetPainted(painted) {
47643
47901
  this.painted = painted;
47644
47902
  }
47903
+ /**
47904
+ * The surface's 256 step RGBA ramp, for telling whether a cell was painted without a tinted frame.
47905
+ */
47906
+ SetPaintedRamp(ramp) {
47907
+ this.paintedRamp = ramp;
47908
+ }
47645
47909
  /**
47646
47910
  * The archive's per-cell extremes, when it publishes them. Without these a label can still
47647
47911
  * print the current reading, just not the range behind it.
@@ -47651,21 +47915,28 @@
47651
47915
  this.ceiling = ceiling;
47652
47916
  }
47653
47917
  GetDrawnCount() {
47654
- return this.drawn;
47918
+ return this.cellLabels.size;
47655
47919
  }
47656
47920
  Clear() {
47657
47921
  this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
47658
- this.drawn = 0;
47922
+ this.drawnView = null;
47659
47923
  }
47660
47924
  Dispose() {
47661
47925
  this.disposed = true;
47926
+ this.clearCellLabels();
47927
+ this.releaseHover();
47928
+ if (this.hoverFrame) {
47929
+ cancelAnimationFrame(this.hoverFrame);
47930
+ this.hoverFrame = 0;
47931
+ }
47932
+ this.viewer.canvas.removeEventListener("pointermove", this.onPointerMove);
47933
+ this.viewer.canvas.removeEventListener("pointerleave", this.onPointerLeave);
47662
47934
  if (this.canvas.parentElement) {
47663
47935
  this.canvas.parentElement.removeChild(this.canvas);
47664
47936
  }
47665
47937
  }
47666
47938
  /**
47667
- * Redraws every visible label. Cheap enough to call per rendered frame, because the cell
47668
- * count it considers is bounded by the view rather than by the texture.
47939
+ * Redraws the grid and syncs the value labels, when anything they are drawn from has changed.
47669
47940
  */
47670
47941
  Render() {
47671
47942
  var _a, _b, _c;
@@ -47673,67 +47944,296 @@
47673
47944
  return;
47674
47945
  }
47675
47946
  const view = this.viewer.canvas;
47947
+ const inputs = [this.source, this.painted, this.paintedRamp, this.floor, this.ceiling,
47948
+ view.clientWidth, view.clientHeight, this.heightKey ? this.heightKey() : null];
47949
+ const camera = this.viewer.camera.viewMatrix;
47950
+ if (this.drawnView &&
47951
+ Cesium.Matrix4.equals(this.drawnView, camera) &&
47952
+ inputs.every((input, i) => input === this.drawnInputs[i])) {
47953
+ return;
47954
+ }
47676
47955
  if (this.canvas.width !== view.clientWidth || this.canvas.height !== view.clientHeight) {
47677
47956
  this.canvas.width = view.clientWidth;
47678
47957
  this.canvas.height = view.clientHeight;
47679
47958
  }
47680
47959
  this.Clear();
47960
+ this.drawnView = Cesium.Matrix4.clone(camera, this.drawnView || undefined);
47961
+ this.drawnInputs = inputs;
47962
+ const projection = this.viewer.camera.frustum.projectionMatrix;
47963
+ this.viewProjection = this.viewer.scene.mode === Cesium.SceneMode.SCENE3D
47964
+ ? Cesium.Matrix4.multiply(projection, camera, this.viewProjectionScratch)
47965
+ : null;
47681
47966
  const cols = this.source.width;
47682
47967
  const rows = this.source.height;
47683
47968
  const window = this.visibleTexels(cols, rows);
47684
47969
  if (!window) {
47970
+ this.syncCellLabels([]);
47685
47971
  return;
47686
47972
  }
47687
- const midCol = (window.col0 + window.col1) / 2;
47688
- const midRow = (window.row0 + window.row1) / 2;
47689
- const a = this.project(this.lonAt(midCol, cols), this.latAt(midRow, rows));
47690
- const b = this.project(this.lonAt(midCol + 1, cols), this.latAt(midRow + 1, rows));
47973
+ // Measured where the view is looking, since in an oblique view up close the middle of the visible
47974
+ // texels sits out toward the horizon, where cells are a fraction of the size.
47975
+ const focus = this.groundAtScreenCentre();
47976
+ const at = focus ? this.texelAt(focus.lon, focus.lat) : null;
47977
+ const focused = (Boolean(at) &&
47978
+ at.col >= window.col0 &&
47979
+ at.col <= window.col1 &&
47980
+ at.row >= window.row0 &&
47981
+ at.row <= window.row1);
47982
+ const midCol = focused ? at.col : (window.col0 + window.col1) / 2;
47983
+ const midRow = focused ? at.row : (window.row0 + window.row1) / 2;
47984
+ const a = this.projectCell(midCol, midRow, cols, rows);
47985
+ const b = this.projectCell(midCol + 1, midRow + 1, cols, rows);
47691
47986
  if (!a || !b) {
47987
+ // Left as they are, since one frame where the focus will not project is not the data leaving.
47692
47988
  return;
47693
47989
  }
47694
47990
  const cellPixels = Math.min(Math.abs(b.x - a.x), Math.abs(b.y - a.y));
47695
47991
  // Ahead of the label threshold on purpose: a grid still reads at a cell size where a
47696
47992
  // number no longer fits.
47697
47993
  this.drawGrid(window, cols, rows, cellPixels);
47698
- if (cellPixels < ((_a = this.settings.minCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_CELL_PIXELS)) {
47994
+ // By area rather than the shorter side, which an oblique view foreshortens to a fraction of the other.
47995
+ const across = this.projectCell(midCol + 1, midRow, cols, rows);
47996
+ const down = this.projectCell(midCol, midRow + 1, cols, rows);
47997
+ const cellSize = across && down
47998
+ ? Math.sqrt(Math.abs((across.x - a.x) * (down.y - a.y) - (across.y - a.y) * (down.x - a.x)))
47999
+ : cellPixels;
48000
+ const visibleWidth = cellSize * (window.col1 - window.col0 + 1);
48001
+ const visibleHeight = cellSize * (window.row1 - window.row0 + 1);
48002
+ if (cellSize < ((_a = this.settings.minCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_CELL_PIXELS) ||
48003
+ Math.max(visibleWidth, visibleHeight) < MIN_VISIBLE_PIXELS) {
48004
+ this.syncCellLabels([]);
47699
48005
  return;
47700
48006
  }
47701
- const visibleWidth = cellPixels * (window.col1 - window.col0 + 1);
47702
- const visibleHeight = cellPixels * (window.row1 - window.row0 + 1);
47703
- if (Math.max(visibleWidth, visibleHeight) < MIN_VISIBLE_PIXELS) {
47704
- return;
47705
- }
47706
- const parts = this.settings.parts || ["value", "floor", "ceiling"];
47707
- this.ctx.font = LABEL_FONT;
47708
- this.ctx.textAlign = "center";
47709
48007
  const maxLabels = (_b = this.settings.maxLabels) !== null && _b !== void 0 ? _b : DEFAULT_MAX_LABELS;
47710
48008
  this.stride = ChooseLabelStride({
47711
- cellPixels,
48009
+ cellPixels: cellSize,
47712
48010
  visibleCols: window.col1 - window.col0 + 1,
47713
48011
  visibleRows: window.row1 - window.row0 + 1,
47714
48012
  maxLabels,
47715
- // Measured, not assumed: the spacing that keeps labels apart is whatever the widest
47716
- // one is, so changing what a label says cannot quietly start overlapping them.
47717
- minSpacingPixels: Math.max((_c = this.settings.minSpacingPixels) !== null && _c !== void 0 ? _c : DEFAULT_MIN_SPACING_PIXELS, this.widestLabel(parts)),
48013
+ minSpacingPixels: (_c = this.settings.minSpacingPixels) !== null && _c !== void 0 ? _c : DEFAULT_MIN_SPACING_PIXELS,
47718
48014
  held: this.stride
47719
48015
  });
47720
- const stride = this.stride;
48016
+ const parts = this.settings.parts || ["value", "floor", "ceiling"];
48017
+ this.syncCellLabels(this.labelCells(window, cols, rows, this.stride, maxLabels, parts));
48018
+ }
48019
+ /*
48020
+ * One labelled cell per lattice block, keyed by the block's origin so it survives frame and stride changes.
48021
+ */
48022
+ labelCells(window, cols, rows, stride, max, parts) {
48023
+ const width = this.canvas.width;
48024
+ const height = this.canvas.height;
47721
48025
  const firstRow = Math.floor(window.row0 / stride) * stride;
47722
48026
  const firstCol = Math.floor(window.col0 / stride) * stride;
47723
- for (let row = firstRow; row <= window.row1 && this.drawn < maxLabels; row += stride) {
47724
- for (let col = firstCol; col <= window.col1 && this.drawn < maxLabels; col += stride) {
48027
+ const candidates = [];
48028
+ for (let row = firstRow; row <= window.row1; row += stride) {
48029
+ for (let col = firstCol; col <= window.col1; col += stride) {
47725
48030
  const found = this.readNear(col, row, cols, rows, stride);
47726
48031
  if (!found) {
47727
48032
  continue;
47728
48033
  }
47729
- const point = this.project(this.lonAt(found.col, cols), this.latAt(found.row, rows));
47730
- if (!point || point.x < 0 || point.y < 0
47731
- || point.x > this.canvas.width || point.y > this.canvas.height) {
48034
+ const point = this.projectCell(found.col, found.row, cols, rows);
48035
+ // A margin past the edge, so a label does not drop the moment its anchor does.
48036
+ const margin = LABEL_EDGE_MARGIN_PIXELS;
48037
+ if (!point ||
48038
+ point.x < -margin ||
48039
+ point.y < -margin ||
48040
+ point.x > width + margin ||
48041
+ point.y > height + margin) {
47732
48042
  continue;
47733
48043
  }
47734
- this.drawLabel(point, found.reading, parts, this.cellOutline(found.col, found.row, cols, rows));
48044
+ const text = this.labelLines(found.reading, parts).join("\n");
48045
+ if (!text) {
48046
+ continue;
48047
+ }
48048
+ const key = col + "," + row;
48049
+ const distance = Math.hypot(point.x - width / 2, point.y - height / 2);
48050
+ candidates.push({
48051
+ key,
48052
+ lon: this.lonAt(found.col, cols),
48053
+ lat: this.latAt(found.row, rows),
48054
+ text,
48055
+ // Labels already up outrank new ones, so the capped set does not shuffle as the view pans.
48056
+ rank: this.cellLabels.has(key) ? distance * LABEL_KEEP_BIAS : distance
48057
+ });
48058
+ }
48059
+ }
48060
+ if (candidates.length > max) {
48061
+ candidates.sort((x, y) => x.rank - y.rank);
48062
+ candidates.length = max;
48063
+ }
48064
+ return candidates;
48065
+ }
48066
+ /*
48067
+ * Keeps one scene label per wanted cell, reusing those that are still wanted.
48068
+ */
48069
+ syncCellLabels(wanted) {
48070
+ var _a;
48071
+ const keep = new Set(wanted.map((w) => w.key));
48072
+ const now = Date.now();
48073
+ let lingering = false;
48074
+ for (const [key, entry] of this.cellLabels) {
48075
+ if (keep.has(key)) {
48076
+ entry.missingSince = undefined;
48077
+ continue;
48078
+ }
48079
+ entry.missingSince = (_a = entry.missingSince) !== null && _a !== void 0 ? _a : now;
48080
+ if (now - entry.missingSince >= LABEL_LINGER_MS) {
48081
+ entry.label.Dispose();
48082
+ this.cellLabels.delete(key);
48083
+ }
48084
+ else {
48085
+ lingering = true;
48086
+ }
48087
+ }
48088
+ // A still camera draws nothing more, so a lingering label is given its own later look.
48089
+ if (lingering && !this.lingerTimer) {
48090
+ this.lingerTimer = setTimeout(() => {
48091
+ this.lingerTimer = null;
48092
+ if (!this.disposed) {
48093
+ this.drawnView = null;
48094
+ this.viewer.scene.requestRender();
48095
+ }
48096
+ }, LABEL_LINGER_MS);
48097
+ }
48098
+ for (const want of wanted) {
48099
+ const pos3d = Cesium.Cartesian3.fromDegrees(want.lon, want.lat, this.surfaceHeight(want.lon, want.lat));
48100
+ const options = {
48101
+ presentation: "text",
48102
+ text: want.text,
48103
+ leader: true,
48104
+ selectOnClick: false
48105
+ };
48106
+ const existing = this.cellLabels.get(want.key);
48107
+ if (existing) {
48108
+ if (!Cesium.Cartesian3.equalsEpsilon(existing.label.pos3d, pos3d, 0, 0.01)) {
48109
+ existing.label.pos3d = pos3d;
48110
+ }
48111
+ if (existing.text !== want.text) {
48112
+ existing.label.Configure(options);
48113
+ existing.text = want.text;
48114
+ }
48115
+ continue;
48116
+ }
48117
+ const id = (this.hover ? this.hover.entityId : "texture-value") + "#cell:" + want.key;
48118
+ const label = new exports.EntityLabel.Label({
48119
+ rego: {
48120
+ entityId: id,
48121
+ menuItemId: this.hover ? this.hover.menuItemId : undefined,
48122
+ name: want.text,
48123
+ priority: 0,
48124
+ // No graphic, since a point or billboard visual pulls a label back to itself on rebuild.
48125
+ visual: new Cesium.Entity({ position: pos3d })
48126
+ },
48127
+ viewer: this.viewer,
48128
+ pos3d,
48129
+ options,
48130
+ persistent: true,
48131
+ visible: () => !this.disposed
48132
+ });
48133
+ this.cellLabels.set(want.key, { label, text: want.text });
48134
+ }
48135
+ }
48136
+ clearCellLabels() {
48137
+ if (this.lingerTimer) {
48138
+ clearTimeout(this.lingerTimer);
48139
+ this.lingerTimer = null;
48140
+ }
48141
+ for (const entry of this.cellLabels.values()) {
48142
+ entry.label.Dispose();
48143
+ }
48144
+ this.cellLabels.clear();
48145
+ }
48146
+ /*
48147
+ * Shows the cell under the pointer through the register's hover label, unless something else holds it.
48148
+ */
48149
+ updateHover() {
48150
+ const hover = this.hover;
48151
+ if (this.disposed || !hover || !hover.register || !this.source || !this.hoverAt) {
48152
+ return;
48153
+ }
48154
+ const register = hover.register;
48155
+ const target = register.GetLabelHoverTarget();
48156
+ const ours = Boolean(target) && register.IsLabelHovered(hover.entityId, hover.menuItemId);
48157
+ if (target && !ours) {
48158
+ this.hovering = false;
48159
+ return;
48160
+ }
48161
+ const under = this.readingUnder(this.hoverAt.x, this.hoverAt.y);
48162
+ if (!under) {
48163
+ this.releaseHover();
48164
+ return;
48165
+ }
48166
+ register.SetLabelHover({
48167
+ entityId: hover.entityId,
48168
+ menuItemId: hover.menuItemId,
48169
+ position: { x: this.hoverAt.x, y: this.hoverAt.y },
48170
+ text: under.text,
48171
+ pos3d: under.pos3d
48172
+ });
48173
+ this.hovering = true;
48174
+ }
48175
+ releaseHover() {
48176
+ const hover = this.hover;
48177
+ if (!this.hovering || !hover || !hover.register) {
48178
+ return;
48179
+ }
48180
+ this.hovering = false;
48181
+ if (hover.register.IsLabelHovered(hover.entityId, hover.menuItemId)) {
48182
+ hover.register.ClearLabelHover();
48183
+ }
48184
+ }
48185
+ /*
48186
+ * The text for the painted cell under a window position and its centre on the sheet, or null.
48187
+ */
48188
+ readingUnder(x, y) {
48189
+ const at = this.cellUnder(x, y);
48190
+ if (!at) {
48191
+ return null;
48192
+ }
48193
+ const cols = this.source.width;
48194
+ const found = this.readAt(at.col, at.row, cols);
48195
+ if (!found || !this.paintedAt(at.col, at.row, cols)) {
48196
+ return null;
48197
+ }
48198
+ const text = this.labelLines(found, this.settings.parts || ["value", "floor", "ceiling"]).join("\n");
48199
+ if (!text) {
48200
+ return null;
48201
+ }
48202
+ const lon = this.lonAt(at.col, cols);
48203
+ const lat = this.latAt(at.row, this.source.height);
48204
+ return { text, pos3d: Cesium.Cartesian3.fromDegrees(lon, lat, this.surfaceHeight(lon, lat)) };
48205
+ }
48206
+ /*
48207
+ * The cell under a window position, found on the sheet rather than on the ground beneath it.
48208
+ */
48209
+ cellUnder(x, y) {
48210
+ const scene = this.viewer.scene;
48211
+ const camera = this.viewer.camera;
48212
+ const screen = new Cesium.Cartesian2(x, y);
48213
+ const ray = camera.getPickRay(screen);
48214
+ if (!ray) {
48215
+ return null;
48216
+ }
48217
+ let hit = (scene.globe && scene.globe.pick(ray, scene)) || camera.pickEllipsoid(screen);
48218
+ if (!hit) {
48219
+ return null;
48220
+ }
48221
+ const radii = Cesium.Ellipsoid.WGS84.radii;
48222
+ for (let i = 0; i < HOVER_REFINE_STEPS && this.heightAt; i++) {
48223
+ const carto = Cesium.Cartographic.fromCartesian(hit);
48224
+ const height = this.heightAt(Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude));
48225
+ if (height == null) {
48226
+ break;
47735
48227
  }
48228
+ const inflated = new Cesium.Ellipsoid(radii.x + height, radii.y + height, radii.z + height);
48229
+ const interval = Cesium.IntersectionTests.rayEllipsoid(ray, inflated);
48230
+ if (!interval) {
48231
+ break;
48232
+ }
48233
+ hit = Cesium.Ray.getPoint(ray, interval.start);
47736
48234
  }
48235
+ const carto = Cesium.Cartographic.fromCartesian(hit);
48236
+ return this.texelAt(Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude));
47737
48237
  }
47738
48238
  /*
47739
48239
  * Outlines each covered cell, from a projected lattice of its corners.
@@ -47761,44 +48261,25 @@
47761
48261
  if (wide * high > MAX_GRID_CELLS) {
47762
48262
  return;
47763
48263
  }
47764
- const lattice = new Array((wide + 1) * (high + 1));
47765
- for (let r = 0; r <= high; r++) {
47766
- for (let c = 0; c <= wide; c++) {
47767
- // Corners sit half a texel out from the centres lonAt/latAt return.
47768
- const lon = this.lonAt(window.col0 + c - 0.5, cols);
47769
- const lat = this.latAt(window.row0 + r - 0.5, rows);
47770
- lattice[r * (wide + 1) + c] = this.project(lon, lat);
47771
- }
47772
- }
47773
- const path = new Path2D();
47774
- let drew = false;
48264
+ const lattice = new Lattice(wide + 1, high + 1, (c, r) => {
48265
+ // Corners sit half a texel out from the centres lonAt/latAt return.
48266
+ const lon = this.lonAt(window.col0 + c - 0.5, cols);
48267
+ const lat = this.latAt(window.row0 + r - 0.5, rows);
48268
+ return this.projectInto(lon, lat);
48269
+ });
48270
+ const covered = new Uint8Array(wide * high);
47775
48271
  for (let r = 0; r < high; r++) {
47776
48272
  for (let c = 0; c < wide; c++) {
47777
- if (!this.isCovered(window.col0 + c, window.row0 + r, cols)) {
47778
- continue;
47779
- }
47780
- const tl = lattice[r * (wide + 1) + c];
47781
- const tr = lattice[r * (wide + 1) + c + 1];
47782
- const bl = lattice[(r + 1) * (wide + 1) + c];
47783
- const br = lattice[(r + 1) * (wide + 1) + c + 1];
47784
- if (!tl || !tr || !bl || !br) {
47785
- continue;
47786
- }
47787
- path.moveTo(tl.x, tl.y);
47788
- path.lineTo(tr.x, tr.y);
47789
- path.lineTo(br.x, br.y);
47790
- path.lineTo(bl.x, bl.y);
47791
- path.closePath();
47792
- drew = true;
48273
+ covered[r * wide + c] = this.isCovered(window.col0 + c, window.row0 + r, cols) ? 1 : 0;
47793
48274
  }
47794
48275
  }
47795
- if (!drew) {
48276
+ const path = new Path2D();
48277
+ if (!lattice.Trace(path, covered, wide, high)) {
47796
48278
  return;
47797
48279
  }
47798
48280
  this.ctx.strokeStyle = color;
47799
48281
  this.ctx.lineWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
47800
48282
  this.ctx.stroke(path);
47801
- +" alpha" + this.ctx.globalAlpha;
47802
48283
  }
47803
48284
  /*
47804
48285
  * Restricts drawing to the polygon, so nothing is ruled over ground it does not cover.
@@ -47820,33 +48301,11 @@
47820
48301
  path.closePath();
47821
48302
  this.ctx.clip(path);
47822
48303
  }
47823
- /*
47824
- * The cell of whichever tile covers a point, as screen corners.
47825
- */
47826
- tileCellOutline(lon, lat) {
47827
- for (const tile of this.tiles) {
47828
- if (lon < tile.West || lon > tile.East || lat < tile.South || lat > tile.North) {
47829
- continue;
47830
- }
47831
- const c = (Math.min(tile.ResolutionX - 1, Math.max(0, Math.floor((lon - tile.West) / (tile.East - tile.West) * tile.ResolutionX))));
47832
- const r = (Math.min(tile.ResolutionY - 1, Math.max(0, Math.floor((tile.North - lat) / (tile.North - tile.South) * tile.ResolutionY))));
47833
- const w0 = tile.West + (c / tile.ResolutionX) * (tile.East - tile.West);
47834
- const w1 = tile.West + ((c + 1) / tile.ResolutionX) * (tile.East - tile.West);
47835
- const n0 = tile.North - (r / tile.ResolutionY) * (tile.North - tile.South);
47836
- const n1 = tile.North - ((r + 1) / tile.ResolutionY) * (tile.North - tile.South);
47837
- const corners = [
47838
- this.project(w0, n0), this.project(w1, n0),
47839
- this.project(w1, n1), this.project(w0, n1)
47840
- ];
47841
- return corners.every((q) => q) ? corners : null;
47842
- }
47843
- return null;
47844
- }
47845
48304
  /*
47846
48305
  * Outlines each tile's own cells, so cell size still shows where the mesh is refined.
47847
48306
  */
47848
48307
  drawTileGrid(color) {
47849
- var _a, _b, _c;
48308
+ var _a, _b;
47850
48309
  const view = this.viewer.camera.computeViewRectangle(Cesium.Ellipsoid.WGS84);
47851
48310
  if (!view) {
47852
48311
  return;
@@ -47875,7 +48334,7 @@
47875
48334
  coarse.lineTo(tr.x, tr.y);
47876
48335
  coarse.lineTo(br.x, br.y);
47877
48336
  coarse.lineTo(bl.x, bl.y);
47878
- coarse.closePath();
48337
+ coarse.lineTo(tl.x, tl.y);
47879
48338
  drewCoarse = true;
47880
48339
  };
47881
48340
  // Planned for every visible tile BEFORE any is drawn.
@@ -47936,31 +48395,29 @@
47936
48395
  // moves, and so each step up is a subset of the one below it.
47937
48396
  const firstR = Math.floor(r0 / step) * step;
47938
48397
  const firstC = Math.floor(c0 / step) * step;
47939
- for (let r = firstR; r < r1; r += step) {
47940
- const n0 = tile.North - (r / tile.ResolutionY) * (tile.North - tile.South);
47941
- const n1 = tile.North
47942
- - (Math.min(r + step, tile.ResolutionY) / tile.ResolutionY) * (tile.North - tile.South);
47943
- for (let c = firstC; c < c1; c += step) {
47944
- const w0 = tile.West + (c / tile.ResolutionX) * (tile.East - tile.West);
47945
- const w1 = tile.West
47946
- + (Math.min(c + step, tile.ResolutionX) / tile.ResolutionX) * (tile.East - tile.West);
47947
- if (!this.coveredAt((w0 + w1) / 2, (n0 + n1) / 2)) {
47948
- continue;
47949
- }
47950
- const tl = this.project(w0, n0);
47951
- const tr = this.project(w1, n0);
47952
- const br = this.project(w1, n1);
47953
- const bl = this.project(w0, n1);
47954
- if (!tl || !tr || !br || !bl) {
47955
- continue;
47956
- }
47957
- path.moveTo(tl.x, tl.y);
47958
- path.lineTo(tr.x, tr.y);
47959
- path.lineTo(br.x, br.y);
47960
- path.lineTo(bl.x, bl.y);
47961
- path.closePath();
47962
- drew = true;
47963
- }
48398
+ const wide = Math.ceil((c1 - firstC) / step);
48399
+ const high = Math.ceil((r1 - firstR) / step);
48400
+ const lonOf = (c) => tile.West
48401
+ + (Math.min(firstC + c * step, tile.ResolutionX) / tile.ResolutionX) * (tile.East - tile.West);
48402
+ const latOf = (r) => tile.North
48403
+ - (Math.min(firstR + r * step, tile.ResolutionY) / tile.ResolutionY) * (tile.North - tile.South);
48404
+ const covered = new Uint8Array(wide * high);
48405
+ let any = false;
48406
+ for (let r = 0; r < high; r++) {
48407
+ for (let c = 0; c < wide; c++) {
48408
+ if (this.coveredAt((lonOf(c) + lonOf(c + 1)) / 2, (latOf(r) + latOf(r + 1)) / 2)) {
48409
+ covered[r * wide + c] = 1;
48410
+ any = true;
48411
+ }
48412
+ }
48413
+ }
48414
+ if (!any) {
48415
+ continue;
48416
+ }
48417
+ // Projected on demand, since a sparse raster leaves most corners belonging to no cell.
48418
+ const lattice = new Lattice(wide + 1, high + 1, (c, r) => this.projectInto(lonOf(c), latOf(r)));
48419
+ if (lattice.Trace(path, covered, wide, high)) {
48420
+ drew = true;
47964
48421
  }
47965
48422
  }
47966
48423
  const gridWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
@@ -47978,7 +48435,7 @@
47978
48435
  return;
47979
48436
  }
47980
48437
  this.ctx.strokeStyle = color;
47981
- this.ctx.lineWidth = (_c = this.settings.gridWidthPixels) !== null && _c !== void 0 ? _c : DEFAULT_GRID_WIDTH_PIXELS;
48438
+ this.ctx.lineWidth = gridWidth;
47982
48439
  this.ctx.stroke(path);
47983
48440
  }
47984
48441
  /*
@@ -47998,7 +48455,7 @@
47998
48455
  };
47999
48456
  }
48000
48457
  /*
48001
- * Whether the composited raster holds data at a lon/lat.
48458
+ * Whether the composited raster holds painted data at a lon/lat.
48002
48459
  */
48003
48460
  coveredAt(lon, lat) {
48004
48461
  if (!this.source) {
@@ -48011,60 +48468,20 @@
48011
48468
  if (col < 0 || row < 0 || col >= cols || row >= rows) {
48012
48469
  return false;
48013
48470
  }
48014
- const at = (row * cols + col) * 4 + 3;
48015
48471
  // Painted, not merely covered: a dry cell holds a reading the ramp deliberately hides, and
48016
48472
  // ruling a grid over it draws detail where the picture shows none.
48017
- if (this.painted && this.painted.length === cols * rows * 4) {
48018
- return this.painted[at] > 0;
48473
+ if (this.painted || this.paintedRamp) {
48474
+ return this.paintedAt(col, row, cols);
48019
48475
  }
48020
- return this.source.data[at] >= 128;
48476
+ return this.source.data[(row * cols + col) * 4 + 3] >= 128;
48021
48477
  }
48022
48478
  isCovered(col, row, cols) {
48023
48479
  return Boolean(this.source && this.source.data[(row * cols + col) * 4 + 3] >= 128);
48024
48480
  }
48025
48481
  /*
48026
- * Width of the widest label this overlay could draw, in pixels.
48027
- */
48028
- widestLabel(parts) {
48029
- const sample = [];
48030
- if (parts.indexOf("value") >= 0) {
48031
- sample.push(valueLine("-000.00", LabelUnits(this.metadata)));
48032
- }
48033
- const range = [];
48034
- const showsRange = !exports.TextureFrameSeriesAnimator.IsTransformed(this.metadata);
48035
- if (showsRange && parts.indexOf("floor") >= 0) {
48036
- range.push("min: -000.00");
48037
- }
48038
- if (showsRange && parts.indexOf("ceiling") >= 0) {
48039
- range.push("max: -000.00");
48040
- }
48041
- if (range.length) {
48042
- sample.push(range.join(" - "));
48043
- }
48044
- if (!sample.length) {
48045
- return 0;
48046
- }
48047
- const widest = Math.max(...sample.map((line) => this.ctx.measureText(line).width));
48048
- return widest + LABEL_GAP_PIXELS;
48049
- }
48050
- /*
48051
- * The four screen corners of one cell, or null if any of them will not project.
48482
+ * What a label says about one reading, a line at a time.
48052
48483
  */
48053
- cellOutline(col, row, cols, rows) {
48054
- // On a pyramid the composited texel is not a cell. Outlining it would draw a second,
48055
- // uniform lattice over the tile grid, misaligned with it everywhere.
48056
- if (this.tiles && this.tiles.length) {
48057
- return this.tileCellOutline(this.lonAt(col, cols), this.latAt(row, rows));
48058
- }
48059
- const corners = [
48060
- this.project(this.lonAt(col - 0.5, cols), this.latAt(row - 0.5, rows)),
48061
- this.project(this.lonAt(col + 0.5, cols), this.latAt(row - 0.5, rows)),
48062
- this.project(this.lonAt(col + 0.5, cols), this.latAt(row + 0.5, rows)),
48063
- this.project(this.lonAt(col - 0.5, cols), this.latAt(row + 0.5, rows))
48064
- ];
48065
- return corners.every((c) => c) ? corners : null;
48066
- }
48067
- drawLabel(point, reading, parts, outline) {
48484
+ labelLines(reading, parts) {
48068
48485
  var _a;
48069
48486
  const dp = (_a = this.settings.decimals) !== null && _a !== void 0 ? _a : (Math.abs(reading.value) < 10 ? 2 : 1);
48070
48487
  const lines = [];
@@ -48083,32 +48500,7 @@
48083
48500
  if (range.length) {
48084
48501
  lines.push(range.join(" - "));
48085
48502
  }
48086
- if (!lines.length) {
48087
- return;
48088
- }
48089
- if (outline) {
48090
- this.ctx.strokeStyle = LABEL_ANCHOR_COLOR;
48091
- this.ctx.lineWidth = 1.5;
48092
- this.ctx.beginPath();
48093
- this.ctx.moveTo(outline[0].x, outline[0].y);
48094
- for (let i = 1; i < outline.length; i++) {
48095
- this.ctx.lineTo(outline[i].x, outline[i].y);
48096
- }
48097
- this.ctx.closePath();
48098
- this.ctx.stroke();
48099
- }
48100
- const width = Math.max(...lines.map((l) => this.ctx.measureText(l).width)) + 10;
48101
- const height = lines.length * LINE_HEIGHT + 6;
48102
- const top = point.y - height / 2;
48103
- this.ctx.fillStyle = "rgba(16,16,14,0.72)";
48104
- this.ctx.beginPath();
48105
- this.ctx.roundRect(point.x - width / 2, top, width, height, LABEL_RADIUS);
48106
- this.ctx.fill();
48107
- lines.forEach((line, i) => {
48108
- this.ctx.fillStyle = i === 0 ? "#ffffff" : "#a8a89f";
48109
- this.ctx.fillText(line, point.x, top + LINE_HEIGHT * (i + 1) - 1);
48110
- });
48111
- this.drawn++;
48503
+ return lines;
48112
48504
  }
48113
48505
  /*
48114
48506
  * The first cell with a reading at or near a lattice point, searched within its own block.
@@ -48135,11 +48527,24 @@
48135
48527
  return null;
48136
48528
  }
48137
48529
  paintedAt(col, row, cols) {
48138
- if (!this.painted) {
48139
- return true;
48530
+ const at = (row * cols + col) * 4;
48531
+ if (this.painted && this.painted.length === this.source.data.length) {
48532
+ return this.painted[at + 3] > 0;
48140
48533
  }
48141
- const at = (row * cols + col) * 4 + 3;
48142
- return this.painted[at] > 0;
48534
+ if (this.paintedRamp) {
48535
+ const data = this.source.data;
48536
+ if (data[at + 3] === 0) {
48537
+ return false;
48538
+ }
48539
+ // The 8 bit grey the ramp is indexed on, which is what a tinted frame would have used.
48540
+ const grey = exports.TextureFrameSeriesAnimator.IsPackedValue(this.metadata)
48541
+ ? Math.round((data[at] * 256 + data[at + 1]) / 65535 * 255)
48542
+ : data[at];
48543
+ const steps = this.paintedRamp.length / 4;
48544
+ const index = Math.min(steps - 1, Math.round(grey / 255 * (steps - 1)));
48545
+ return this.paintedRamp[index * 4 + 3] > 0;
48546
+ }
48547
+ return true;
48143
48548
  }
48144
48549
  // Decodes one cell, skipping anything the texture marks as having no data.
48145
48550
  readAt(col, row, cols) {
@@ -48155,6 +48560,16 @@
48155
48560
  sourceMap: this.sourceMap
48156
48561
  });
48157
48562
  }
48563
+ /*
48564
+ * The texel holding a lon/lat, or null outside the raster.
48565
+ */
48566
+ texelAt(lon, lat) {
48567
+ const cols = this.source.width;
48568
+ const rows = this.source.height;
48569
+ const col = Math.floor((lon - this.extent.West) / (this.extent.East - this.extent.West) * cols);
48570
+ const row = Math.floor((this.extent.North - lat) / (this.extent.North - this.extent.South) * rows);
48571
+ return col < 0 || row < 0 || col >= cols || row >= rows ? null : { col, row };
48572
+ }
48158
48573
  lonAt(col, cols) {
48159
48574
  return this.extent.West + ((col + 0.5) / cols) * (this.extent.East - this.extent.West);
48160
48575
  }
@@ -48183,15 +48598,102 @@
48183
48598
  row1: Math.min(rows - 1, Math.ceil((this.extent.North - south) / spanLat * rows) + 1)
48184
48599
  };
48185
48600
  }
48601
+ // Metres the sheet sits at over a point, or the ellipsoid when nothing says otherwise.
48602
+ surfaceHeight(lon, lat) {
48603
+ const height = this.heightAt ? this.heightAt(lon, lat) : null;
48604
+ return height == null || !isFinite(height) ? 0 : height;
48605
+ }
48606
+ projectCell(col, row, cols, rows) {
48607
+ return this.project(this.lonAt(col, cols), this.latAt(row, rows));
48608
+ }
48609
+ // Projected at the sheet's own height, since up close the ellipsoid is metres from where it is drawn.
48186
48610
  project(lon, lat) {
48187
- const world = Cesium.Cartesian3.fromDegrees(lon, lat, 0);
48611
+ return this.projectInto(lon, lat) ? { x: this.scratchWindow.x, y: this.scratchWindow.y } : null;
48612
+ }
48613
+ /*
48614
+ * Projects into the shared scratch window position, returning whether it landed anywhere.
48615
+ */
48616
+ projectInto(lon, lat) {
48617
+ const world = Cesium.Cartesian3.fromDegrees(lon, lat, this.surfaceHeight(lon, lat), Cesium.Ellipsoid.WGS84, this.scratchWorld);
48618
+ // One matrix multiply in 3D, where the general path rederives the viewport and frustum per point.
48619
+ if (this.viewProjection) {
48620
+ const clip = Cesium.Matrix4.multiplyByVector(this.viewProjection, Cesium.Cartesian4.fromElements(world.x, world.y, world.z, 1, this.scratchClip), this.scratchClip);
48621
+ if (!(clip.w > 0)) {
48622
+ return null;
48623
+ }
48624
+ const view = this.viewer.canvas;
48625
+ this.scratchWindow.x = (clip.x / clip.w * 0.5 + 0.5) * view.clientWidth;
48626
+ this.scratchWindow.y = (0.5 - clip.y / clip.w * 0.5) * view.clientHeight;
48627
+ return this.scratchWindow;
48628
+ }
48188
48629
  const transforms = Cesium.SceneTransforms;
48189
48630
  // Renamed in newer Cesium, and this library runs on whatever the host app supplies.
48190
48631
  const fn = transforms.worldToWindowCoordinates || transforms.wgs84ToWindowCoordinates;
48191
- return fn ? fn.call(transforms, this.viewer.scene, world) : null;
48632
+ return fn ? fn.call(transforms, this.viewer.scene, world, this.scratchWindow) || null : null;
48192
48633
  }
48193
48634
  }
48194
48635
  TextureValueLabels.Labels = Labels;
48636
+ /*
48637
+ * A grid of window positions projected at most once each, and the cell edges traced through them.
48638
+ */
48639
+ class Lattice {
48640
+ constructor(wide, high, projectAt) {
48641
+ this.wide = wide;
48642
+ this.projectAt = projectAt;
48643
+ this.xs = new Float64Array(wide * high);
48644
+ this.ys = new Float64Array(wide * high);
48645
+ this.state = new Uint8Array(wide * high);
48646
+ }
48647
+ at(c, r) {
48648
+ const i = r * this.wide + c;
48649
+ if (this.state[i] === 0) {
48650
+ const p = this.projectAt(c, r);
48651
+ if (p) {
48652
+ this.xs[i] = p.x;
48653
+ this.ys[i] = p.y;
48654
+ this.state[i] = 1;
48655
+ }
48656
+ else {
48657
+ this.state[i] = 2;
48658
+ }
48659
+ }
48660
+ return this.state[i] === 1 ? i : -1;
48661
+ }
48662
+ edge(path, a, b) {
48663
+ if (a < 0 || b < 0) {
48664
+ return false;
48665
+ }
48666
+ path.moveTo(this.xs[a], this.ys[a]);
48667
+ path.lineTo(this.xs[b], this.ys[b]);
48668
+ return true;
48669
+ }
48670
+ /*
48671
+ * Adds each covered cell's edges to a path, a shared edge once, and says whether any were added.
48672
+ */
48673
+ Trace(path, covered, cellsWide, cellsHigh) {
48674
+ let drew = false;
48675
+ for (let r = 0; r < cellsHigh; r++) {
48676
+ for (let c = 0; c < cellsWide; c++) {
48677
+ if (!covered[r * cellsWide + c]) {
48678
+ continue;
48679
+ }
48680
+ const tl = this.at(c, r);
48681
+ const tr = this.at(c + 1, r);
48682
+ const bl = this.at(c, r + 1);
48683
+ if (!(r > 0 && covered[(r - 1) * cellsWide + c])) {
48684
+ drew = this.edge(path, tl, tr) || drew;
48685
+ }
48686
+ if (!(c > 0 && covered[r * cellsWide + c - 1])) {
48687
+ drew = this.edge(path, tl, bl) || drew;
48688
+ }
48689
+ const br = this.at(c + 1, r + 1);
48690
+ drew = this.edge(path, tr, br) || drew;
48691
+ drew = this.edge(path, bl, br) || drew;
48692
+ }
48693
+ }
48694
+ return drew;
48695
+ }
48696
+ }
48195
48697
  /*
48196
48698
  * The value line. Units go on it alone: repeating them on the range underneath doubles the widest
48197
48699
  * label to say nothing, and the two lines are plainly the same quantity.
@@ -48287,6 +48789,9 @@ in vec2 st;
48287
48789
  in float skirt;
48288
48790
 
48289
48791
  uniform sampler2D u_valueTexture;
48792
+ // The frame being faded away from, and how far the fade has got. The same texture at 1 when settled.
48793
+ uniform sampler2D u_valueTextureFrom;
48794
+ uniform float u_valueFade;
48290
48795
  uniform sampler2D u_groundTexture;
48291
48796
  uniform float u_valuePacked;
48292
48797
  uniform float u_valueMin;
@@ -48407,18 +48912,22 @@ float decodeValue(vec4 texel) {
48407
48912
  }
48408
48913
 
48409
48914
  void main() {
48410
- vec4 texel = texture(u_valueTexture, st);
48915
+ vec4 texelTo = texture(u_valueTexture, st);
48916
+ vec4 texelFrom = texture(u_valueTextureFrom, st);
48411
48917
  v_st = st;
48918
+ // Decoded before mixing, since blending a packed pair byte by byte reads as some other value
48919
+ // halfway through the fade.
48920
+ float raw = mix(decodeValue(texelFrom), decodeValue(texelTo), u_valueFade);
48921
+ float alpha = mix(texelFrom.a, texelTo.a, u_valueFade);
48412
48922
 
48413
48923
  // Inside a region the scene knows is water, a texel with no value still has to paint, or the
48414
48924
  // hole the region exists to close is still a hole.
48415
48925
  // The value it paints is the shallowest the ramp is not transparent at.
48416
48926
  float inMask = u_maskWeight * texture(u_maskTexture, st).a;
48417
- float hasData = step(u_coverageCutoffVertex, texel.a);
48418
- float raw = decodeValue(texel);
48927
+ float hasData = step(u_coverageCutoffVertex, alpha);
48419
48928
  float filled = mix(u_maskFillValue, max(raw, u_maskFillValue), hasData);
48420
48929
  v_value = mix(raw, filled, inMask * u_maskFill);
48421
- v_coverage = max(texel.a, inMask * u_maskFill);
48930
+ v_coverage = max(alpha, inMask * u_maskFill);
48422
48931
 
48423
48932
  // Exaggeration stretches the span above the archive's minimum rather than the height itself. On an
48424
48933
  // elevation the height is measured from the ellipsoid, tens of metres from any of the data, so
@@ -48461,6 +48970,8 @@ void main() {
48461
48970
  `;
48462
48971
  const FRAGMENT_SHADER_GLSL300 = `
48463
48972
  uniform sampler2D u_valueTexture;
48973
+ uniform sampler2D u_valueTextureFrom;
48974
+ uniform float u_valueFade;
48464
48975
  uniform sampler2D u_rampTexture;
48465
48976
  uniform float u_coverageCutoff;
48466
48977
  uniform vec2 u_texelSize;
@@ -48497,12 +49008,15 @@ in vec2 v_waveMetres;
48497
49008
  * Scaled per channel for the same reason the vertex shader's decode is: reassembling the pair reaches
48498
49009
  * 65535, which mediump does not have to represent, and this is the shader that may not get highp.
48499
49010
  */
48500
- float valueAt(vec2 uv) {
48501
- vec4 texel = texture(u_valueTexture, uv);
49011
+ float decodeValue(vec4 texel) {
48502
49012
  float packed16 = texel.r * (255.0 * 256.0 / 65535.0) + texel.g * (255.0 / 65535.0);
48503
49013
  return mix(texel.r, packed16, u_valuePacked);
48504
49014
  }
48505
49015
 
49016
+ float valueAt(vec2 uv) {
49017
+ return mix(decodeValue(texture(u_valueTextureFrom, uv)), decodeValue(texture(u_valueTexture, uv)), u_valueFade);
49018
+ }
49019
+
48506
49020
  //#water
48507
49021
  /*
48508
49022
  * Slope of the ripple field at a point, in metres of rise per metre travelled.
@@ -48949,6 +49463,13 @@ void main() {
48949
49463
  this.rampTexture = null;
48950
49464
  this.texture = null;
48951
49465
  this.textureDirty = true;
49466
+ // Fed from a fading source: the frame being left, and which pixels each texture holds.
49467
+ this.fade = null;
49468
+ this.textureFrom = null;
49469
+ this.uploadedTo = null;
49470
+ this.uploadedFrom = null;
49471
+ // Size of whichever value raster is bound, canvas or pixels.
49472
+ this.sourceDims = null;
48952
49473
  this.shaderProgram = null;
48953
49474
  this.shaderProgramWater = false;
48954
49475
  this.renderState = null;
@@ -49119,6 +49640,65 @@ void main() {
49119
49640
  this.maskGroundTexture = null;
49120
49641
  }
49121
49642
  }
49643
+ /**
49644
+ * Metres above the ellipsoid the sheet sits at over a point, or null unless a fading source is bound.
49645
+ */
49646
+ HeightAt(lon, lat) {
49647
+ const pixels = this.fade ? this.fade.to : null;
49648
+ const dims = this.sourceDims;
49649
+ if (!pixels || !dims) {
49650
+ return null;
49651
+ }
49652
+ const fx = (lon - this.extent.West) / (this.extent.East - this.extent.West);
49653
+ const fy = (this.extent.North - lat) / (this.extent.North - this.extent.South);
49654
+ if (!(fx >= 0 && fx <= 1 && fy >= 0 && fy <= 1)) {
49655
+ return null;
49656
+ }
49657
+ const col = Math.min(dims.width - 1, Math.floor(fx * dims.width));
49658
+ const row = Math.min(dims.height - 1, Math.floor(fy * dims.height));
49659
+ const at = (row * dims.width + col) * 4;
49660
+ const coverage = pixels[at + 3] / 255;
49661
+ const normalised = this.packedValue
49662
+ ? (pixels[at] * 256 + pixels[at + 1]) / 65535
49663
+ : pixels[at] / 255;
49664
+ const metres = this.valueMin + normalised * (this.valueMax - this.valueMin) * this.exaggeration;
49665
+ const following = this.followGround && Boolean(this.groundPixels);
49666
+ const ground = following ? this.groundAt(fx, fy) : 0;
49667
+ return this.baseHeight + ground + metres * (following ? coverage : 1);
49668
+ }
49669
+ /**
49670
+ * Changes whenever HeightAt would answer differently for the same frame.
49671
+ */
49672
+ GetPlacementKey() {
49673
+ return [this.groundPixels ? 1 : 0, this.baseHeight, this.exaggeration, this.followGround ? 1 : 0].join("|");
49674
+ }
49675
+ /**
49676
+ * The ramp the sheet is painted with, as 256 RGBA steps across the value range.
49677
+ */
49678
+ GetRampPixels() {
49679
+ return this.rampPixels;
49680
+ }
49681
+ /*
49682
+ * The packed ground raster read the way sampleGround reads it, bilinear between texel centres.
49683
+ */
49684
+ groundAt(u, v) {
49685
+ const size = GROUND_SAMPLES_PER_SIDE;
49686
+ const pixels = this.groundPixels;
49687
+ const read = (x, y) => {
49688
+ const i = (Math.min(size - 1, Math.max(0, y)) * size + Math.min(size - 1, Math.max(0, x))) * 4;
49689
+ const packed = (pixels[i] << 16) + (pixels[i + 1] << 8) + pixels[i + 2];
49690
+ return GROUND_MIN_METRES + (packed / 16777215) * (GROUND_MAX_METRES - GROUND_MIN_METRES);
49691
+ };
49692
+ const x = u * size - 0.5;
49693
+ const y = v * size - 0.5;
49694
+ const x0 = Math.floor(x);
49695
+ const y0 = Math.floor(y);
49696
+ const fx = x - x0;
49697
+ const fy = y - y0;
49698
+ const top = read(x0, y0) + (read(x0 + 1, y0) - read(x0, y0)) * fx;
49699
+ const bottom = read(x0, y0 + 1) + (read(x0 + 1, y0 + 1) - read(x0, y0 + 1)) * fx;
49700
+ return top + (bottom - top) * fy;
49701
+ }
49122
49702
  /**
49123
49703
  * Feeds the surface from an animated frame series.
49124
49704
  * Height and colour advance with the clock without the caller having to push each frame in.
@@ -49137,23 +49717,39 @@ void main() {
49137
49717
  return;
49138
49718
  }
49139
49719
  const context = frameState.context;
49140
- if (this.valueSource) {
49141
- const canvas = this.valueSource.GetValueCanvas();
49142
- if (!canvas) {
49143
- // No frame decoded yet, so there is nothing to displace against.
49144
- return;
49145
- }
49146
- const version = this.valueSource.GetPresentVersion();
49147
- if (canvas !== this.source || version !== this.lastPresentVersion) {
49148
- this.source = canvas;
49149
- this.lastPresentVersion = version;
49150
- this.textureDirty = true;
49720
+ const fade = this.valueSource && this.valueSource.GetValueFade
49721
+ ? this.valueSource.GetValueFade()
49722
+ : null;
49723
+ if (fade) {
49724
+ this.fade = fade;
49725
+ this.sourceDims = { width: fade.width, height: fade.height };
49726
+ }
49727
+ else {
49728
+ this.fade = null;
49729
+ if (this.valueSource) {
49730
+ const canvas = this.valueSource.GetValueCanvas();
49731
+ if (!canvas) {
49732
+ // No frame decoded yet, so there is nothing to displace against.
49733
+ return;
49734
+ }
49735
+ const version = this.valueSource.GetPresentVersion();
49736
+ if (canvas !== this.source || version !== this.lastPresentVersion) {
49737
+ this.source = canvas;
49738
+ this.lastPresentVersion = version;
49739
+ this.textureDirty = true;
49740
+ }
49151
49741
  }
49742
+ this.sourceDims = this.source ? { width: this.source.width, height: this.source.height } : null;
49152
49743
  }
49153
- if (!this.source || !this.source.width || !this.source.height) {
49744
+ if (!this.sourceDims || !this.sourceDims.width || !this.sourceDims.height) {
49154
49745
  return;
49155
49746
  }
49156
- this.syncTexture(context);
49747
+ if (this.fade) {
49748
+ this.syncFadeTextures(context);
49749
+ }
49750
+ else {
49751
+ this.syncTexture(context);
49752
+ }
49157
49753
  this.syncRampTexture(context);
49158
49754
  this.requestGround();
49159
49755
  this.syncGroundTexture(context);
@@ -49201,6 +49797,10 @@ void main() {
49201
49797
  this.texture.destroy();
49202
49798
  this.texture = null;
49203
49799
  }
49800
+ if (this.textureFrom) {
49801
+ this.textureFrom.destroy();
49802
+ this.textureFrom = null;
49803
+ }
49204
49804
  if (this.rampTexture) {
49205
49805
  this.rampTexture.destroy();
49206
49806
  this.rampTexture = null;
@@ -49468,7 +50068,7 @@ void main() {
49468
50068
  const southWest = Cesium.Cartesian3.fromDegrees(this.extent.West, this.extent.South, 0);
49469
50069
  const southEast = Cesium.Cartesian3.fromDegrees(this.extent.East, this.extent.South, 0);
49470
50070
  const width = Cesium.Cartesian3.distance(southWest, southEast);
49471
- return Math.max(1, width / Math.max(1, this.source ? this.source.width : 1));
50071
+ return Math.max(1, width / Math.max(1, this.sourceDims ? this.sourceDims.width : 1));
49472
50072
  }
49473
50073
  syncRampTexture(context) {
49474
50074
  if (this.rampTexture) {
@@ -49489,6 +50089,67 @@ void main() {
49489
50089
  })
49490
50090
  });
49491
50091
  }
50092
+ valueSampler() {
50093
+ // Nearest for a packed value, because filtering would blend the two bytes of the
50094
+ // packing rather than the values they encode, the same trap sampleGround avoids.
50095
+ const minification = this.packedValue
50096
+ ? Cesium.TextureMinificationFilter.NEAREST
50097
+ : Cesium.TextureMinificationFilter.LINEAR;
50098
+ const magnification = this.packedValue
50099
+ ? Cesium.TextureMagnificationFilter.NEAREST
50100
+ : Cesium.TextureMagnificationFilter.LINEAR;
50101
+ return new Cesium.Sampler({
50102
+ wrapS: Cesium.TextureWrap.CLAMP_TO_EDGE,
50103
+ wrapT: Cesium.TextureWrap.CLAMP_TO_EDGE,
50104
+ minificationFilter: minification,
50105
+ magnificationFilter: magnification
50106
+ });
50107
+ }
50108
+ /*
50109
+ * Uploads value pixels into a texture, reusing it when the size still fits.
50110
+ */
50111
+ uploadPixels(context, texture, pixels, width, height) {
50112
+ const view = new Uint8Array(pixels.buffer, pixels.byteOffset, pixels.byteLength);
50113
+ const source = { width, height, arrayBufferView: view };
50114
+ if (texture && texture.width === width && texture.height === height) {
50115
+ texture.copyFrom({ source });
50116
+ return texture;
50117
+ }
50118
+ if (texture) {
50119
+ texture.destroy();
50120
+ }
50121
+ return new Cesium.Texture({
50122
+ context,
50123
+ width,
50124
+ height,
50125
+ pixelFormat: Cesium.PixelFormat.RGBA,
50126
+ pixelDatatype: Cesium.PixelDatatype.UNSIGNED_BYTE,
50127
+ source,
50128
+ // Row 0 is the archive's north edge, and has to stay at the top, as the canvas path does.
50129
+ flipY: false,
50130
+ sampler: this.valueSampler()
50131
+ });
50132
+ }
50133
+ /*
50134
+ * Keeps a texture each side of the fade, swapping them on a new frame so only that one is uploaded.
50135
+ */
50136
+ syncFadeTextures(context) {
50137
+ const fade = this.fade;
50138
+ if (fade.to !== this.uploadedTo) {
50139
+ if (fade.from && fade.from === this.uploadedTo && this.texture) {
50140
+ const previous = this.textureFrom;
50141
+ this.textureFrom = this.texture;
50142
+ this.uploadedFrom = this.uploadedTo;
50143
+ this.texture = previous;
50144
+ }
50145
+ this.texture = this.uploadPixels(context, this.texture, fade.to, fade.width, fade.height);
50146
+ this.uploadedTo = fade.to;
50147
+ }
50148
+ if (fade.from && fade.from !== this.uploadedFrom) {
50149
+ this.textureFrom = this.uploadPixels(context, this.textureFrom, fade.from, fade.width, fade.height);
50150
+ this.uploadedFrom = fade.from;
50151
+ }
50152
+ }
49492
50153
  syncTexture(context) {
49493
50154
  const sizeChanged = this.texture
49494
50155
  && (this.texture.width !== this.source.width || this.texture.height !== this.source.height);
@@ -49496,27 +50157,15 @@ void main() {
49496
50157
  if (this.texture) {
49497
50158
  this.texture.destroy();
49498
50159
  }
49499
- // Nearest for a packed value, because filtering would blend the two bytes of the
49500
- // packing rather than the values they encode, the same trap sampleGround avoids.
49501
- const minification = this.packedValue
49502
- ? Cesium.TextureMinificationFilter.NEAREST
49503
- : Cesium.TextureMinificationFilter.LINEAR;
49504
- const magnification = this.packedValue
49505
- ? Cesium.TextureMagnificationFilter.NEAREST
49506
- : Cesium.TextureMagnificationFilter.LINEAR;
49507
50160
  this.texture = new Cesium.Texture({
49508
50161
  context,
49509
50162
  source: this.source,
49510
50163
  // Cesium flips canvas rows on upload by default, which would put the archive's
49511
50164
  // row 0 (its north edge) at the south edge and mirror the whole surface.
49512
50165
  flipY: false,
49513
- sampler: new Cesium.Sampler({
49514
- wrapS: Cesium.TextureWrap.CLAMP_TO_EDGE,
49515
- wrapT: Cesium.TextureWrap.CLAMP_TO_EDGE,
49516
- minificationFilter: minification,
49517
- magnificationFilter: magnification
49518
- })
50166
+ sampler: this.valueSampler()
49519
50167
  });
50168
+ this.uploadedTo = null;
49520
50169
  this.textureDirty = false;
49521
50170
  return;
49522
50171
  }
@@ -49579,7 +50228,7 @@ void main() {
49579
50228
  * carry, so the ladder is capped one step above the texel count rather than run to its top.
49580
50229
  */
49581
50230
  tierCeiling() {
49582
- const source = this.source;
50231
+ const source = this.sourceDims;
49583
50232
  if (!source || !source.width || !source.height) {
49584
50233
  return TIERS[TIERS.length - 1];
49585
50234
  }
@@ -49657,6 +50306,10 @@ void main() {
49657
50306
  owner: this,
49658
50307
  uniformMap: {
49659
50308
  u_valueTexture: () => self.texture,
50309
+ u_valueTextureFrom: () => (self.fade && self.fade.from && self.textureFrom
50310
+ ? self.textureFrom
50311
+ : self.texture),
50312
+ u_valueFade: () => (self.fade && self.fade.from ? self.fade.mix : 1),
49660
50313
  u_groundTexture: () => self.groundTexture,
49661
50314
  u_valuePacked: () => (self.packedValue ? 1 : 0),
49662
50315
  u_groundWeight: () => (self.followGround && self.groundPixels ? 1 : 0),
@@ -49694,7 +50347,7 @@ void main() {
49694
50347
  : 0,
49695
50348
  u_coverageCutoff: () => COVERAGE_CUTOFF,
49696
50349
  u_rampTexture: () => self.rampTexture,
49697
- u_texelSize: () => new Cesium.Cartesian2(1 / self.source.width, 1 / self.source.height),
50350
+ u_texelSize: () => new Cesium.Cartesian2(1 / self.sourceDims.width, 1 / self.sourceDims.height),
49698
50351
  u_waterOn: () => (self.water ? 1 : 0),
49699
50352
  u_waterWavelength: () => (self.water && self.water.wavelength)
49700
50353
  || DEFAULT_WATER_WAVELENGTH_METRES,
@@ -50065,6 +50718,10 @@ void main() {
50065
50718
  syncTextureValueLabels({
50066
50719
  cEntity,
50067
50720
  viewer: params.viewer,
50721
+ surface: drawsDisplacedSurface ? cEntity[DISPLACED_SURFACE_KEY] : null,
50722
+ hover: params.visualRegister
50723
+ ? { register: params.visualRegister, entityId: String(entity.Bruce.ID), menuItemId: params.menuItemId }
50724
+ : undefined,
50068
50725
  settings: overlaySettings(style, entity, params.tags),
50069
50726
  extent: labelArchive
50070
50727
  ? (drawsDisplacedSurface ? archiveExtent(labelArchive.metadata, posses) : drapeExtent)
@@ -50371,6 +51028,7 @@ void main() {
50371
51028
  // rendered: null
50372
51029
  rendered: (_c = params.rendered) === null || _c === void 0 ? void 0 : _c.get(entity.Bruce.ID),
50373
51030
  menuItemId: params.menuItemId,
51031
+ visualRegister: params.visualRegister,
50374
51032
  onSeriesDiscovered: params.onSeriesDiscovered
50375
51033
  });
50376
51034
  }
@@ -50808,8 +51466,8 @@ void main() {
50808
51466
  */
50809
51467
  const LABEL_GRID_COLOR = "rgba(255,255,255,0.55)";
50810
51468
  const LABEL_GRID_WIDTH_PIXELS = 1;
50811
- const LABEL_MIN_SPACING_PIXELS = 78;
50812
- const LABEL_MAX_LABELS = 240;
51469
+ const LABEL_MIN_SPACING_PIXELS = 140;
51470
+ const LABEL_MAX_LABELS = 60;
50813
51471
  function overlaySettings(style, entity, tags) {
50814
51472
  if (!wantsLabels(style)) {
50815
51473
  return undefined;
@@ -50829,6 +51487,7 @@ void main() {
50829
51487
  */
50830
51488
  function syncTextureValueLabels(params) {
50831
51489
  const { cEntity, viewer, settings, extent, animator, metadata } = params;
51490
+ const surface = params.surface && !params.surface.isDestroyed() ? params.surface : null;
50832
51491
  const existing = cEntity[TEXTURE_VALUE_LABELS_KEY];
50833
51492
  // Offline has no extent to rebuild the overlay from, so an existing one is left running.
50834
51493
  if (params.offline && existing) {
@@ -50847,8 +51506,13 @@ void main() {
50847
51506
  West: tile.West, East: tile.East, South: tile.South, North: tile.North,
50848
51507
  ResolutionX: tile.ResolutionX, ResolutionY: tile.ResolutionY
50849
51508
  })),
50850
- clipRing: params.clipRing
51509
+ clipRing: params.clipRing,
51510
+ heightAt: surface ? (lon, lat) => surface.HeightAt(lon, lat) : undefined,
51511
+ heightKey: surface ? () => surface.GetPlacementKey() : undefined,
51512
+ hover: params.hover
50851
51513
  });
51514
+ // A surface keeps no tinted frame, so whether a cell was painted is read off its ramp.
51515
+ labels.SetPaintedRamp(surface ? surface.GetRampPixels() : null);
50852
51516
  let lastVersion = -1;
50853
51517
  let appliedFloor = null;
50854
51518
  let appliedCeiling = null;
@@ -50860,15 +51524,15 @@ void main() {
50860
51524
  if (animator.IsDisposed()) {
50861
51525
  return;
50862
51526
  }
50863
- const canvas = animator.GetValueCanvas();
50864
- if (!canvas) {
50865
- return;
50866
- }
50867
51527
  const version = animator.GetPresentVersion();
50868
51528
  if (version !== lastVersion) {
51529
+ // Read from the frame cache, since reading the value canvas back copies a whole frame.
51530
+ const raster = animator.GetValueRaster();
51531
+ if (!raster) {
51532
+ return;
51533
+ }
50869
51534
  lastVersion = version;
50870
- const ctx = canvas.getContext("2d");
50871
- labels.SetSource(ctx.getImageData(0, 0, canvas.width, canvas.height));
51535
+ labels.SetSource(raster);
50872
51536
  labels.SetPainted(animator.GetDisplayedPixels());
50873
51537
  }
50874
51538
  // Tracked by reference rather than latched on the first one to arrive: a tiled archive
@@ -52592,7 +53256,7 @@ void main() {
52592
53256
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
52593
53257
  })(exports.StyleUtils || (exports.StyleUtils = {}));
52594
53258
 
52595
- const VERSION = "7.5.8";
53259
+ const VERSION = "7.5.9";
52596
53260
  /**
52597
53261
  * Updates the environment instance used by bruce-cesium to one specified.
52598
53262
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.