bruce-cesium 7.5.8 → 7.6.0

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.
@@ -7887,6 +7888,22 @@
7887
7888
  radius: Math.max(width, height) / 2
7888
7889
  };
7889
7890
  };
7891
+ /*
7892
+ * The on-screen centre and half-size of a model too small for its box, so the box is ringed like a glyph.
7893
+ */
7894
+ const measureModel = (anchor) => {
7895
+ const hull = layer.modelBoxes.Hull(this.id);
7896
+ if (!(hull === null || hull === void 0 ? void 0 : hull.length)) {
7897
+ return null;
7898
+ }
7899
+ const xs = hull.map(point => point.x), ys = hull.map(point => point.y);
7900
+ const left = Math.min(...xs), top = Math.min(...ys);
7901
+ const width = Math.max(...xs) - left, height = Math.max(...ys) - top;
7902
+ if (Math.max(width, height) >= MODEL_BOX_MIN_SIZE) {
7903
+ return null;
7904
+ }
7905
+ return { x: left + width / 2 - anchor.x, y: top + height / 2 - anchor.y, radius: Math.max(width, height) / 2 };
7906
+ };
7890
7907
  // Only CAD tilesets and model Entities are assembly parts with a geometry box; anything else stays a point.
7891
7908
  const boxable = Boolean(normalLeader && ((_r = this.rego) === null || _r === void 0 ? void 0 : _r.entityId) && ((visual instanceof Cesium.Cesium3DTileFeature && this.rego.tilesetType === BModels.Tileset.EType.Cad) ||
7892
7909
  (visual instanceof Cesium.Entity && Boolean(visual.model))));
@@ -7945,7 +7962,7 @@
7945
7962
  }
7946
7963
  let cursorMotion = this.cursorMotion;
7947
7964
  const project = () => {
7948
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
7965
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
7949
7966
  if (this.disposed || !this.viewer || this.viewer.isDestroyed() || ((_a = this.visible) === null || _a === void 0 ? void 0 : _a.call(this)) === false ||
7950
7967
  (presentation === "icon" && this.options.iconScale === 0)) {
7951
7968
  return null;
@@ -7988,7 +8005,8 @@
7988
8005
  const boxBlend = this.morphToBox(boxed, shownOnce ? (_f = (_e = this.options.anchorRing) === null || _e === void 0 ? void 0 : _e.transitionDuration) !== null && _f !== void 0 ? _f : 200 : 0);
7989
8006
  // Eased between the ring and where the stem meets the box, so gaining or losing a box never jumps.
7990
8007
  const boxTarget = this.boxMorph.anchor;
7991
- const ringPoint = { x: anchor.x + pixelOffset.x, y: anchor.y + pixelOffset.y };
8008
+ const model = ((_g = this.modelBox) === null || _g === void 0 ? void 0 : _g.corners) ? measureModel(anchor) : null;
8009
+ const ringPoint = { x: anchor.x + (model || pixelOffset).x, y: anchor.y + (model || pixelOffset).y };
7992
8010
  const boxAnchor = boxBlend > 0 && boxTarget ? {
7993
8011
  x: ringPoint.x + (boxTarget.x - ringPoint.x) * boxBlend,
7994
8012
  y: ringPoint.y + (boxTarget.y - ringPoint.y) * boxBlend
@@ -7999,13 +8017,13 @@
7999
8017
  return null;
8000
8018
  if (onScreen && !this.imageAsked && this.groupId && !HasIcon(this.options) && !this.options.photo) {
8001
8019
  this.imageAsked = true;
8002
- (_g = this.visualRegister) === null || _g === void 0 ? void 0 : _g.RequestLabelImage((_h = this.rego) === null || _h === void 0 ? void 0 : _h.entityId);
8020
+ (_h = this.visualRegister) === null || _h === void 0 ? void 0 : _h.RequestLabelImage((_j = this.rego) === null || _j === void 0 ? void 0 : _j.entityId);
8003
8021
  }
8004
8022
  const transientPivot = (this.Transient && !this.Persistent ?
8005
- (this.hoverFade.to === 0 ? this.retainedHoverPosition : (_j = this.visualRegister) === null || _j === void 0 ? void 0 : _j.GetLabelHoverPosition((_k = this.rego) === null || _k === void 0 ? void 0 : _k.entityId, (_l = this.rego) === null || _l === void 0 ? void 0 : _l.menuItemId)) : undefined);
8023
+ (this.hoverFade.to === 0 ? this.retainedHoverPosition : (_k = this.visualRegister) === null || _k === void 0 ? void 0 : _k.GetLabelHoverPosition((_l = this.rego) === null || _l === void 0 ? void 0 : _l.entityId, (_m = this.rego) === null || _m === void 0 ? void 0 : _m.menuItemId)) : undefined);
8006
8024
  const hover = (presentation !== "icon" ?
8007
8025
  (this.Transient && !this.Persistent && this.hoverFade.to === 0 ? this.retainedHoverPosition :
8008
- (_m = this.visualRegister) === null || _m === void 0 ? void 0 : _m.GetLabelHoverPosition((_o = this.rego) === null || _o === void 0 ? void 0 : _o.entityId, (_p = this.rego) === null || _p === void 0 ? void 0 : _p.menuItemId)) : undefined);
8026
+ (_o = this.visualRegister) === null || _o === void 0 ? void 0 : _o.GetLabelHoverPosition((_p = this.rego) === null || _p === void 0 ? void 0 : _p.entityId, (_q = this.rego) === null || _q === void 0 ? void 0 : _q.menuItemId)) : undefined);
8009
8027
  const reused = !this.Transient || this.Persistent;
8010
8028
  const now = Date.now();
8011
8029
  const animateCursor = () => {
@@ -8029,13 +8047,13 @@
8029
8047
  const cursorAmount = cursorMotion.amount;
8030
8048
  leader = normalLeader || cursorAmount > 0;
8031
8049
  // An icon keeps its callout centred on the glyph rather than following the cursor across it.
8032
- const icon = boxAnchor ? null : measureIcon();
8050
+ const icon = boxAnchor ? null : measureIcon() || model;
8033
8051
  const iconRing = icon ? Math.max(ICON_RING_MIN, icon.radius + ICON_RING_PADDING) : 0;
8034
8052
  // A boxed model is pinned the same way, with its stem leaving the top of the box outline.
8035
8053
  const pinned = Boolean(icon || boxAnchor);
8036
8054
  const pivot = pinned ? undefined : transientPivot;
8037
- let x = boxAnchor ? boxAnchor.x : ((_q = pivot === null || pivot === void 0 ? void 0 : pivot.x) !== null && _q !== void 0 ? _q : anchor.x) + (icon || pixelOffset).x;
8038
- let y = boxAnchor ? boxAnchor.y : ((_r = pivot === null || pivot === void 0 ? void 0 : pivot.y) !== null && _r !== void 0 ? _r : anchor.y) + (icon || pixelOffset).y;
8055
+ let x = boxAnchor ? boxAnchor.x : ((_r = pivot === null || pivot === void 0 ? void 0 : pivot.x) !== null && _r !== void 0 ? _r : anchor.x) + (icon || pixelOffset).x;
8056
+ let y = boxAnchor ? boxAnchor.y : ((_s = pivot === null || pivot === void 0 ? void 0 : pivot.y) !== null && _s !== void 0 ? _s : anchor.y) + (icon || pixelOffset).y;
8039
8057
  if (cursorAmount > 0 && !pinned) {
8040
8058
  // A leader-less label starts centred on its anchor; preserve that box as it lifts away.
8041
8059
  if (!normalLeader) {
@@ -8090,7 +8108,7 @@
8090
8108
  const width = ele.offsetWidth / coordinates.scaleX;
8091
8109
  const height = ele.offsetHeight / coordinates.scaleY;
8092
8110
  let clusterBounds;
8093
- if ((_s = this.options.cluster) === null || _s === void 0 ? void 0 : _s.enabled) {
8111
+ if ((_t = this.options.cluster) === null || _t === void 0 ? void 0 : _t.enabled) {
8094
8112
  const textWidth = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetWidth) || 0;
8095
8113
  const textHeight = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetHeight) || 0;
8096
8114
  // A photo is never part of the footprint, so its arrival cannot change which cluster a label joins.
@@ -8124,7 +8142,7 @@
8124
8142
  if (!leader && (bounds.x + bounds.width < 0 || bounds.y + bounds.height < 0 || bounds.x > this.viewer.canvas.clientWidth || bounds.y > this.viewer.canvas.clientHeight)) {
8125
8143
  return null;
8126
8144
  }
8127
- const state = (_t = this.visualRegister) === null || _t === void 0 ? void 0 : _t.GetState({ entityId: (_u = this.rego) === null || _u === void 0 ? void 0 : _u.entityId, menuItemId: (_v = this.rego) === null || _v === void 0 ? void 0 : _v.menuItemId });
8145
+ const state = (_u = this.visualRegister) === null || _u === void 0 ? void 0 : _u.GetState({ entityId: (_v = this.rego) === null || _v === void 0 ? void 0 : _v.entityId, menuItemId: (_w = this.rego) === null || _w === void 0 ? void 0 : _w.menuItemId });
8128
8146
  const opacity = this.prePositioned || hoverOnly || MAX_DISTANCE === Infinity ? 1 : 1 - distance / MAX_DISTANCE;
8129
8147
  return {
8130
8148
  canFlip: leader && presentation !== "icon", hasLeader: leader,
@@ -8132,8 +8150,8 @@
8132
8150
  transient: this.Transient || Boolean(hover) || cursorAmount > 0,
8133
8151
  extendedGeometry: visual instanceof Cesium.Entity && Boolean(visual.polygon || visual.polyline || visual.corridor),
8134
8152
  viewportWidth: this.viewer.canvas.clientWidth,
8135
- id: this.id, entityKey: ((_w = this.rego) === null || _w === void 0 ? void 0 : _w.entityId) ? JSON.stringify([this.rego.entityId, this.rego.menuItemId || "", this.Transient && !this.Persistent ? "hover" : ""]) : undefined,
8136
- entityId: (_x = this.rego) === null || _x === void 0 ? void 0 : _x.entityId, group: this.groupId, x, y, width, height, anchorX, anchorY, depth, leaderLength, leaderRun, leaderSide, distance,
8153
+ id: this.id, entityKey: ((_x = this.rego) === null || _x === void 0 ? void 0 : _x.entityId) ? JSON.stringify([this.rego.entityId, this.rego.menuItemId || "", this.Transient && !this.Persistent ? "hover" : ""]) : undefined,
8154
+ entityId: (_y = this.rego) === null || _y === void 0 ? void 0 : _y.entityId, group: this.groupId, x, y, width, height, anchorX, anchorY, depth, leaderLength, leaderRun, leaderSide, distance,
8137
8155
  iconRadius: icon === null || icon === void 0 ? void 0 : icon.radius, modelBox: boxed || undefined, boxBlend,
8138
8156
  // Priority shows the hovered member individually without dragging its cluster along.
8139
8157
  opacity: waiting ? 0 : opacity, priority: hover || cursorAmount > 0 || (state === null || state === void 0 ? void 0 : state.highlighted) ? 1 : 0, options: this.options, text: originalText, clusterBounds
@@ -13789,7 +13807,9 @@
13789
13807
  entityId: params.entityId,
13790
13808
  menuItemId: params.menuItemId,
13791
13809
  position: params.position ? { ...params.position } : undefined,
13792
- createIfMissing: params.createIfMissing !== false
13810
+ createIfMissing: params.createIfMissing !== false,
13811
+ text: params.text,
13812
+ pos3d: params.pos3d ? Cesium.Cartesian3.clone(params.pos3d) : undefined
13793
13813
  };
13794
13814
  const doc = this.viewer.container.ownerDocument;
13795
13815
  // Only a label following the cursor should go when the cursor leaves the canvas.
@@ -13805,6 +13825,13 @@
13805
13825
  this.SyncLabelHover();
13806
13826
  this.viewer.scene.requestRender();
13807
13827
  }
13828
+ /*
13829
+ * Which Entity holds the label hover, if any, so a caller can leave another's hover alone.
13830
+ */
13831
+ GetLabelHoverTarget() {
13832
+ const hover = this.labelHover;
13833
+ return hover ? { entityId: hover.entityId, menuItemId: hover.menuItemId } : undefined;
13834
+ }
13808
13835
  /*
13809
13836
  * Whether the label hover is on this Entity, with or without a cursor position.
13810
13837
  */
@@ -13817,7 +13844,7 @@
13817
13844
  return (hover === null || hover === void 0 ? void 0 : hover.entityId) === entityId && (hover.menuItemId == null || hover.menuItemId === menuItemId) ? hover.position : undefined;
13818
13845
  }
13819
13846
  SyncLabelHover() {
13820
- var _a, _b, _c;
13847
+ var _a, _b, _c, _d;
13821
13848
  const hover = this.labelHover;
13822
13849
  if (!hover || this.disposed) {
13823
13850
  return;
@@ -13849,12 +13876,24 @@
13849
13876
  this.ReleaseHoverLabel(true);
13850
13877
  return;
13851
13878
  }
13879
+ const hoverOptions = {
13880
+ presentation: "text",
13881
+ text: (_b = hover.text) !== null && _b !== void 0 ? _b : group === null || group === void 0 ? void 0 : group.options.text,
13882
+ selectOnClick: false
13883
+ };
13852
13884
  if (this.hoverLabel && !this.hoverLabel.Disposed) {
13885
+ // Rebuilds only when the text changed, as it does when the pointer crosses a cell.
13886
+ if (hover.pos3d && !Cesium.Cartesian3.equals(hover.pos3d, this.hoverLabel.pos3d)) {
13887
+ this.hoverLabel.pos3d = hover.pos3d;
13888
+ }
13889
+ if (hover.text != null) {
13890
+ this.hoverLabel.Configure(hoverOptions);
13891
+ }
13853
13892
  this.hoverLabel.SetHoverVisible(true);
13854
13893
  return;
13855
13894
  }
13856
13895
  // 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)) {
13896
+ if (!rego && group && [group.item.longitude, group.item.latitude, (_c = group.item.altitude) !== null && _c !== void 0 ? _c : 0].every(Number.isFinite)) {
13858
13897
  rego = {
13859
13898
  entityId: hover.entityId,
13860
13899
  menuItemId: hover.menuItemId,
@@ -13876,15 +13915,12 @@
13876
13915
  rego,
13877
13916
  viewer: this.viewer,
13878
13917
  visualRegister: this,
13879
- api: (_c = this.ApiGetters) === null || _c === void 0 ? void 0 : _c.GetBruceApi(),
13918
+ api: (_d = this.ApiGetters) === null || _d === void 0 ? void 0 : _d.GetBruceApi(),
13880
13919
  persistent: false,
13881
13920
  transient: true,
13882
13921
  hoverRingState: this.hoverRingState,
13883
- options: {
13884
- presentation: "text",
13885
- text: group === null || group === void 0 ? void 0 : group.options.text,
13886
- selectOnClick: false
13887
- },
13922
+ pos3d: hover.pos3d,
13923
+ options: hoverOptions,
13888
13924
  visible: () => {
13889
13925
  var _a;
13890
13926
  const current = this.GetState(hover);
@@ -46708,6 +46744,10 @@
46708
46744
  TextureFrameSeriesAnimator.AppearanceSignature = AppearanceSignature;
46709
46745
  const MAX_COMPOSITE_TEXELS = 2048;
46710
46746
  const VALUE_DILATE_PASSES = 2;
46747
+ // Decoded frame bytes kept at once. A 2048 square composite is 16 MB, so a long series cannot all stay.
46748
+ const MAX_CACHED_FRAME_BYTES = 256 * 1024 * 1024;
46749
+ // Fetched bytes kept for decoding again, a small fraction of what they decode to.
46750
+ const MAX_RAW_FRAME_BYTES = 96 * 1024 * 1024;
46711
46751
  /*
46712
46752
  * Rewrites a packed 16 bit value as the 8 bit grey the colour ramps index on.
46713
46753
  *
@@ -46728,19 +46768,33 @@
46728
46768
  /*
46729
46769
  * Bleeds covered values outward into uncovered texels, leaving alpha untouched.
46730
46770
  */
46731
- function dilateValues(value, width, height, passes, packed) {
46771
+ function DilateValues(value, width, height, passes, packed) {
46732
46772
  const texels = width * height;
46733
- const filled = new Uint8Array(texels);
46773
+ // 255 for a texel that had data to begin with, otherwise the 1 based pass that filled it, so a pass
46774
+ // reads only texels filled before it and can work in place.
46775
+ const stamp = new Uint8Array(texels);
46734
46776
  for (let p = 0; p < texels; p++) {
46735
- filled[p] = value[p * 4 + 3] !== 0 ? 1 : 0;
46777
+ stamp[p] = value[p * 4 + 3] !== 0 ? 255 : 0;
46736
46778
  }
46779
+ // Whether a texel or a horizontal neighbour had data, so empty space is skipped in three reads.
46780
+ const across = new Uint8Array(texels);
46737
46781
  for (let pass = 0; pass < passes; pass++) {
46738
- const source = value.slice();
46739
- const wasFilled = filled.slice();
46782
+ const current = pass + 1;
46783
+ const had = (p) => stamp[p] !== 0 && stamp[p] !== current;
46784
+ for (let y = 0; y < height; y++) {
46785
+ const row = y * width;
46786
+ for (let x = 0; x < width; x++) {
46787
+ const p = row + x;
46788
+ across[p] = (had(p) || (x > 0 && had(p - 1)) || (x + 1 < width && had(p + 1))) ? 1 : 0;
46789
+ }
46790
+ }
46740
46791
  for (let y = 0; y < height; y++) {
46741
46792
  for (let x = 0; x < width; x++) {
46742
46793
  const p = y * width + x;
46743
- if (wasFilled[p]) {
46794
+ if (stamp[p] !== 0) {
46795
+ continue;
46796
+ }
46797
+ if (!across[p] && !(y > 0 && across[p - width]) && !(y + 1 < height && across[p + width])) {
46744
46798
  continue;
46745
46799
  }
46746
46800
  let sum = 0;
@@ -46749,13 +46803,13 @@
46749
46803
  for (let dx = -1; dx <= 1; dx++) {
46750
46804
  const nx = x + dx;
46751
46805
  const ny = y + dy;
46752
- if (nx < 0 || ny < 0 || nx >= width || ny >= height || !wasFilled[ny * width + nx]) {
46806
+ if (nx < 0 || ny < 0 || nx >= width || ny >= height || !had(ny * width + nx)) {
46753
46807
  continue;
46754
46808
  }
46755
46809
  const at = (ny * width + nx) * 4;
46756
46810
  // Averaged as a value rather than per byte: averaging a packed low byte
46757
46811
  // on its own wraps at every 256th step and speckles the bled edge.
46758
- sum += packed ? source[at] * 256 + source[at + 1] : source[at];
46812
+ sum += packed ? value[at] * 256 + value[at + 1] : value[at];
46759
46813
  hits++;
46760
46814
  }
46761
46815
  }
@@ -46771,18 +46825,25 @@
46771
46825
  value[p * 4 + 1] = v;
46772
46826
  value[p * 4 + 2] = v;
46773
46827
  }
46774
- filled[p] = 1;
46828
+ stamp[p] = current;
46775
46829
  }
46776
46830
  }
46777
46831
  }
46778
46832
  }
46779
46833
  }
46834
+ TextureFrameSeriesAnimator.DilateValues = DilateValues;
46835
+ /*
46836
+ * Bytes one cached frame holds, counting a buffer shared between colour and value once.
46837
+ */
46838
+ function frameBytes(pixels, value) {
46839
+ return (pixels ? pixels.byteLength : 0) + (value && value !== pixels ? value.byteLength : 0);
46840
+ }
46780
46841
  const DEFAULT_CROSSFADE_MS = 900;
46781
46842
  const DEFAULT_LOW_COLOR = { red: 255, green: 255, blue: 255, alpha: 0.12 };
46782
46843
  const DEFAULT_HIGH_COLOR = { red: 21, green: 96, blue: 196, alpha: 0.92 };
46783
46844
  class Animator {
46784
46845
  constructor(options) {
46785
- var _a, _b, _c;
46846
+ var _a, _b, _c, _d;
46786
46847
  this.removeOnTick = null;
46787
46848
  this.removeCrossfadeTick = null;
46788
46849
  this.disposed = false;
@@ -46813,6 +46874,15 @@
46813
46874
  this.extremesLoad = null;
46814
46875
  this.floorPixels = null;
46815
46876
  this.ceilingPixels = null;
46877
+ // How far through the fade from the previous value frame to the presented one, 1 when settled.
46878
+ this.fadeMix = 1;
46879
+ // Where the current fade started from, above zero when it took over from one still running.
46880
+ this.fadeMixStart = 0;
46881
+ // Version the value canvas was last written at, since in value only mode it is written on demand.
46882
+ this.valueCanvasVersion = -1;
46883
+ this.rawBytes = 0;
46884
+ this.cachedBytes = 0;
46885
+ this.lastFrameBytes = 0;
46816
46886
  if (!options.entity.polygon) {
46817
46887
  throw new Error("TextureFrameSeriesAnimator requires an entity with polygon graphics.");
46818
46888
  }
@@ -46860,6 +46930,9 @@
46860
46930
  this.maskBaseline = (_c = options.maskBaseline) !== null && _c !== void 0 ? _c : this.tiles.some((tile) => Boolean(tile.BaselineMask));
46861
46931
  this.valueCanvas = this.produceValueCanvas ? document.createElement("canvas") : null;
46862
46932
  this.driveMaterial = options.driveMaterial !== false;
46933
+ this.valueOnly = this.produceValueCanvas && !this.driveMaterial;
46934
+ this.rawCache = new Array(this.frames.length).fill(null);
46935
+ this.cacheBudget = (_d = options.cacheBytes) !== null && _d !== void 0 ? _d : MAX_CACHED_FRAME_BYTES;
46863
46936
  this.originalMaterial = options.entity.polygon.material;
46864
46937
  this.imageProperty = new Cesium.CallbackProperty(() => this.pool[this.poolIdx], false);
46865
46938
  if (this.driveMaterial) {
@@ -46925,7 +46998,40 @@
46925
46998
  * or null unless the instance was constructed with produceValueCanvas.
46926
46999
  */
46927
47000
  GetValueCanvas() {
46928
- return this.valueDims ? this.valueCanvas : null;
47001
+ if (!this.valueDims) {
47002
+ return null;
47003
+ }
47004
+ if (this.valueOnly && this.valueCanvasVersion !== this.presentVersion && this.displayedValuePixels) {
47005
+ this.writeInto(this.valueCanvas, this.displayedValuePixels, this.valueDims);
47006
+ this.valueCanvasVersion = this.presentVersion;
47007
+ }
47008
+ return this.valueCanvas;
47009
+ }
47010
+ /**
47011
+ * The presented frame's value pixels, or null before one. Shares the cached array, so never write to it.
47012
+ */
47013
+ GetValueRaster() {
47014
+ if (!this.valueDims || !this.displayedValuePixels) {
47015
+ return null;
47016
+ }
47017
+ return { data: this.displayedValuePixels, width: this.valueDims.width, height: this.valueDims.height };
47018
+ }
47019
+ /**
47020
+ * The two value frames being faded between and the mix, or null unless the material is not driven.
47021
+ */
47022
+ GetValueFade() {
47023
+ if (!this.valueOnly || !this.valueDims || !this.displayedValuePixels) {
47024
+ return null;
47025
+ }
47026
+ const previous = this.fadeFromValuePixels;
47027
+ const from = previous && previous.length === this.displayedValuePixels.length ? previous : null;
47028
+ return {
47029
+ from,
47030
+ to: this.displayedValuePixels,
47031
+ width: this.valueDims.width,
47032
+ height: this.valueDims.height,
47033
+ mix: from ? this.fadeMix : 1
47034
+ };
46929
47035
  }
46930
47036
  /**
46931
47037
  * Increments every time a frame is presented, so a consumer holding the value canvas can tell
@@ -46992,7 +47098,11 @@
46992
47098
  if (this.currentFrameIndex < 0) {
46993
47099
  return this.frameCache[0] ? -1 : 0;
46994
47100
  }
46995
- for (let d = 0; d < n; d++) {
47101
+ // Full once another frame will not fit, so a prefetch only displaces a frame further from the
47102
+ // playhead than itself and never trades the two ends of the window back and forth.
47103
+ const full = this.cachedBytes + this.lastFrameBytes > this.cacheBudget;
47104
+ const reach = full ? this.farthestCachedDistance() : n;
47105
+ for (let d = 0; d < Math.min(n, reach); d++) {
46996
47106
  const forward = this.currentFrameIndex + d;
46997
47107
  if (forward < n && !this.frameCache[forward]) {
46998
47108
  return forward;
@@ -47004,6 +47114,68 @@
47004
47114
  }
47005
47115
  return -1;
47006
47116
  }
47117
+ farthestCachedDistance() {
47118
+ let farthest = 0;
47119
+ for (let i = 0; i < this.frameCache.length; i++) {
47120
+ if (this.frameCache[i]) {
47121
+ farthest = Math.max(farthest, Math.abs(i - this.currentFrameIndex));
47122
+ }
47123
+ }
47124
+ return farthest;
47125
+ }
47126
+ /*
47127
+ * Drops decoded frames furthest from the playhead until the cache fits its budget again.
47128
+ */
47129
+ evictDecoded(keep) {
47130
+ // A tinted fade reads its target from the cache each tick, where a value fade holds the arrays.
47131
+ const fading = !this.valueOnly && this.fadeStartMs !== null ? this.fadeToIndex : -1;
47132
+ while (this.cachedBytes > this.cacheBudget) {
47133
+ let victim = -1;
47134
+ let distance = -1;
47135
+ for (let i = 0; i < this.frameCache.length; i++) {
47136
+ if (!this.frameCache[i] || i === keep || i === this.currentFrameIndex || i === fading) {
47137
+ continue;
47138
+ }
47139
+ const d = Math.abs(i - this.currentFrameIndex);
47140
+ if (d > distance) {
47141
+ distance = d;
47142
+ victim = i;
47143
+ }
47144
+ }
47145
+ if (victim < 0) {
47146
+ return;
47147
+ }
47148
+ this.cachedBytes -= frameBytes(this.frameCache[victim], this.valueCache[victim]);
47149
+ this.frameCache[victim] = null;
47150
+ this.valueCache[victim] = null;
47151
+ }
47152
+ }
47153
+ /*
47154
+ * Keeps a frame's fetched bytes unless they are large, as a tile major archive's are.
47155
+ */
47156
+ keepRaw(idx, buffer, base) {
47157
+ if (buffer.byteLength > MAX_RAW_FRAME_BYTES / 16) {
47158
+ return;
47159
+ }
47160
+ this.rawCache[idx] = { buffer, base };
47161
+ this.rawBytes += buffer.byteLength;
47162
+ while (this.rawBytes > MAX_RAW_FRAME_BYTES) {
47163
+ let victim = -1;
47164
+ let distance = -1;
47165
+ for (let i = 0; i < this.rawCache.length; i++) {
47166
+ const d = Math.abs(i - this.currentFrameIndex);
47167
+ if (this.rawCache[i] && i !== idx && d > distance) {
47168
+ distance = d;
47169
+ victim = i;
47170
+ }
47171
+ }
47172
+ if (victim < 0) {
47173
+ return;
47174
+ }
47175
+ this.rawBytes -= this.rawCache[victim].buffer.byteLength;
47176
+ this.rawCache[victim] = null;
47177
+ }
47178
+ }
47007
47179
  pumpFetchQueue() {
47008
47180
  if (this.disposed || this.inFlightIndex !== -1) {
47009
47181
  return;
@@ -47047,10 +47219,18 @@
47047
47219
  lo = Math.min(lo, at.ByteOffset);
47048
47220
  hi = Math.max(hi, at.ByteOffset + at.ByteLength);
47049
47221
  }
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;
47222
+ let buffer;
47223
+ const raw = this.rawCache[idx];
47224
+ if (raw && raw.base === lo) {
47225
+ buffer = raw.buffer;
47226
+ }
47227
+ else {
47228
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
47229
+ buffer = await response.arrayBuffer();
47230
+ if (this.disposed) {
47231
+ return;
47232
+ }
47233
+ this.keepRaw(idx, buffer, lo);
47054
47234
  }
47055
47235
  // Awaited before decoding rather than applied later, so no frame is ever cached unmasked and
47056
47236
  // then corrected once the mask lands.
@@ -47062,9 +47242,15 @@
47062
47242
  if (this.disposed) {
47063
47243
  return;
47064
47244
  }
47245
+ if (this.frameCache[idx]) {
47246
+ this.cachedBytes -= frameBytes(this.frameCache[idx], this.valueCache[idx]);
47247
+ }
47065
47248
  this.frameCache[idx] = decoded.pixels;
47066
47249
  this.frameDims[idx] = { width: decoded.width, height: decoded.height };
47067
47250
  this.valueCache[idx] = decoded.valuePixels || null;
47251
+ this.lastFrameBytes = frameBytes(decoded.pixels, decoded.valuePixels || null);
47252
+ this.cachedBytes += this.lastFrameBytes;
47253
+ this.evictDecoded(idx);
47068
47254
  if (this.shouldPresent(idx, isTarget)) {
47069
47255
  this.beginCrossfadeTo(idx);
47070
47256
  }
@@ -47134,6 +47320,13 @@
47134
47320
  */
47135
47321
  tint(imageData) {
47136
47322
  const canvas = { width: imageData.width, height: imageData.height };
47323
+ if (this.valueOnly) {
47324
+ // One array serves as both, since nothing will ever display the colour.
47325
+ const values = imageData.data;
47326
+ this.applyBaselineMask(values, undefined, canvas.width, canvas.height);
47327
+ DilateValues(values, canvas.width, canvas.height, VALUE_DILATE_PASSES, IsPackedValue(this.metadata));
47328
+ return { pixels: values, width: canvas.width, height: canvas.height, valuePixels: values };
47329
+ }
47137
47330
  // Copied before tinting: the ramp is a lerp between two colours, so the value cannot be
47138
47331
  // recovered from the tinted pixels afterwards.
47139
47332
  const valuePixels = this.produceValueCanvas ? new Uint8ClampedArray(imageData.data) : undefined;
@@ -47154,7 +47347,7 @@
47154
47347
  }
47155
47348
  this.applyBaselineMask(imageData.data, valuePixels, canvas.width, canvas.height);
47156
47349
  if (valuePixels) {
47157
- dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES, IsPackedValue(this.metadata));
47350
+ DilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES, IsPackedValue(this.metadata));
47158
47351
  }
47159
47352
  return { pixels: imageData.data, width: canvas.width, height: canvas.height, valuePixels };
47160
47353
  }
@@ -47412,6 +47605,10 @@
47412
47605
  }
47413
47606
  }
47414
47607
  beginCrossfadeTo(idx) {
47608
+ if (this.valueOnly) {
47609
+ this.beginValueFadeTo(idx);
47610
+ return;
47611
+ }
47415
47612
  const to = this.frameCache[idx];
47416
47613
  this.fadeFromValuePixels = this.displayedValuePixels === null && this.valueCache[idx]
47417
47614
  ? new Uint8ClampedArray(this.valueCache[idx].length)
@@ -47421,12 +47618,53 @@
47421
47618
  this.fadeToDims = this.frameDims[idx];
47422
47619
  this.fadeStartMs = Now();
47423
47620
  }
47621
+ /*
47622
+ * Presents a value frame at once and leaves the fade to the consumer, which blends on the GPU.
47623
+ */
47624
+ beginValueFadeTo(idx) {
47625
+ const to = this.valueCache[idx];
47626
+ if (!to || to === this.displayedValuePixels) {
47627
+ return;
47628
+ }
47629
+ // A new target mid fade keeps whichever frame is mostly on screen and carries on from the current
47630
+ // mix, so frames landing in quick succession do not jump the sheet back.
47631
+ const midFade = this.fadeStartMs !== null && Boolean(this.fadeFromValuePixels);
47632
+ if (midFade && this.fadeMix < 0.5) {
47633
+ this.fadeMixStart = this.fadeMix;
47634
+ }
47635
+ else {
47636
+ this.fadeFromValuePixels = this.displayedValuePixels;
47637
+ this.fadeMixStart = 0;
47638
+ }
47639
+ this.displayedValuePixels = to;
47640
+ this.valueDims = this.frameDims[idx];
47641
+ this.fadeToIndex = idx;
47642
+ this.fadeMix = this.fadeFromValuePixels ? this.fadeMixStart : 1;
47643
+ this.fadeStartMs = this.fadeFromValuePixels ? Now() : null;
47644
+ this.presentVersion++;
47645
+ this.requestRender();
47646
+ }
47647
+ requestRender() {
47648
+ if (this.viewer && this.viewer.scene && this.viewer.scene.requestRenderMode) {
47649
+ this.viewer.scene.requestRender();
47650
+ }
47651
+ }
47424
47652
  tickCrossfade() {
47425
47653
  if (this.disposed || this.fadeStartMs === null) {
47426
47654
  return;
47427
47655
  }
47428
47656
  const t = Math.min(1, (Now() - this.fadeStartMs) / this.crossfadeMs);
47429
47657
  const eased = easeInOutQuad(t);
47658
+ if (this.valueOnly) {
47659
+ this.fadeMix = this.fadeMixStart + (1 - this.fadeMixStart) * eased;
47660
+ if (t >= 1) {
47661
+ this.fadeStartMs = null;
47662
+ this.fadeFromValuePixels = null;
47663
+ this.fadeMix = 1;
47664
+ }
47665
+ this.requestRender();
47666
+ return;
47667
+ }
47430
47668
  const from = this.fadeFromPixels;
47431
47669
  const to = this.frameCache[this.fadeToIndex];
47432
47670
  const n = Math.min(from.length, to.length);
@@ -47466,9 +47704,7 @@
47466
47704
  this.valueDims = dims;
47467
47705
  }
47468
47706
  this.presentVersion++;
47469
- if (this.viewer && this.viewer.scene && this.viewer.scene.requestRenderMode) {
47470
- this.viewer.scene.requestRender();
47471
- }
47707
+ this.requestRender();
47472
47708
  }
47473
47709
  }
47474
47710
  TextureFrameSeriesAnimator.Animator = Animator;
@@ -47487,13 +47723,15 @@
47487
47723
  const MIN_VISIBLE_PIXELS = 60;
47488
47724
  const MAX_STRIDE = 512;
47489
47725
  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)";
47726
+ // Scene labels carry a leader and are wider than a value, so they want room and not many of them.
47727
+ const DEFAULT_MIN_SPACING_PIXELS = 140;
47728
+ const DEFAULT_MAX_LABELS = 60;
47729
+ // How far past the edge of the view an anchor may sit before its label is dropped.
47730
+ const LABEL_EDGE_MARGIN_PIXELS = 120;
47731
+ // Distance discount for a label already up when the cap forces a choice.
47732
+ const LABEL_KEEP_BIAS = 0.6;
47733
+ // How long a label outlives its cell leaving the wanted set, so one back a frame later does not blink.
47734
+ const LABEL_LINGER_MS = 300;
47497
47735
  // 8 rather than 5: below this a cell reads as texture rather than as a boundary, and it also
47498
47736
  // bounds the worst case, since a full screen of 8 px cells fits inside MAX_GRID_CELLS.
47499
47737
  const DEFAULT_MIN_GRID_CELL_PIXELS = 8;
@@ -47507,7 +47745,8 @@
47507
47745
  const TILE_OUTLINE_ALPHA = 0.45;
47508
47746
  // Cells per grid square at the coarsest. Beyond this a tile is outlined instead.
47509
47747
  const MAX_GRID_STRIDE = 256;
47510
- const LINE_HEIGHT = 13;
47748
+ // Times the pointer's ground hit is moved onto the sheet's own height before reading a cell.
47749
+ const HOVER_REFINE_STEPS = 3;
47511
47750
  /**
47512
47751
  * A clip outline from a projected ring, or null when the ring cannot be drawn.
47513
47752
  * @param projected one entry per ring vertex, null where the vertex has no window coordinate
@@ -47601,11 +47840,25 @@
47601
47840
  this.sourceMap = null;
47602
47841
  this.source = null;
47603
47842
  this.painted = null;
47843
+ this.paintedRamp = null;
47604
47844
  this.floor = null;
47605
47845
  this.ceiling = null;
47606
47846
  this.disposed = false;
47607
- this.drawn = 0;
47608
47847
  this.stride = 0;
47848
+ this.cellLabels = new Map();
47849
+ this.lingerTimer = null;
47850
+ // What the last draw was made from, so a frame that changes none of it draws nothing.
47851
+ this.drawnView = null;
47852
+ this.drawnInputs = [];
47853
+ this.scratchWorld = new Cesium.Cartesian3();
47854
+ this.scratchWindow = new Cesium.Cartesian2();
47855
+ this.scratchClip = new Cesium.Cartesian4();
47856
+ // The camera's view and projection as one matrix, rebuilt per draw, or null outside 3D.
47857
+ this.viewProjection = null;
47858
+ this.viewProjectionScratch = new Cesium.Matrix4();
47859
+ this.hovering = false;
47860
+ this.hoverFrame = 0;
47861
+ this.hoverAt = null;
47609
47862
  this.viewer = options.viewer;
47610
47863
  this.extent = options.extent;
47611
47864
  this.settings = options.settings || {};
@@ -47615,6 +47868,9 @@
47615
47868
  this.sourceMap = exports.TextureFrameSeriesAnimator.SourceMap(options.metadata);
47616
47869
  this.tiles = options.tiles;
47617
47870
  this.clipRing = options.clipRing;
47871
+ this.heightAt = options.heightAt;
47872
+ this.heightKey = options.heightKey;
47873
+ this.hover = options.hover;
47618
47874
  this.canvas = document.createElement("canvas");
47619
47875
  this.canvas.style.position = "absolute";
47620
47876
  this.canvas.style.inset = "0";
@@ -47624,12 +47880,31 @@
47624
47880
  parent.appendChild(this.canvas);
47625
47881
  }
47626
47882
  this.ctx = this.canvas.getContext("2d");
47883
+ this.onPointerMove = (event) => {
47884
+ const bounds = this.viewer.canvas.getBoundingClientRect();
47885
+ this.hoverAt = { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
47886
+ // At most once a frame, since a pointer reports far faster than anything is drawn.
47887
+ if (!this.hoverFrame) {
47888
+ this.hoverFrame = requestAnimationFrame(() => {
47889
+ this.hoverFrame = 0;
47890
+ this.updateHover();
47891
+ });
47892
+ }
47893
+ };
47894
+ this.onPointerLeave = () => {
47895
+ this.hoverAt = null;
47896
+ this.releaseHover();
47897
+ };
47898
+ if (this.hover && this.hover.register) {
47899
+ this.viewer.canvas.addEventListener("pointermove", this.onPointerMove, { passive: true });
47900
+ this.viewer.canvas.addEventListener("pointerleave", this.onPointerLeave);
47901
+ }
47627
47902
  }
47628
47903
  SetExtent(extent) {
47629
47904
  this.extent = extent;
47630
47905
  }
47631
47906
  /**
47632
- * The current frame's decoded values, as the animator's value canvas already provides them.
47907
+ * The current frame's decoded values.
47633
47908
  * @param source value pixels, one texel per cell
47634
47909
  */
47635
47910
  SetSource(source) {
@@ -47637,11 +47912,17 @@
47637
47912
  }
47638
47913
  /**
47639
47914
  * 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
47915
+ * @param painted RGBA of the presented frame, or null to fall back to the ramp or coverage alone
47641
47916
  */
47642
47917
  SetPainted(painted) {
47643
47918
  this.painted = painted;
47644
47919
  }
47920
+ /**
47921
+ * The surface's 256 step RGBA ramp, for telling whether a cell was painted without a tinted frame.
47922
+ */
47923
+ SetPaintedRamp(ramp) {
47924
+ this.paintedRamp = ramp;
47925
+ }
47645
47926
  /**
47646
47927
  * The archive's per-cell extremes, when it publishes them. Without these a label can still
47647
47928
  * print the current reading, just not the range behind it.
@@ -47651,21 +47932,28 @@
47651
47932
  this.ceiling = ceiling;
47652
47933
  }
47653
47934
  GetDrawnCount() {
47654
- return this.drawn;
47935
+ return this.cellLabels.size;
47655
47936
  }
47656
47937
  Clear() {
47657
47938
  this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
47658
- this.drawn = 0;
47939
+ this.drawnView = null;
47659
47940
  }
47660
47941
  Dispose() {
47661
47942
  this.disposed = true;
47943
+ this.clearCellLabels();
47944
+ this.releaseHover();
47945
+ if (this.hoverFrame) {
47946
+ cancelAnimationFrame(this.hoverFrame);
47947
+ this.hoverFrame = 0;
47948
+ }
47949
+ this.viewer.canvas.removeEventListener("pointermove", this.onPointerMove);
47950
+ this.viewer.canvas.removeEventListener("pointerleave", this.onPointerLeave);
47662
47951
  if (this.canvas.parentElement) {
47663
47952
  this.canvas.parentElement.removeChild(this.canvas);
47664
47953
  }
47665
47954
  }
47666
47955
  /**
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.
47956
+ * Redraws the grid and syncs the value labels, when anything they are drawn from has changed.
47669
47957
  */
47670
47958
  Render() {
47671
47959
  var _a, _b, _c;
@@ -47673,67 +47961,296 @@
47673
47961
  return;
47674
47962
  }
47675
47963
  const view = this.viewer.canvas;
47964
+ const inputs = [this.source, this.painted, this.paintedRamp, this.floor, this.ceiling,
47965
+ view.clientWidth, view.clientHeight, this.heightKey ? this.heightKey() : null];
47966
+ const camera = this.viewer.camera.viewMatrix;
47967
+ if (this.drawnView &&
47968
+ Cesium.Matrix4.equals(this.drawnView, camera) &&
47969
+ inputs.every((input, i) => input === this.drawnInputs[i])) {
47970
+ return;
47971
+ }
47676
47972
  if (this.canvas.width !== view.clientWidth || this.canvas.height !== view.clientHeight) {
47677
47973
  this.canvas.width = view.clientWidth;
47678
47974
  this.canvas.height = view.clientHeight;
47679
47975
  }
47680
47976
  this.Clear();
47977
+ this.drawnView = Cesium.Matrix4.clone(camera, this.drawnView || undefined);
47978
+ this.drawnInputs = inputs;
47979
+ const projection = this.viewer.camera.frustum.projectionMatrix;
47980
+ this.viewProjection = this.viewer.scene.mode === Cesium.SceneMode.SCENE3D
47981
+ ? Cesium.Matrix4.multiply(projection, camera, this.viewProjectionScratch)
47982
+ : null;
47681
47983
  const cols = this.source.width;
47682
47984
  const rows = this.source.height;
47683
47985
  const window = this.visibleTexels(cols, rows);
47684
47986
  if (!window) {
47987
+ this.syncCellLabels([]);
47685
47988
  return;
47686
47989
  }
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));
47990
+ // Measured where the view is looking, since in an oblique view up close the middle of the visible
47991
+ // texels sits out toward the horizon, where cells are a fraction of the size.
47992
+ const focus = this.groundAtScreenCentre();
47993
+ const at = focus ? this.texelAt(focus.lon, focus.lat) : null;
47994
+ const focused = (Boolean(at) &&
47995
+ at.col >= window.col0 &&
47996
+ at.col <= window.col1 &&
47997
+ at.row >= window.row0 &&
47998
+ at.row <= window.row1);
47999
+ const midCol = focused ? at.col : (window.col0 + window.col1) / 2;
48000
+ const midRow = focused ? at.row : (window.row0 + window.row1) / 2;
48001
+ const a = this.projectCell(midCol, midRow, cols, rows);
48002
+ const b = this.projectCell(midCol + 1, midRow + 1, cols, rows);
47691
48003
  if (!a || !b) {
48004
+ // Left as they are, since one frame where the focus will not project is not the data leaving.
47692
48005
  return;
47693
48006
  }
47694
48007
  const cellPixels = Math.min(Math.abs(b.x - a.x), Math.abs(b.y - a.y));
47695
48008
  // Ahead of the label threshold on purpose: a grid still reads at a cell size where a
47696
48009
  // number no longer fits.
47697
48010
  this.drawGrid(window, cols, rows, cellPixels);
47698
- if (cellPixels < ((_a = this.settings.minCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_CELL_PIXELS)) {
48011
+ // By area rather than the shorter side, which an oblique view foreshortens to a fraction of the other.
48012
+ const across = this.projectCell(midCol + 1, midRow, cols, rows);
48013
+ const down = this.projectCell(midCol, midRow + 1, cols, rows);
48014
+ const cellSize = across && down
48015
+ ? Math.sqrt(Math.abs((across.x - a.x) * (down.y - a.y) - (across.y - a.y) * (down.x - a.x)))
48016
+ : cellPixels;
48017
+ const visibleWidth = cellSize * (window.col1 - window.col0 + 1);
48018
+ const visibleHeight = cellSize * (window.row1 - window.row0 + 1);
48019
+ if (cellSize < ((_a = this.settings.minCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_CELL_PIXELS) ||
48020
+ Math.max(visibleWidth, visibleHeight) < MIN_VISIBLE_PIXELS) {
48021
+ this.syncCellLabels([]);
47699
48022
  return;
47700
48023
  }
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
48024
  const maxLabels = (_b = this.settings.maxLabels) !== null && _b !== void 0 ? _b : DEFAULT_MAX_LABELS;
47710
48025
  this.stride = ChooseLabelStride({
47711
- cellPixels,
48026
+ cellPixels: cellSize,
47712
48027
  visibleCols: window.col1 - window.col0 + 1,
47713
48028
  visibleRows: window.row1 - window.row0 + 1,
47714
48029
  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)),
48030
+ minSpacingPixels: (_c = this.settings.minSpacingPixels) !== null && _c !== void 0 ? _c : DEFAULT_MIN_SPACING_PIXELS,
47718
48031
  held: this.stride
47719
48032
  });
47720
- const stride = this.stride;
48033
+ const parts = this.settings.parts || ["value", "floor", "ceiling"];
48034
+ this.syncCellLabels(this.labelCells(window, cols, rows, this.stride, maxLabels, parts));
48035
+ }
48036
+ /*
48037
+ * One labelled cell per lattice block, keyed by the block's origin so it survives frame and stride changes.
48038
+ */
48039
+ labelCells(window, cols, rows, stride, max, parts) {
48040
+ const width = this.canvas.width;
48041
+ const height = this.canvas.height;
47721
48042
  const firstRow = Math.floor(window.row0 / stride) * stride;
47722
48043
  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) {
48044
+ const candidates = [];
48045
+ for (let row = firstRow; row <= window.row1; row += stride) {
48046
+ for (let col = firstCol; col <= window.col1; col += stride) {
47725
48047
  const found = this.readNear(col, row, cols, rows, stride);
47726
48048
  if (!found) {
47727
48049
  continue;
47728
48050
  }
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) {
48051
+ const point = this.projectCell(found.col, found.row, cols, rows);
48052
+ // A margin past the edge, so a label does not drop the moment its anchor does.
48053
+ const margin = LABEL_EDGE_MARGIN_PIXELS;
48054
+ if (!point ||
48055
+ point.x < -margin ||
48056
+ point.y < -margin ||
48057
+ point.x > width + margin ||
48058
+ point.y > height + margin) {
48059
+ continue;
48060
+ }
48061
+ const text = this.labelLines(found.reading, parts).join("\n");
48062
+ if (!text) {
47732
48063
  continue;
47733
48064
  }
47734
- this.drawLabel(point, found.reading, parts, this.cellOutline(found.col, found.row, cols, rows));
48065
+ const key = col + "," + row;
48066
+ const distance = Math.hypot(point.x - width / 2, point.y - height / 2);
48067
+ candidates.push({
48068
+ key,
48069
+ lon: this.lonAt(found.col, cols),
48070
+ lat: this.latAt(found.row, rows),
48071
+ text,
48072
+ // Labels already up outrank new ones, so the capped set does not shuffle as the view pans.
48073
+ rank: this.cellLabels.has(key) ? distance * LABEL_KEEP_BIAS : distance
48074
+ });
47735
48075
  }
47736
48076
  }
48077
+ if (candidates.length > max) {
48078
+ candidates.sort((x, y) => x.rank - y.rank);
48079
+ candidates.length = max;
48080
+ }
48081
+ return candidates;
48082
+ }
48083
+ /*
48084
+ * Keeps one scene label per wanted cell, reusing those that are still wanted.
48085
+ */
48086
+ syncCellLabels(wanted) {
48087
+ var _a;
48088
+ const keep = new Set(wanted.map((w) => w.key));
48089
+ const now = Date.now();
48090
+ let lingering = false;
48091
+ for (const [key, entry] of this.cellLabels) {
48092
+ if (keep.has(key)) {
48093
+ entry.missingSince = undefined;
48094
+ continue;
48095
+ }
48096
+ entry.missingSince = (_a = entry.missingSince) !== null && _a !== void 0 ? _a : now;
48097
+ if (now - entry.missingSince >= LABEL_LINGER_MS) {
48098
+ entry.label.Dispose();
48099
+ this.cellLabels.delete(key);
48100
+ }
48101
+ else {
48102
+ lingering = true;
48103
+ }
48104
+ }
48105
+ // A still camera draws nothing more, so a lingering label is given its own later look.
48106
+ if (lingering && !this.lingerTimer) {
48107
+ this.lingerTimer = setTimeout(() => {
48108
+ this.lingerTimer = null;
48109
+ if (!this.disposed) {
48110
+ this.drawnView = null;
48111
+ this.viewer.scene.requestRender();
48112
+ }
48113
+ }, LABEL_LINGER_MS);
48114
+ }
48115
+ for (const want of wanted) {
48116
+ const pos3d = Cesium.Cartesian3.fromDegrees(want.lon, want.lat, this.surfaceHeight(want.lon, want.lat));
48117
+ const options = {
48118
+ presentation: "text",
48119
+ text: want.text,
48120
+ leader: true,
48121
+ selectOnClick: false
48122
+ };
48123
+ const existing = this.cellLabels.get(want.key);
48124
+ if (existing) {
48125
+ if (!Cesium.Cartesian3.equalsEpsilon(existing.label.pos3d, pos3d, 0, 0.01)) {
48126
+ existing.label.pos3d = pos3d;
48127
+ }
48128
+ if (existing.text !== want.text) {
48129
+ existing.label.Configure(options);
48130
+ existing.text = want.text;
48131
+ }
48132
+ continue;
48133
+ }
48134
+ const id = (this.hover ? this.hover.entityId : "texture-value") + "#cell:" + want.key;
48135
+ const label = new exports.EntityLabel.Label({
48136
+ rego: {
48137
+ entityId: id,
48138
+ menuItemId: this.hover ? this.hover.menuItemId : undefined,
48139
+ name: want.text,
48140
+ priority: 0,
48141
+ // No graphic, since a point or billboard visual pulls a label back to itself on rebuild.
48142
+ visual: new Cesium.Entity({ position: pos3d })
48143
+ },
48144
+ viewer: this.viewer,
48145
+ pos3d,
48146
+ options,
48147
+ persistent: true,
48148
+ visible: () => !this.disposed
48149
+ });
48150
+ this.cellLabels.set(want.key, { label, text: want.text });
48151
+ }
48152
+ }
48153
+ clearCellLabels() {
48154
+ if (this.lingerTimer) {
48155
+ clearTimeout(this.lingerTimer);
48156
+ this.lingerTimer = null;
48157
+ }
48158
+ for (const entry of this.cellLabels.values()) {
48159
+ entry.label.Dispose();
48160
+ }
48161
+ this.cellLabels.clear();
48162
+ }
48163
+ /*
48164
+ * Shows the cell under the pointer through the register's hover label, unless something else holds it.
48165
+ */
48166
+ updateHover() {
48167
+ const hover = this.hover;
48168
+ if (this.disposed || !hover || !hover.register || !this.source || !this.hoverAt) {
48169
+ return;
48170
+ }
48171
+ const register = hover.register;
48172
+ const target = register.GetLabelHoverTarget();
48173
+ const ours = Boolean(target) && register.IsLabelHovered(hover.entityId, hover.menuItemId);
48174
+ if (target && !ours) {
48175
+ this.hovering = false;
48176
+ return;
48177
+ }
48178
+ const under = this.readingUnder(this.hoverAt.x, this.hoverAt.y);
48179
+ if (!under) {
48180
+ this.releaseHover();
48181
+ return;
48182
+ }
48183
+ register.SetLabelHover({
48184
+ entityId: hover.entityId,
48185
+ menuItemId: hover.menuItemId,
48186
+ position: { x: this.hoverAt.x, y: this.hoverAt.y },
48187
+ text: under.text,
48188
+ pos3d: under.pos3d
48189
+ });
48190
+ this.hovering = true;
48191
+ }
48192
+ releaseHover() {
48193
+ const hover = this.hover;
48194
+ if (!this.hovering || !hover || !hover.register) {
48195
+ return;
48196
+ }
48197
+ this.hovering = false;
48198
+ if (hover.register.IsLabelHovered(hover.entityId, hover.menuItemId)) {
48199
+ hover.register.ClearLabelHover();
48200
+ }
48201
+ }
48202
+ /*
48203
+ * The text for the painted cell under a window position and its centre on the sheet, or null.
48204
+ */
48205
+ readingUnder(x, y) {
48206
+ const at = this.cellUnder(x, y);
48207
+ if (!at) {
48208
+ return null;
48209
+ }
48210
+ const cols = this.source.width;
48211
+ const found = this.readAt(at.col, at.row, cols);
48212
+ if (!found || !this.paintedAt(at.col, at.row, cols)) {
48213
+ return null;
48214
+ }
48215
+ const text = this.labelLines(found, this.settings.parts || ["value", "floor", "ceiling"]).join("\n");
48216
+ if (!text) {
48217
+ return null;
48218
+ }
48219
+ const lon = this.lonAt(at.col, cols);
48220
+ const lat = this.latAt(at.row, this.source.height);
48221
+ return { text, pos3d: Cesium.Cartesian3.fromDegrees(lon, lat, this.surfaceHeight(lon, lat)) };
48222
+ }
48223
+ /*
48224
+ * The cell under a window position, found on the sheet rather than on the ground beneath it.
48225
+ */
48226
+ cellUnder(x, y) {
48227
+ const scene = this.viewer.scene;
48228
+ const camera = this.viewer.camera;
48229
+ const screen = new Cesium.Cartesian2(x, y);
48230
+ const ray = camera.getPickRay(screen);
48231
+ if (!ray) {
48232
+ return null;
48233
+ }
48234
+ let hit = (scene.globe && scene.globe.pick(ray, scene)) || camera.pickEllipsoid(screen);
48235
+ if (!hit) {
48236
+ return null;
48237
+ }
48238
+ const radii = Cesium.Ellipsoid.WGS84.radii;
48239
+ for (let i = 0; i < HOVER_REFINE_STEPS && this.heightAt; i++) {
48240
+ const carto = Cesium.Cartographic.fromCartesian(hit);
48241
+ const height = this.heightAt(Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude));
48242
+ if (height == null) {
48243
+ break;
48244
+ }
48245
+ const inflated = new Cesium.Ellipsoid(radii.x + height, radii.y + height, radii.z + height);
48246
+ const interval = Cesium.IntersectionTests.rayEllipsoid(ray, inflated);
48247
+ if (!interval) {
48248
+ break;
48249
+ }
48250
+ hit = Cesium.Ray.getPoint(ray, interval.start);
48251
+ }
48252
+ const carto = Cesium.Cartographic.fromCartesian(hit);
48253
+ return this.texelAt(Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude));
47737
48254
  }
47738
48255
  /*
47739
48256
  * Outlines each covered cell, from a projected lattice of its corners.
@@ -47761,44 +48278,25 @@
47761
48278
  if (wide * high > MAX_GRID_CELLS) {
47762
48279
  return;
47763
48280
  }
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;
48281
+ const lattice = new Lattice(wide + 1, high + 1, (c, r) => {
48282
+ // Corners sit half a texel out from the centres lonAt/latAt return.
48283
+ const lon = this.lonAt(window.col0 + c - 0.5, cols);
48284
+ const lat = this.latAt(window.row0 + r - 0.5, rows);
48285
+ return this.projectInto(lon, lat);
48286
+ });
48287
+ const covered = new Uint8Array(wide * high);
47775
48288
  for (let r = 0; r < high; r++) {
47776
48289
  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;
48290
+ covered[r * wide + c] = this.isCovered(window.col0 + c, window.row0 + r, cols) ? 1 : 0;
47793
48291
  }
47794
48292
  }
47795
- if (!drew) {
48293
+ const path = new Path2D();
48294
+ if (!lattice.Trace(path, covered, wide, high)) {
47796
48295
  return;
47797
48296
  }
47798
48297
  this.ctx.strokeStyle = color;
47799
48298
  this.ctx.lineWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
47800
48299
  this.ctx.stroke(path);
47801
- +" alpha" + this.ctx.globalAlpha;
47802
48300
  }
47803
48301
  /*
47804
48302
  * Restricts drawing to the polygon, so nothing is ruled over ground it does not cover.
@@ -47820,33 +48318,11 @@
47820
48318
  path.closePath();
47821
48319
  this.ctx.clip(path);
47822
48320
  }
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
48321
  /*
47846
48322
  * Outlines each tile's own cells, so cell size still shows where the mesh is refined.
47847
48323
  */
47848
48324
  drawTileGrid(color) {
47849
- var _a, _b, _c;
48325
+ var _a, _b;
47850
48326
  const view = this.viewer.camera.computeViewRectangle(Cesium.Ellipsoid.WGS84);
47851
48327
  if (!view) {
47852
48328
  return;
@@ -47875,7 +48351,7 @@
47875
48351
  coarse.lineTo(tr.x, tr.y);
47876
48352
  coarse.lineTo(br.x, br.y);
47877
48353
  coarse.lineTo(bl.x, bl.y);
47878
- coarse.closePath();
48354
+ coarse.lineTo(tl.x, tl.y);
47879
48355
  drewCoarse = true;
47880
48356
  };
47881
48357
  // Planned for every visible tile BEFORE any is drawn.
@@ -47936,31 +48412,29 @@
47936
48412
  // moves, and so each step up is a subset of the one below it.
47937
48413
  const firstR = Math.floor(r0 / step) * step;
47938
48414
  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
- }
48415
+ const wide = Math.ceil((c1 - firstC) / step);
48416
+ const high = Math.ceil((r1 - firstR) / step);
48417
+ const lonOf = (c) => tile.West
48418
+ + (Math.min(firstC + c * step, tile.ResolutionX) / tile.ResolutionX) * (tile.East - tile.West);
48419
+ const latOf = (r) => tile.North
48420
+ - (Math.min(firstR + r * step, tile.ResolutionY) / tile.ResolutionY) * (tile.North - tile.South);
48421
+ const covered = new Uint8Array(wide * high);
48422
+ let any = false;
48423
+ for (let r = 0; r < high; r++) {
48424
+ for (let c = 0; c < wide; c++) {
48425
+ if (this.coveredAt((lonOf(c) + lonOf(c + 1)) / 2, (latOf(r) + latOf(r + 1)) / 2)) {
48426
+ covered[r * wide + c] = 1;
48427
+ any = true;
48428
+ }
48429
+ }
48430
+ }
48431
+ if (!any) {
48432
+ continue;
48433
+ }
48434
+ // Projected on demand, since a sparse raster leaves most corners belonging to no cell.
48435
+ const lattice = new Lattice(wide + 1, high + 1, (c, r) => this.projectInto(lonOf(c), latOf(r)));
48436
+ if (lattice.Trace(path, covered, wide, high)) {
48437
+ drew = true;
47964
48438
  }
47965
48439
  }
47966
48440
  const gridWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
@@ -47978,7 +48452,7 @@
47978
48452
  return;
47979
48453
  }
47980
48454
  this.ctx.strokeStyle = color;
47981
- this.ctx.lineWidth = (_c = this.settings.gridWidthPixels) !== null && _c !== void 0 ? _c : DEFAULT_GRID_WIDTH_PIXELS;
48455
+ this.ctx.lineWidth = gridWidth;
47982
48456
  this.ctx.stroke(path);
47983
48457
  }
47984
48458
  /*
@@ -47998,7 +48472,7 @@
47998
48472
  };
47999
48473
  }
48000
48474
  /*
48001
- * Whether the composited raster holds data at a lon/lat.
48475
+ * Whether the composited raster holds painted data at a lon/lat.
48002
48476
  */
48003
48477
  coveredAt(lon, lat) {
48004
48478
  if (!this.source) {
@@ -48011,60 +48485,20 @@
48011
48485
  if (col < 0 || row < 0 || col >= cols || row >= rows) {
48012
48486
  return false;
48013
48487
  }
48014
- const at = (row * cols + col) * 4 + 3;
48015
48488
  // Painted, not merely covered: a dry cell holds a reading the ramp deliberately hides, and
48016
48489
  // 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;
48490
+ if (this.painted || this.paintedRamp) {
48491
+ return this.paintedAt(col, row, cols);
48019
48492
  }
48020
- return this.source.data[at] >= 128;
48493
+ return this.source.data[(row * cols + col) * 4 + 3] >= 128;
48021
48494
  }
48022
48495
  isCovered(col, row, cols) {
48023
48496
  return Boolean(this.source && this.source.data[(row * cols + col) * 4 + 3] >= 128);
48024
48497
  }
48025
48498
  /*
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.
48499
+ * What a label says about one reading, a line at a time.
48052
48500
  */
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) {
48501
+ labelLines(reading, parts) {
48068
48502
  var _a;
48069
48503
  const dp = (_a = this.settings.decimals) !== null && _a !== void 0 ? _a : (Math.abs(reading.value) < 10 ? 2 : 1);
48070
48504
  const lines = [];
@@ -48083,32 +48517,7 @@
48083
48517
  if (range.length) {
48084
48518
  lines.push(range.join(" - "));
48085
48519
  }
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++;
48520
+ return lines;
48112
48521
  }
48113
48522
  /*
48114
48523
  * The first cell with a reading at or near a lattice point, searched within its own block.
@@ -48135,11 +48544,24 @@
48135
48544
  return null;
48136
48545
  }
48137
48546
  paintedAt(col, row, cols) {
48138
- if (!this.painted) {
48139
- return true;
48547
+ const at = (row * cols + col) * 4;
48548
+ if (this.painted && this.painted.length === this.source.data.length) {
48549
+ return this.painted[at + 3] > 0;
48550
+ }
48551
+ if (this.paintedRamp) {
48552
+ const data = this.source.data;
48553
+ if (data[at + 3] === 0) {
48554
+ return false;
48555
+ }
48556
+ // The 8 bit grey the ramp is indexed on, which is what a tinted frame would have used.
48557
+ const grey = exports.TextureFrameSeriesAnimator.IsPackedValue(this.metadata)
48558
+ ? Math.round((data[at] * 256 + data[at + 1]) / 65535 * 255)
48559
+ : data[at];
48560
+ const steps = this.paintedRamp.length / 4;
48561
+ const index = Math.min(steps - 1, Math.round(grey / 255 * (steps - 1)));
48562
+ return this.paintedRamp[index * 4 + 3] > 0;
48140
48563
  }
48141
- const at = (row * cols + col) * 4 + 3;
48142
- return this.painted[at] > 0;
48564
+ return true;
48143
48565
  }
48144
48566
  // Decodes one cell, skipping anything the texture marks as having no data.
48145
48567
  readAt(col, row, cols) {
@@ -48155,6 +48577,16 @@
48155
48577
  sourceMap: this.sourceMap
48156
48578
  });
48157
48579
  }
48580
+ /*
48581
+ * The texel holding a lon/lat, or null outside the raster.
48582
+ */
48583
+ texelAt(lon, lat) {
48584
+ const cols = this.source.width;
48585
+ const rows = this.source.height;
48586
+ const col = Math.floor((lon - this.extent.West) / (this.extent.East - this.extent.West) * cols);
48587
+ const row = Math.floor((this.extent.North - lat) / (this.extent.North - this.extent.South) * rows);
48588
+ return col < 0 || row < 0 || col >= cols || row >= rows ? null : { col, row };
48589
+ }
48158
48590
  lonAt(col, cols) {
48159
48591
  return this.extent.West + ((col + 0.5) / cols) * (this.extent.East - this.extent.West);
48160
48592
  }
@@ -48183,15 +48615,102 @@
48183
48615
  row1: Math.min(rows - 1, Math.ceil((this.extent.North - south) / spanLat * rows) + 1)
48184
48616
  };
48185
48617
  }
48618
+ // Metres the sheet sits at over a point, or the ellipsoid when nothing says otherwise.
48619
+ surfaceHeight(lon, lat) {
48620
+ const height = this.heightAt ? this.heightAt(lon, lat) : null;
48621
+ return height == null || !isFinite(height) ? 0 : height;
48622
+ }
48623
+ projectCell(col, row, cols, rows) {
48624
+ return this.project(this.lonAt(col, cols), this.latAt(row, rows));
48625
+ }
48626
+ // Projected at the sheet's own height, since up close the ellipsoid is metres from where it is drawn.
48186
48627
  project(lon, lat) {
48187
- const world = Cesium.Cartesian3.fromDegrees(lon, lat, 0);
48628
+ return this.projectInto(lon, lat) ? { x: this.scratchWindow.x, y: this.scratchWindow.y } : null;
48629
+ }
48630
+ /*
48631
+ * Projects into the shared scratch window position, returning whether it landed anywhere.
48632
+ */
48633
+ projectInto(lon, lat) {
48634
+ const world = Cesium.Cartesian3.fromDegrees(lon, lat, this.surfaceHeight(lon, lat), Cesium.Ellipsoid.WGS84, this.scratchWorld);
48635
+ // One matrix multiply in 3D, where the general path rederives the viewport and frustum per point.
48636
+ if (this.viewProjection) {
48637
+ const clip = Cesium.Matrix4.multiplyByVector(this.viewProjection, Cesium.Cartesian4.fromElements(world.x, world.y, world.z, 1, this.scratchClip), this.scratchClip);
48638
+ if (!(clip.w > 0)) {
48639
+ return null;
48640
+ }
48641
+ const view = this.viewer.canvas;
48642
+ this.scratchWindow.x = (clip.x / clip.w * 0.5 + 0.5) * view.clientWidth;
48643
+ this.scratchWindow.y = (0.5 - clip.y / clip.w * 0.5) * view.clientHeight;
48644
+ return this.scratchWindow;
48645
+ }
48188
48646
  const transforms = Cesium.SceneTransforms;
48189
48647
  // Renamed in newer Cesium, and this library runs on whatever the host app supplies.
48190
48648
  const fn = transforms.worldToWindowCoordinates || transforms.wgs84ToWindowCoordinates;
48191
- return fn ? fn.call(transforms, this.viewer.scene, world) : null;
48649
+ return fn ? fn.call(transforms, this.viewer.scene, world, this.scratchWindow) || null : null;
48192
48650
  }
48193
48651
  }
48194
48652
  TextureValueLabels.Labels = Labels;
48653
+ /*
48654
+ * A grid of window positions projected at most once each, and the cell edges traced through them.
48655
+ */
48656
+ class Lattice {
48657
+ constructor(wide, high, projectAt) {
48658
+ this.wide = wide;
48659
+ this.projectAt = projectAt;
48660
+ this.xs = new Float64Array(wide * high);
48661
+ this.ys = new Float64Array(wide * high);
48662
+ this.state = new Uint8Array(wide * high);
48663
+ }
48664
+ at(c, r) {
48665
+ const i = r * this.wide + c;
48666
+ if (this.state[i] === 0) {
48667
+ const p = this.projectAt(c, r);
48668
+ if (p) {
48669
+ this.xs[i] = p.x;
48670
+ this.ys[i] = p.y;
48671
+ this.state[i] = 1;
48672
+ }
48673
+ else {
48674
+ this.state[i] = 2;
48675
+ }
48676
+ }
48677
+ return this.state[i] === 1 ? i : -1;
48678
+ }
48679
+ edge(path, a, b) {
48680
+ if (a < 0 || b < 0) {
48681
+ return false;
48682
+ }
48683
+ path.moveTo(this.xs[a], this.ys[a]);
48684
+ path.lineTo(this.xs[b], this.ys[b]);
48685
+ return true;
48686
+ }
48687
+ /*
48688
+ * Adds each covered cell's edges to a path, a shared edge once, and says whether any were added.
48689
+ */
48690
+ Trace(path, covered, cellsWide, cellsHigh) {
48691
+ let drew = false;
48692
+ for (let r = 0; r < cellsHigh; r++) {
48693
+ for (let c = 0; c < cellsWide; c++) {
48694
+ if (!covered[r * cellsWide + c]) {
48695
+ continue;
48696
+ }
48697
+ const tl = this.at(c, r);
48698
+ const tr = this.at(c + 1, r);
48699
+ const bl = this.at(c, r + 1);
48700
+ if (!(r > 0 && covered[(r - 1) * cellsWide + c])) {
48701
+ drew = this.edge(path, tl, tr) || drew;
48702
+ }
48703
+ if (!(c > 0 && covered[r * cellsWide + c - 1])) {
48704
+ drew = this.edge(path, tl, bl) || drew;
48705
+ }
48706
+ const br = this.at(c + 1, r + 1);
48707
+ drew = this.edge(path, tr, br) || drew;
48708
+ drew = this.edge(path, bl, br) || drew;
48709
+ }
48710
+ }
48711
+ return drew;
48712
+ }
48713
+ }
48195
48714
  /*
48196
48715
  * The value line. Units go on it alone: repeating them on the range underneath doubles the widest
48197
48716
  * label to say nothing, and the two lines are plainly the same quantity.
@@ -48287,6 +48806,9 @@ in vec2 st;
48287
48806
  in float skirt;
48288
48807
 
48289
48808
  uniform sampler2D u_valueTexture;
48809
+ // The frame being faded away from, and how far the fade has got. The same texture at 1 when settled.
48810
+ uniform sampler2D u_valueTextureFrom;
48811
+ uniform float u_valueFade;
48290
48812
  uniform sampler2D u_groundTexture;
48291
48813
  uniform float u_valuePacked;
48292
48814
  uniform float u_valueMin;
@@ -48407,18 +48929,22 @@ float decodeValue(vec4 texel) {
48407
48929
  }
48408
48930
 
48409
48931
  void main() {
48410
- vec4 texel = texture(u_valueTexture, st);
48932
+ vec4 texelTo = texture(u_valueTexture, st);
48933
+ vec4 texelFrom = texture(u_valueTextureFrom, st);
48411
48934
  v_st = st;
48935
+ // Decoded before mixing, since blending a packed pair byte by byte reads as some other value
48936
+ // halfway through the fade.
48937
+ float raw = mix(decodeValue(texelFrom), decodeValue(texelTo), u_valueFade);
48938
+ float alpha = mix(texelFrom.a, texelTo.a, u_valueFade);
48412
48939
 
48413
48940
  // Inside a region the scene knows is water, a texel with no value still has to paint, or the
48414
48941
  // hole the region exists to close is still a hole.
48415
48942
  // The value it paints is the shallowest the ramp is not transparent at.
48416
48943
  float inMask = u_maskWeight * texture(u_maskTexture, st).a;
48417
- float hasData = step(u_coverageCutoffVertex, texel.a);
48418
- float raw = decodeValue(texel);
48944
+ float hasData = step(u_coverageCutoffVertex, alpha);
48419
48945
  float filled = mix(u_maskFillValue, max(raw, u_maskFillValue), hasData);
48420
48946
  v_value = mix(raw, filled, inMask * u_maskFill);
48421
- v_coverage = max(texel.a, inMask * u_maskFill);
48947
+ v_coverage = max(alpha, inMask * u_maskFill);
48422
48948
 
48423
48949
  // Exaggeration stretches the span above the archive's minimum rather than the height itself. On an
48424
48950
  // elevation the height is measured from the ellipsoid, tens of metres from any of the data, so
@@ -48461,6 +48987,8 @@ void main() {
48461
48987
  `;
48462
48988
  const FRAGMENT_SHADER_GLSL300 = `
48463
48989
  uniform sampler2D u_valueTexture;
48990
+ uniform sampler2D u_valueTextureFrom;
48991
+ uniform float u_valueFade;
48464
48992
  uniform sampler2D u_rampTexture;
48465
48993
  uniform float u_coverageCutoff;
48466
48994
  uniform vec2 u_texelSize;
@@ -48497,12 +49025,15 @@ in vec2 v_waveMetres;
48497
49025
  * Scaled per channel for the same reason the vertex shader's decode is: reassembling the pair reaches
48498
49026
  * 65535, which mediump does not have to represent, and this is the shader that may not get highp.
48499
49027
  */
48500
- float valueAt(vec2 uv) {
48501
- vec4 texel = texture(u_valueTexture, uv);
49028
+ float decodeValue(vec4 texel) {
48502
49029
  float packed16 = texel.r * (255.0 * 256.0 / 65535.0) + texel.g * (255.0 / 65535.0);
48503
49030
  return mix(texel.r, packed16, u_valuePacked);
48504
49031
  }
48505
49032
 
49033
+ float valueAt(vec2 uv) {
49034
+ return mix(decodeValue(texture(u_valueTextureFrom, uv)), decodeValue(texture(u_valueTexture, uv)), u_valueFade);
49035
+ }
49036
+
48506
49037
  //#water
48507
49038
  /*
48508
49039
  * Slope of the ripple field at a point, in metres of rise per metre travelled.
@@ -48949,6 +49480,13 @@ void main() {
48949
49480
  this.rampTexture = null;
48950
49481
  this.texture = null;
48951
49482
  this.textureDirty = true;
49483
+ // Fed from a fading source: the frame being left, and which pixels each texture holds.
49484
+ this.fade = null;
49485
+ this.textureFrom = null;
49486
+ this.uploadedTo = null;
49487
+ this.uploadedFrom = null;
49488
+ // Size of whichever value raster is bound, canvas or pixels.
49489
+ this.sourceDims = null;
48952
49490
  this.shaderProgram = null;
48953
49491
  this.shaderProgramWater = false;
48954
49492
  this.renderState = null;
@@ -49119,6 +49657,65 @@ void main() {
49119
49657
  this.maskGroundTexture = null;
49120
49658
  }
49121
49659
  }
49660
+ /**
49661
+ * Metres above the ellipsoid the sheet sits at over a point, or null unless a fading source is bound.
49662
+ */
49663
+ HeightAt(lon, lat) {
49664
+ const pixels = this.fade ? this.fade.to : null;
49665
+ const dims = this.sourceDims;
49666
+ if (!pixels || !dims) {
49667
+ return null;
49668
+ }
49669
+ const fx = (lon - this.extent.West) / (this.extent.East - this.extent.West);
49670
+ const fy = (this.extent.North - lat) / (this.extent.North - this.extent.South);
49671
+ if (!(fx >= 0 && fx <= 1 && fy >= 0 && fy <= 1)) {
49672
+ return null;
49673
+ }
49674
+ const col = Math.min(dims.width - 1, Math.floor(fx * dims.width));
49675
+ const row = Math.min(dims.height - 1, Math.floor(fy * dims.height));
49676
+ const at = (row * dims.width + col) * 4;
49677
+ const coverage = pixels[at + 3] / 255;
49678
+ const normalised = this.packedValue
49679
+ ? (pixels[at] * 256 + pixels[at + 1]) / 65535
49680
+ : pixels[at] / 255;
49681
+ const metres = this.valueMin + normalised * (this.valueMax - this.valueMin) * this.exaggeration;
49682
+ const following = this.followGround && Boolean(this.groundPixels);
49683
+ const ground = following ? this.groundAt(fx, fy) : 0;
49684
+ return this.baseHeight + ground + metres * (following ? coverage : 1);
49685
+ }
49686
+ /**
49687
+ * Changes whenever HeightAt would answer differently for the same frame.
49688
+ */
49689
+ GetPlacementKey() {
49690
+ return [this.groundPixels ? 1 : 0, this.baseHeight, this.exaggeration, this.followGround ? 1 : 0].join("|");
49691
+ }
49692
+ /**
49693
+ * The ramp the sheet is painted with, as 256 RGBA steps across the value range.
49694
+ */
49695
+ GetRampPixels() {
49696
+ return this.rampPixels;
49697
+ }
49698
+ /*
49699
+ * The packed ground raster read the way sampleGround reads it, bilinear between texel centres.
49700
+ */
49701
+ groundAt(u, v) {
49702
+ const size = GROUND_SAMPLES_PER_SIDE;
49703
+ const pixels = this.groundPixels;
49704
+ const read = (x, y) => {
49705
+ const i = (Math.min(size - 1, Math.max(0, y)) * size + Math.min(size - 1, Math.max(0, x))) * 4;
49706
+ const packed = (pixels[i] << 16) + (pixels[i + 1] << 8) + pixels[i + 2];
49707
+ return GROUND_MIN_METRES + (packed / 16777215) * (GROUND_MAX_METRES - GROUND_MIN_METRES);
49708
+ };
49709
+ const x = u * size - 0.5;
49710
+ const y = v * size - 0.5;
49711
+ const x0 = Math.floor(x);
49712
+ const y0 = Math.floor(y);
49713
+ const fx = x - x0;
49714
+ const fy = y - y0;
49715
+ const top = read(x0, y0) + (read(x0 + 1, y0) - read(x0, y0)) * fx;
49716
+ const bottom = read(x0, y0 + 1) + (read(x0 + 1, y0 + 1) - read(x0, y0 + 1)) * fx;
49717
+ return top + (bottom - top) * fy;
49718
+ }
49122
49719
  /**
49123
49720
  * Feeds the surface from an animated frame series.
49124
49721
  * Height and colour advance with the clock without the caller having to push each frame in.
@@ -49137,23 +49734,39 @@ void main() {
49137
49734
  return;
49138
49735
  }
49139
49736
  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;
49737
+ const fade = this.valueSource && this.valueSource.GetValueFade
49738
+ ? this.valueSource.GetValueFade()
49739
+ : null;
49740
+ if (fade) {
49741
+ this.fade = fade;
49742
+ this.sourceDims = { width: fade.width, height: fade.height };
49743
+ }
49744
+ else {
49745
+ this.fade = null;
49746
+ if (this.valueSource) {
49747
+ const canvas = this.valueSource.GetValueCanvas();
49748
+ if (!canvas) {
49749
+ // No frame decoded yet, so there is nothing to displace against.
49750
+ return;
49751
+ }
49752
+ const version = this.valueSource.GetPresentVersion();
49753
+ if (canvas !== this.source || version !== this.lastPresentVersion) {
49754
+ this.source = canvas;
49755
+ this.lastPresentVersion = version;
49756
+ this.textureDirty = true;
49757
+ }
49151
49758
  }
49759
+ this.sourceDims = this.source ? { width: this.source.width, height: this.source.height } : null;
49152
49760
  }
49153
- if (!this.source || !this.source.width || !this.source.height) {
49761
+ if (!this.sourceDims || !this.sourceDims.width || !this.sourceDims.height) {
49154
49762
  return;
49155
49763
  }
49156
- this.syncTexture(context);
49764
+ if (this.fade) {
49765
+ this.syncFadeTextures(context);
49766
+ }
49767
+ else {
49768
+ this.syncTexture(context);
49769
+ }
49157
49770
  this.syncRampTexture(context);
49158
49771
  this.requestGround();
49159
49772
  this.syncGroundTexture(context);
@@ -49201,6 +49814,10 @@ void main() {
49201
49814
  this.texture.destroy();
49202
49815
  this.texture = null;
49203
49816
  }
49817
+ if (this.textureFrom) {
49818
+ this.textureFrom.destroy();
49819
+ this.textureFrom = null;
49820
+ }
49204
49821
  if (this.rampTexture) {
49205
49822
  this.rampTexture.destroy();
49206
49823
  this.rampTexture = null;
@@ -49468,7 +50085,7 @@ void main() {
49468
50085
  const southWest = Cesium.Cartesian3.fromDegrees(this.extent.West, this.extent.South, 0);
49469
50086
  const southEast = Cesium.Cartesian3.fromDegrees(this.extent.East, this.extent.South, 0);
49470
50087
  const width = Cesium.Cartesian3.distance(southWest, southEast);
49471
- return Math.max(1, width / Math.max(1, this.source ? this.source.width : 1));
50088
+ return Math.max(1, width / Math.max(1, this.sourceDims ? this.sourceDims.width : 1));
49472
50089
  }
49473
50090
  syncRampTexture(context) {
49474
50091
  if (this.rampTexture) {
@@ -49489,6 +50106,67 @@ void main() {
49489
50106
  })
49490
50107
  });
49491
50108
  }
50109
+ valueSampler() {
50110
+ // Nearest for a packed value, because filtering would blend the two bytes of the
50111
+ // packing rather than the values they encode, the same trap sampleGround avoids.
50112
+ const minification = this.packedValue
50113
+ ? Cesium.TextureMinificationFilter.NEAREST
50114
+ : Cesium.TextureMinificationFilter.LINEAR;
50115
+ const magnification = this.packedValue
50116
+ ? Cesium.TextureMagnificationFilter.NEAREST
50117
+ : Cesium.TextureMagnificationFilter.LINEAR;
50118
+ return new Cesium.Sampler({
50119
+ wrapS: Cesium.TextureWrap.CLAMP_TO_EDGE,
50120
+ wrapT: Cesium.TextureWrap.CLAMP_TO_EDGE,
50121
+ minificationFilter: minification,
50122
+ magnificationFilter: magnification
50123
+ });
50124
+ }
50125
+ /*
50126
+ * Uploads value pixels into a texture, reusing it when the size still fits.
50127
+ */
50128
+ uploadPixels(context, texture, pixels, width, height) {
50129
+ const view = new Uint8Array(pixels.buffer, pixels.byteOffset, pixels.byteLength);
50130
+ const source = { width, height, arrayBufferView: view };
50131
+ if (texture && texture.width === width && texture.height === height) {
50132
+ texture.copyFrom({ source });
50133
+ return texture;
50134
+ }
50135
+ if (texture) {
50136
+ texture.destroy();
50137
+ }
50138
+ return new Cesium.Texture({
50139
+ context,
50140
+ width,
50141
+ height,
50142
+ pixelFormat: Cesium.PixelFormat.RGBA,
50143
+ pixelDatatype: Cesium.PixelDatatype.UNSIGNED_BYTE,
50144
+ source,
50145
+ // Row 0 is the archive's north edge, and has to stay at the top, as the canvas path does.
50146
+ flipY: false,
50147
+ sampler: this.valueSampler()
50148
+ });
50149
+ }
50150
+ /*
50151
+ * Keeps a texture each side of the fade, swapping them on a new frame so only that one is uploaded.
50152
+ */
50153
+ syncFadeTextures(context) {
50154
+ const fade = this.fade;
50155
+ if (fade.to !== this.uploadedTo) {
50156
+ if (fade.from && fade.from === this.uploadedTo && this.texture) {
50157
+ const previous = this.textureFrom;
50158
+ this.textureFrom = this.texture;
50159
+ this.uploadedFrom = this.uploadedTo;
50160
+ this.texture = previous;
50161
+ }
50162
+ this.texture = this.uploadPixels(context, this.texture, fade.to, fade.width, fade.height);
50163
+ this.uploadedTo = fade.to;
50164
+ }
50165
+ if (fade.from && fade.from !== this.uploadedFrom) {
50166
+ this.textureFrom = this.uploadPixels(context, this.textureFrom, fade.from, fade.width, fade.height);
50167
+ this.uploadedFrom = fade.from;
50168
+ }
50169
+ }
49492
50170
  syncTexture(context) {
49493
50171
  const sizeChanged = this.texture
49494
50172
  && (this.texture.width !== this.source.width || this.texture.height !== this.source.height);
@@ -49496,27 +50174,15 @@ void main() {
49496
50174
  if (this.texture) {
49497
50175
  this.texture.destroy();
49498
50176
  }
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
50177
  this.texture = new Cesium.Texture({
49508
50178
  context,
49509
50179
  source: this.source,
49510
50180
  // Cesium flips canvas rows on upload by default, which would put the archive's
49511
50181
  // row 0 (its north edge) at the south edge and mirror the whole surface.
49512
50182
  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
- })
50183
+ sampler: this.valueSampler()
49519
50184
  });
50185
+ this.uploadedTo = null;
49520
50186
  this.textureDirty = false;
49521
50187
  return;
49522
50188
  }
@@ -49579,7 +50245,7 @@ void main() {
49579
50245
  * carry, so the ladder is capped one step above the texel count rather than run to its top.
49580
50246
  */
49581
50247
  tierCeiling() {
49582
- const source = this.source;
50248
+ const source = this.sourceDims;
49583
50249
  if (!source || !source.width || !source.height) {
49584
50250
  return TIERS[TIERS.length - 1];
49585
50251
  }
@@ -49657,6 +50323,10 @@ void main() {
49657
50323
  owner: this,
49658
50324
  uniformMap: {
49659
50325
  u_valueTexture: () => self.texture,
50326
+ u_valueTextureFrom: () => (self.fade && self.fade.from && self.textureFrom
50327
+ ? self.textureFrom
50328
+ : self.texture),
50329
+ u_valueFade: () => (self.fade && self.fade.from ? self.fade.mix : 1),
49660
50330
  u_groundTexture: () => self.groundTexture,
49661
50331
  u_valuePacked: () => (self.packedValue ? 1 : 0),
49662
50332
  u_groundWeight: () => (self.followGround && self.groundPixels ? 1 : 0),
@@ -49694,7 +50364,7 @@ void main() {
49694
50364
  : 0,
49695
50365
  u_coverageCutoff: () => COVERAGE_CUTOFF,
49696
50366
  u_rampTexture: () => self.rampTexture,
49697
- u_texelSize: () => new Cesium.Cartesian2(1 / self.source.width, 1 / self.source.height),
50367
+ u_texelSize: () => new Cesium.Cartesian2(1 / self.sourceDims.width, 1 / self.sourceDims.height),
49698
50368
  u_waterOn: () => (self.water ? 1 : 0),
49699
50369
  u_waterWavelength: () => (self.water && self.water.wavelength)
49700
50370
  || DEFAULT_WATER_WAVELENGTH_METRES,
@@ -50065,6 +50735,10 @@ void main() {
50065
50735
  syncTextureValueLabels({
50066
50736
  cEntity,
50067
50737
  viewer: params.viewer,
50738
+ surface: drawsDisplacedSurface ? cEntity[DISPLACED_SURFACE_KEY] : null,
50739
+ hover: params.visualRegister
50740
+ ? { register: params.visualRegister, entityId: String(entity.Bruce.ID), menuItemId: params.menuItemId }
50741
+ : undefined,
50068
50742
  settings: overlaySettings(style, entity, params.tags),
50069
50743
  extent: labelArchive
50070
50744
  ? (drawsDisplacedSurface ? archiveExtent(labelArchive.metadata, posses) : drapeExtent)
@@ -50371,6 +51045,7 @@ void main() {
50371
51045
  // rendered: null
50372
51046
  rendered: (_c = params.rendered) === null || _c === void 0 ? void 0 : _c.get(entity.Bruce.ID),
50373
51047
  menuItemId: params.menuItemId,
51048
+ visualRegister: params.visualRegister,
50374
51049
  onSeriesDiscovered: params.onSeriesDiscovered
50375
51050
  });
50376
51051
  }
@@ -50808,8 +51483,8 @@ void main() {
50808
51483
  */
50809
51484
  const LABEL_GRID_COLOR = "rgba(255,255,255,0.55)";
50810
51485
  const LABEL_GRID_WIDTH_PIXELS = 1;
50811
- const LABEL_MIN_SPACING_PIXELS = 78;
50812
- const LABEL_MAX_LABELS = 240;
51486
+ const LABEL_MIN_SPACING_PIXELS = 140;
51487
+ const LABEL_MAX_LABELS = 60;
50813
51488
  function overlaySettings(style, entity, tags) {
50814
51489
  if (!wantsLabels(style)) {
50815
51490
  return undefined;
@@ -50829,6 +51504,7 @@ void main() {
50829
51504
  */
50830
51505
  function syncTextureValueLabels(params) {
50831
51506
  const { cEntity, viewer, settings, extent, animator, metadata } = params;
51507
+ const surface = params.surface && !params.surface.isDestroyed() ? params.surface : null;
50832
51508
  const existing = cEntity[TEXTURE_VALUE_LABELS_KEY];
50833
51509
  // Offline has no extent to rebuild the overlay from, so an existing one is left running.
50834
51510
  if (params.offline && existing) {
@@ -50847,8 +51523,13 @@ void main() {
50847
51523
  West: tile.West, East: tile.East, South: tile.South, North: tile.North,
50848
51524
  ResolutionX: tile.ResolutionX, ResolutionY: tile.ResolutionY
50849
51525
  })),
50850
- clipRing: params.clipRing
51526
+ clipRing: params.clipRing,
51527
+ heightAt: surface ? (lon, lat) => surface.HeightAt(lon, lat) : undefined,
51528
+ heightKey: surface ? () => surface.GetPlacementKey() : undefined,
51529
+ hover: params.hover
50851
51530
  });
51531
+ // A surface keeps no tinted frame, so whether a cell was painted is read off its ramp.
51532
+ labels.SetPaintedRamp(surface ? surface.GetRampPixels() : null);
50852
51533
  let lastVersion = -1;
50853
51534
  let appliedFloor = null;
50854
51535
  let appliedCeiling = null;
@@ -50860,15 +51541,15 @@ void main() {
50860
51541
  if (animator.IsDisposed()) {
50861
51542
  return;
50862
51543
  }
50863
- const canvas = animator.GetValueCanvas();
50864
- if (!canvas) {
50865
- return;
50866
- }
50867
51544
  const version = animator.GetPresentVersion();
50868
51545
  if (version !== lastVersion) {
51546
+ // Read from the frame cache, since reading the value canvas back copies a whole frame.
51547
+ const raster = animator.GetValueRaster();
51548
+ if (!raster) {
51549
+ return;
51550
+ }
50869
51551
  lastVersion = version;
50870
- const ctx = canvas.getContext("2d");
50871
- labels.SetSource(ctx.getImageData(0, 0, canvas.width, canvas.height));
51552
+ labels.SetSource(raster);
50872
51553
  labels.SetPainted(animator.GetDisplayedPixels());
50873
51554
  }
50874
51555
  // Tracked by reference rather than latched on the first one to arrive: a tiled archive
@@ -52592,7 +53273,7 @@ void main() {
52592
53273
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
52593
53274
  })(exports.StyleUtils || (exports.StyleUtils = {}));
52594
53275
 
52595
- const VERSION = "7.5.8";
53276
+ const VERSION = "7.6.0";
52596
53277
  /**
52597
53278
  * Updates the environment instance used by bruce-cesium to one specified.
52598
53279
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.