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.
@@ -1,6 +1,6 @@
1
1
  import * as Cesium from 'cesium';
2
- import { Cartographic, ColorMaterialProperty, Entity, Color, ConstantProperty, CallbackProperty, Primitive, Cesium3DTileFeature, ShaderSource, Cartesian3, Math as Math$1, Ellipsoid, Transforms, Matrix4, Geometry, GeometryAttribute, ComponentDatatype, PrimitiveType, BoundingSphere, GeometryPipeline, Cartesian2, Cartesian4, sampleTerrain, Texture, PixelFormat, Sampler, TextureWrap, TextureMinificationFilter, TextureMagnificationFilter, VertexArray, BufferUsage, ShaderProgram, RenderState, BlendingState, DrawCommand, Pass, ConstantPositionProperty, HeightReference, DistanceDisplayCondition, JulianDate, Quaternion, HeadingPitchRoll, ColorBlendMode, ShadowMode, ClassificationType, Model, ArcType, CornerType, ImageMaterialProperty, PolygonHierarchy, PolylineGraphics, Rectangle, HorizontalOrigin, VerticalOrigin, SceneTransforms, FeatureDetection, SceneMode, Matrix3, NearFarScalar, KmlDataSource, GeoJsonDataSource, Cesium3DTileStyle, HeadingPitchRange, Cesium3DTileColorBlendMode, Ion, IonResource, Cesium3DTileset, OrthographicFrustum, EasingFunction, Intersect, ModelGraphics, PolygonGraphics, CorridorGraphics, PointGraphics, BillboardGraphics, EllipseGraphics, PolylineDashMaterialProperty, EllipsoidGeodesic, EllipsoidTerrainProvider, sampleTerrainMostDetailed, defined, GeometryInstance, PolygonPipeline, CustomDataSource, IonImageryProvider, createWorldImagery, createWorldImageryAsync, BingMapsImageryProvider, BingMapsStyle, MapboxImageryProvider, MapboxStyleImageryProvider, ArcGisMapServerImageryProvider, OpenStreetMapImageryProvider, UrlTemplateImageryProvider, GridImageryProvider, GeographicTilingScheme, ImageryLayer, TileMapServiceImageryProvider, CesiumTerrainProvider, CesiumInspector, ClockRange, DynamicEnvironmentMapManager, ScreenSpaceEventHandler, ScreenSpaceEventType, Fullscreen, CzmlDataSource } from 'cesium';
3
- import { Cartes, LRUCache, ProjectViewTile, ENVIRONMENT, Calculator, ClientFile, Entity as Entity$1, EntityLod, EntityTag, EntityType, ObjectUtils, Style, UrlUtils, Carto, Geometry as Geometry$1, ClientFileValueMap, Color as Color$1, Bounds, Api, MenuItem, ProjectView, ProjectViewBookmark, Tileset, ZoomControl, BruceEvent, EntityCoords, DataLab, DelayQueue, AccountConcept, RecordChangeFeed, BruceApi, EntityRelation, Camera, ProgramKey, EntitySource, ProjectViewLegacyTile, Account, AccountLimits, Session, EntityAttachment, EntityAttachmentType, EntityAttribute, MathUtils, ProjectViewBookmarkGroup, EntityRelationType, EntityHistoricData } from 'bruce-models';
2
+ import { Cartographic, ColorMaterialProperty, Entity, Color, ConstantProperty, CallbackProperty, Primitive, Cesium3DTileFeature, ConstantPositionProperty, HeightReference, DistanceDisplayCondition, ShaderSource, Cartesian3, Math as Math$1, Ellipsoid, Transforms, Matrix4, Geometry, GeometryAttribute, ComponentDatatype, PrimitiveType, BoundingSphere, GeometryPipeline, Cartesian2, Cartesian4, sampleTerrain, Texture, PixelFormat, Sampler, TextureWrap, TextureMinificationFilter, TextureMagnificationFilter, PixelDatatype, VertexArray, BufferUsage, ShaderProgram, RenderState, BlendingState, DrawCommand, Pass, ColorBlendMode, HeadingPitchRoll, ShadowMode, ClassificationType, Model, JulianDate, Quaternion, ArcType, CornerType, ImageMaterialProperty, PolygonHierarchy, PolylineGraphics, Rectangle, HorizontalOrigin, VerticalOrigin, SceneMode, IntersectionTests, Ray, SceneTransforms, FeatureDetection, Matrix3, NearFarScalar, KmlDataSource, GeoJsonDataSource, Cesium3DTileStyle, HeadingPitchRange, Ion, IonResource, Cesium3DTileColorBlendMode, Cesium3DTileset, OrthographicFrustum, EasingFunction, EllipsoidTerrainProvider, IonImageryProvider, createWorldImagery, createWorldImageryAsync, BingMapsImageryProvider, BingMapsStyle, MapboxImageryProvider, MapboxStyleImageryProvider, ArcGisMapServerImageryProvider, OpenStreetMapImageryProvider, UrlTemplateImageryProvider, GridImageryProvider, GeographicTilingScheme, ImageryLayer, TileMapServiceImageryProvider, CesiumTerrainProvider, ModelGraphics, PolygonGraphics, CorridorGraphics, PointGraphics, BillboardGraphics, EllipseGraphics, PolylineDashMaterialProperty, Intersect, PolygonPipeline, EllipsoidGeodesic, sampleTerrainMostDetailed, defined, CesiumInspector, ClockRange, DynamicEnvironmentMapManager, GeometryInstance, CustomDataSource, ScreenSpaceEventHandler, ScreenSpaceEventType, Fullscreen, CzmlDataSource } from 'cesium';
3
+ import { Cartes, LRUCache, ProjectViewTile, ENVIRONMENT, Calculator, ClientFile, Entity as Entity$1, EntityLod, EntityTag, EntityType, ObjectUtils, Style, UrlUtils, Carto, Geometry as Geometry$1, Bounds, ClientFileValueMap, Color as Color$1, Api, MenuItem, ProjectView, ProjectViewBookmark, Tileset, ZoomControl, BruceEvent, EntityCoords, DataLab, DelayQueue, AccountConcept, BruceApi, EntityRelation, RecordChangeFeed, EntitySource, Camera, ProgramKey, ProjectViewLegacyTile, Account, AccountLimits, Session, ProjectViewBookmarkGroup, EntityAttachment, EntityAttachmentType, EntityAttribute, MathUtils, EntityRelationType, EntityHistoricData } from 'bruce-models';
4
4
 
5
5
  /**
6
6
  * Ensures a number is returned from a given value.
@@ -7311,7 +7311,8 @@ var EntityLabel;
7311
7311
  this.groupId = groupId;
7312
7312
  this.additionalTexts = this.options.text != null ? [] : null;
7313
7313
  this.photo = null;
7314
- this.createLabel();
7314
+ // A label given its position keeps it, or the rebuild goes looking for the entity's own.
7315
+ this.createLabel(this.prePositioned ? this._pos3d : undefined);
7315
7316
  }
7316
7317
  /**
7317
7318
  * Creates cesium entity for the label if one does not exist.
@@ -7903,6 +7904,22 @@ var EntityLabel;
7903
7904
  radius: Math.max(width, height) / 2
7904
7905
  };
7905
7906
  };
7907
+ /*
7908
+ * The on-screen centre and half-size of a model too small for its box, so the box is ringed like a glyph.
7909
+ */
7910
+ const measureModel = (anchor) => {
7911
+ const hull = layer.modelBoxes.Hull(this.id);
7912
+ if (!(hull === null || hull === void 0 ? void 0 : hull.length)) {
7913
+ return null;
7914
+ }
7915
+ const xs = hull.map(point => point.x), ys = hull.map(point => point.y);
7916
+ const left = Math.min(...xs), top = Math.min(...ys);
7917
+ const width = Math.max(...xs) - left, height = Math.max(...ys) - top;
7918
+ if (Math.max(width, height) >= MODEL_BOX_MIN_SIZE) {
7919
+ return null;
7920
+ }
7921
+ return { x: left + width / 2 - anchor.x, y: top + height / 2 - anchor.y, radius: Math.max(width, height) / 2 };
7922
+ };
7906
7923
  // Only CAD tilesets and model Entities are assembly parts with a geometry box; anything else stays a point.
7907
7924
  const boxable = Boolean(normalLeader && ((_r = this.rego) === null || _r === void 0 ? void 0 : _r.entityId) && ((visual instanceof Cesium3DTileFeature && this.rego.tilesetType === Tileset.EType.Cad) ||
7908
7925
  (visual instanceof Entity && Boolean(visual.model))));
@@ -7961,7 +7978,7 @@ var EntityLabel;
7961
7978
  }
7962
7979
  let cursorMotion = this.cursorMotion;
7963
7980
  const project = () => {
7964
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
7981
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
7965
7982
  if (this.disposed || !this.viewer || this.viewer.isDestroyed() || ((_a = this.visible) === null || _a === void 0 ? void 0 : _a.call(this)) === false ||
7966
7983
  (presentation === "icon" && this.options.iconScale === 0)) {
7967
7984
  return null;
@@ -8004,7 +8021,8 @@ var EntityLabel;
8004
8021
  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);
8005
8022
  // Eased between the ring and where the stem meets the box, so gaining or losing a box never jumps.
8006
8023
  const boxTarget = this.boxMorph.anchor;
8007
- const ringPoint = { x: anchor.x + pixelOffset.x, y: anchor.y + pixelOffset.y };
8024
+ const model = ((_g = this.modelBox) === null || _g === void 0 ? void 0 : _g.corners) ? measureModel(anchor) : null;
8025
+ const ringPoint = { x: anchor.x + (model || pixelOffset).x, y: anchor.y + (model || pixelOffset).y };
8008
8026
  const boxAnchor = boxBlend > 0 && boxTarget ? {
8009
8027
  x: ringPoint.x + (boxTarget.x - ringPoint.x) * boxBlend,
8010
8028
  y: ringPoint.y + (boxTarget.y - ringPoint.y) * boxBlend
@@ -8015,13 +8033,13 @@ var EntityLabel;
8015
8033
  return null;
8016
8034
  if (onScreen && !this.imageAsked && this.groupId && !HasIcon(this.options) && !this.options.photo) {
8017
8035
  this.imageAsked = true;
8018
- (_g = this.visualRegister) === null || _g === void 0 ? void 0 : _g.RequestLabelImage((_h = this.rego) === null || _h === void 0 ? void 0 : _h.entityId);
8036
+ (_h = this.visualRegister) === null || _h === void 0 ? void 0 : _h.RequestLabelImage((_j = this.rego) === null || _j === void 0 ? void 0 : _j.entityId);
8019
8037
  }
8020
8038
  const transientPivot = (this.Transient && !this.Persistent ?
8021
- (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);
8039
+ (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);
8022
8040
  const hover = (presentation !== "icon" ?
8023
8041
  (this.Transient && !this.Persistent && this.hoverFade.to === 0 ? this.retainedHoverPosition :
8024
- (_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);
8042
+ (_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);
8025
8043
  const reused = !this.Transient || this.Persistent;
8026
8044
  const now = Date.now();
8027
8045
  const animateCursor = () => {
@@ -8045,13 +8063,13 @@ var EntityLabel;
8045
8063
  const cursorAmount = cursorMotion.amount;
8046
8064
  leader = normalLeader || cursorAmount > 0;
8047
8065
  // An icon keeps its callout centred on the glyph rather than following the cursor across it.
8048
- const icon = boxAnchor ? null : measureIcon();
8066
+ const icon = boxAnchor ? null : measureIcon() || model;
8049
8067
  const iconRing = icon ? Math.max(ICON_RING_MIN, icon.radius + ICON_RING_PADDING) : 0;
8050
8068
  // A boxed model is pinned the same way, with its stem leaving the top of the box outline.
8051
8069
  const pinned = Boolean(icon || boxAnchor);
8052
8070
  const pivot = pinned ? undefined : transientPivot;
8053
- 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;
8054
- 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;
8071
+ 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;
8072
+ 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;
8055
8073
  if (cursorAmount > 0 && !pinned) {
8056
8074
  // A leader-less label starts centred on its anchor; preserve that box as it lifts away.
8057
8075
  if (!normalLeader) {
@@ -8106,7 +8124,7 @@ var EntityLabel;
8106
8124
  const width = ele.offsetWidth / coordinates.scaleX;
8107
8125
  const height = ele.offsetHeight / coordinates.scaleY;
8108
8126
  let clusterBounds;
8109
- if ((_s = this.options.cluster) === null || _s === void 0 ? void 0 : _s.enabled) {
8127
+ if ((_t = this.options.cluster) === null || _t === void 0 ? void 0 : _t.enabled) {
8110
8128
  const textWidth = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetWidth) || 0;
8111
8129
  const textHeight = (sourceText === null || sourceText === void 0 ? void 0 : sourceText.offsetHeight) || 0;
8112
8130
  // A photo is never part of the footprint, so its arrival cannot change which cluster a label joins.
@@ -8140,7 +8158,7 @@ var EntityLabel;
8140
8158
  if (!leader && (bounds.x + bounds.width < 0 || bounds.y + bounds.height < 0 || bounds.x > this.viewer.canvas.clientWidth || bounds.y > this.viewer.canvas.clientHeight)) {
8141
8159
  return null;
8142
8160
  }
8143
- 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 });
8161
+ 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 });
8144
8162
  const opacity = this.prePositioned || hoverOnly || MAX_DISTANCE === Infinity ? 1 : 1 - distance / MAX_DISTANCE;
8145
8163
  return {
8146
8164
  canFlip: leader && presentation !== "icon", hasLeader: leader,
@@ -8148,8 +8166,8 @@ var EntityLabel;
8148
8166
  transient: this.Transient || Boolean(hover) || cursorAmount > 0,
8149
8167
  extendedGeometry: visual instanceof Entity && Boolean(visual.polygon || visual.polyline || visual.corridor),
8150
8168
  viewportWidth: this.viewer.canvas.clientWidth,
8151
- 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,
8152
- 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,
8169
+ 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,
8170
+ 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,
8153
8171
  iconRadius: icon === null || icon === void 0 ? void 0 : icon.radius, modelBox: boxed || undefined, boxBlend,
8154
8172
  // Priority shows the hovered member individually without dragging its cluster along.
8155
8173
  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
@@ -13811,7 +13829,9 @@ var VisualsRegister;
13811
13829
  entityId: params.entityId,
13812
13830
  menuItemId: params.menuItemId,
13813
13831
  position: params.position ? { ...params.position } : undefined,
13814
- createIfMissing: params.createIfMissing !== false
13832
+ createIfMissing: params.createIfMissing !== false,
13833
+ text: params.text,
13834
+ pos3d: params.pos3d ? Cartesian3.clone(params.pos3d) : undefined
13815
13835
  };
13816
13836
  const doc = this.viewer.container.ownerDocument;
13817
13837
  // Only a label following the cursor should go when the cursor leaves the canvas.
@@ -13827,6 +13847,13 @@ var VisualsRegister;
13827
13847
  this.SyncLabelHover();
13828
13848
  this.viewer.scene.requestRender();
13829
13849
  }
13850
+ /*
13851
+ * Which Entity holds the label hover, if any, so a caller can leave another's hover alone.
13852
+ */
13853
+ GetLabelHoverTarget() {
13854
+ const hover = this.labelHover;
13855
+ return hover ? { entityId: hover.entityId, menuItemId: hover.menuItemId } : undefined;
13856
+ }
13830
13857
  /*
13831
13858
  * Whether the label hover is on this Entity, with or without a cursor position.
13832
13859
  */
@@ -13839,7 +13866,7 @@ var VisualsRegister;
13839
13866
  return (hover === null || hover === void 0 ? void 0 : hover.entityId) === entityId && (hover.menuItemId == null || hover.menuItemId === menuItemId) ? hover.position : undefined;
13840
13867
  }
13841
13868
  SyncLabelHover() {
13842
- var _a, _b, _c;
13869
+ var _a, _b, _c, _d;
13843
13870
  const hover = this.labelHover;
13844
13871
  if (!hover || this.disposed) {
13845
13872
  return;
@@ -13871,12 +13898,24 @@ var VisualsRegister;
13871
13898
  this.ReleaseHoverLabel(true);
13872
13899
  return;
13873
13900
  }
13901
+ const hoverOptions = {
13902
+ presentation: "text",
13903
+ text: (_b = hover.text) !== null && _b !== void 0 ? _b : group === null || group === void 0 ? void 0 : group.options.text,
13904
+ selectOnClick: false
13905
+ };
13874
13906
  if (this.hoverLabel && !this.hoverLabel.Disposed) {
13907
+ // Rebuilds only when the text changed, as it does when the pointer crosses a cell.
13908
+ if (hover.pos3d && !Cartesian3.equals(hover.pos3d, this.hoverLabel.pos3d)) {
13909
+ this.hoverLabel.pos3d = hover.pos3d;
13910
+ }
13911
+ if (hover.text != null) {
13912
+ this.hoverLabel.Configure(hoverOptions);
13913
+ }
13875
13914
  this.hoverLabel.SetHoverVisible(true);
13876
13915
  return;
13877
13916
  }
13878
13917
  // Query-only icon groups have coordinates but no registered scene visual to attach to.
13879
- if (!rego && group && [group.item.longitude, group.item.latitude, (_b = group.item.altitude) !== null && _b !== void 0 ? _b : 0].every(Number.isFinite)) {
13918
+ if (!rego && group && [group.item.longitude, group.item.latitude, (_c = group.item.altitude) !== null && _c !== void 0 ? _c : 0].every(Number.isFinite)) {
13880
13919
  rego = {
13881
13920
  entityId: hover.entityId,
13882
13921
  menuItemId: hover.menuItemId,
@@ -13898,15 +13937,12 @@ var VisualsRegister;
13898
13937
  rego,
13899
13938
  viewer: this.viewer,
13900
13939
  visualRegister: this,
13901
- api: (_c = this.ApiGetters) === null || _c === void 0 ? void 0 : _c.GetBruceApi(),
13940
+ api: (_d = this.ApiGetters) === null || _d === void 0 ? void 0 : _d.GetBruceApi(),
13902
13941
  persistent: false,
13903
13942
  transient: true,
13904
13943
  hoverRingState: this.hoverRingState,
13905
- options: {
13906
- presentation: "text",
13907
- text: group === null || group === void 0 ? void 0 : group.options.text,
13908
- selectOnClick: false
13909
- },
13944
+ pos3d: hover.pos3d,
13945
+ options: hoverOptions,
13910
13946
  visible: () => {
13911
13947
  var _a;
13912
13948
  const current = this.GetState(hover);
@@ -46823,6 +46859,10 @@ var TextureFrameSeriesAnimator;
46823
46859
  TextureFrameSeriesAnimator.AppearanceSignature = AppearanceSignature;
46824
46860
  const MAX_COMPOSITE_TEXELS = 2048;
46825
46861
  const VALUE_DILATE_PASSES = 2;
46862
+ // Decoded frame bytes kept at once. A 2048 square composite is 16 MB, so a long series cannot all stay.
46863
+ const MAX_CACHED_FRAME_BYTES = 256 * 1024 * 1024;
46864
+ // Fetched bytes kept for decoding again, a small fraction of what they decode to.
46865
+ const MAX_RAW_FRAME_BYTES = 96 * 1024 * 1024;
46826
46866
  /*
46827
46867
  * Rewrites a packed 16 bit value as the 8 bit grey the colour ramps index on.
46828
46868
  *
@@ -46843,19 +46883,33 @@ var TextureFrameSeriesAnimator;
46843
46883
  /*
46844
46884
  * Bleeds covered values outward into uncovered texels, leaving alpha untouched.
46845
46885
  */
46846
- function dilateValues(value, width, height, passes, packed) {
46886
+ function DilateValues(value, width, height, passes, packed) {
46847
46887
  const texels = width * height;
46848
- const filled = new Uint8Array(texels);
46888
+ // 255 for a texel that had data to begin with, otherwise the 1 based pass that filled it, so a pass
46889
+ // reads only texels filled before it and can work in place.
46890
+ const stamp = new Uint8Array(texels);
46849
46891
  for (let p = 0; p < texels; p++) {
46850
- filled[p] = value[p * 4 + 3] !== 0 ? 1 : 0;
46892
+ stamp[p] = value[p * 4 + 3] !== 0 ? 255 : 0;
46851
46893
  }
46894
+ // Whether a texel or a horizontal neighbour had data, so empty space is skipped in three reads.
46895
+ const across = new Uint8Array(texels);
46852
46896
  for (let pass = 0; pass < passes; pass++) {
46853
- const source = value.slice();
46854
- const wasFilled = filled.slice();
46897
+ const current = pass + 1;
46898
+ const had = (p) => stamp[p] !== 0 && stamp[p] !== current;
46899
+ for (let y = 0; y < height; y++) {
46900
+ const row = y * width;
46901
+ for (let x = 0; x < width; x++) {
46902
+ const p = row + x;
46903
+ across[p] = (had(p) || (x > 0 && had(p - 1)) || (x + 1 < width && had(p + 1))) ? 1 : 0;
46904
+ }
46905
+ }
46855
46906
  for (let y = 0; y < height; y++) {
46856
46907
  for (let x = 0; x < width; x++) {
46857
46908
  const p = y * width + x;
46858
- if (wasFilled[p]) {
46909
+ if (stamp[p] !== 0) {
46910
+ continue;
46911
+ }
46912
+ if (!across[p] && !(y > 0 && across[p - width]) && !(y + 1 < height && across[p + width])) {
46859
46913
  continue;
46860
46914
  }
46861
46915
  let sum = 0;
@@ -46864,13 +46918,13 @@ var TextureFrameSeriesAnimator;
46864
46918
  for (let dx = -1; dx <= 1; dx++) {
46865
46919
  const nx = x + dx;
46866
46920
  const ny = y + dy;
46867
- if (nx < 0 || ny < 0 || nx >= width || ny >= height || !wasFilled[ny * width + nx]) {
46921
+ if (nx < 0 || ny < 0 || nx >= width || ny >= height || !had(ny * width + nx)) {
46868
46922
  continue;
46869
46923
  }
46870
46924
  const at = (ny * width + nx) * 4;
46871
46925
  // Averaged as a value rather than per byte: averaging a packed low byte
46872
46926
  // on its own wraps at every 256th step and speckles the bled edge.
46873
- sum += packed ? source[at] * 256 + source[at + 1] : source[at];
46927
+ sum += packed ? value[at] * 256 + value[at + 1] : value[at];
46874
46928
  hits++;
46875
46929
  }
46876
46930
  }
@@ -46886,18 +46940,25 @@ var TextureFrameSeriesAnimator;
46886
46940
  value[p * 4 + 1] = v;
46887
46941
  value[p * 4 + 2] = v;
46888
46942
  }
46889
- filled[p] = 1;
46943
+ stamp[p] = current;
46890
46944
  }
46891
46945
  }
46892
46946
  }
46893
46947
  }
46894
46948
  }
46949
+ TextureFrameSeriesAnimator.DilateValues = DilateValues;
46950
+ /*
46951
+ * Bytes one cached frame holds, counting a buffer shared between colour and value once.
46952
+ */
46953
+ function frameBytes(pixels, value) {
46954
+ return (pixels ? pixels.byteLength : 0) + (value && value !== pixels ? value.byteLength : 0);
46955
+ }
46895
46956
  const DEFAULT_CROSSFADE_MS = 900;
46896
46957
  const DEFAULT_LOW_COLOR = { red: 255, green: 255, blue: 255, alpha: 0.12 };
46897
46958
  const DEFAULT_HIGH_COLOR = { red: 21, green: 96, blue: 196, alpha: 0.92 };
46898
46959
  class Animator {
46899
46960
  constructor(options) {
46900
- var _a, _b, _c;
46961
+ var _a, _b, _c, _d;
46901
46962
  this.removeOnTick = null;
46902
46963
  this.removeCrossfadeTick = null;
46903
46964
  this.disposed = false;
@@ -46928,6 +46989,15 @@ var TextureFrameSeriesAnimator;
46928
46989
  this.extremesLoad = null;
46929
46990
  this.floorPixels = null;
46930
46991
  this.ceilingPixels = null;
46992
+ // How far through the fade from the previous value frame to the presented one, 1 when settled.
46993
+ this.fadeMix = 1;
46994
+ // Where the current fade started from, above zero when it took over from one still running.
46995
+ this.fadeMixStart = 0;
46996
+ // Version the value canvas was last written at, since in value only mode it is written on demand.
46997
+ this.valueCanvasVersion = -1;
46998
+ this.rawBytes = 0;
46999
+ this.cachedBytes = 0;
47000
+ this.lastFrameBytes = 0;
46931
47001
  if (!options.entity.polygon) {
46932
47002
  throw new Error("TextureFrameSeriesAnimator requires an entity with polygon graphics.");
46933
47003
  }
@@ -46975,6 +47045,9 @@ var TextureFrameSeriesAnimator;
46975
47045
  this.maskBaseline = (_c = options.maskBaseline) !== null && _c !== void 0 ? _c : this.tiles.some((tile) => Boolean(tile.BaselineMask));
46976
47046
  this.valueCanvas = this.produceValueCanvas ? document.createElement("canvas") : null;
46977
47047
  this.driveMaterial = options.driveMaterial !== false;
47048
+ this.valueOnly = this.produceValueCanvas && !this.driveMaterial;
47049
+ this.rawCache = new Array(this.frames.length).fill(null);
47050
+ this.cacheBudget = (_d = options.cacheBytes) !== null && _d !== void 0 ? _d : MAX_CACHED_FRAME_BYTES;
46978
47051
  this.originalMaterial = options.entity.polygon.material;
46979
47052
  this.imageProperty = new CallbackProperty(() => this.pool[this.poolIdx], false);
46980
47053
  if (this.driveMaterial) {
@@ -47040,7 +47113,40 @@ var TextureFrameSeriesAnimator;
47040
47113
  * or null unless the instance was constructed with produceValueCanvas.
47041
47114
  */
47042
47115
  GetValueCanvas() {
47043
- return this.valueDims ? this.valueCanvas : null;
47116
+ if (!this.valueDims) {
47117
+ return null;
47118
+ }
47119
+ if (this.valueOnly && this.valueCanvasVersion !== this.presentVersion && this.displayedValuePixels) {
47120
+ this.writeInto(this.valueCanvas, this.displayedValuePixels, this.valueDims);
47121
+ this.valueCanvasVersion = this.presentVersion;
47122
+ }
47123
+ return this.valueCanvas;
47124
+ }
47125
+ /**
47126
+ * The presented frame's value pixels, or null before one. Shares the cached array, so never write to it.
47127
+ */
47128
+ GetValueRaster() {
47129
+ if (!this.valueDims || !this.displayedValuePixels) {
47130
+ return null;
47131
+ }
47132
+ return { data: this.displayedValuePixels, width: this.valueDims.width, height: this.valueDims.height };
47133
+ }
47134
+ /**
47135
+ * The two value frames being faded between and the mix, or null unless the material is not driven.
47136
+ */
47137
+ GetValueFade() {
47138
+ if (!this.valueOnly || !this.valueDims || !this.displayedValuePixels) {
47139
+ return null;
47140
+ }
47141
+ const previous = this.fadeFromValuePixels;
47142
+ const from = previous && previous.length === this.displayedValuePixels.length ? previous : null;
47143
+ return {
47144
+ from,
47145
+ to: this.displayedValuePixels,
47146
+ width: this.valueDims.width,
47147
+ height: this.valueDims.height,
47148
+ mix: from ? this.fadeMix : 1
47149
+ };
47044
47150
  }
47045
47151
  /**
47046
47152
  * Increments every time a frame is presented, so a consumer holding the value canvas can tell
@@ -47107,7 +47213,11 @@ var TextureFrameSeriesAnimator;
47107
47213
  if (this.currentFrameIndex < 0) {
47108
47214
  return this.frameCache[0] ? -1 : 0;
47109
47215
  }
47110
- for (let d = 0; d < n; d++) {
47216
+ // Full once another frame will not fit, so a prefetch only displaces a frame further from the
47217
+ // playhead than itself and never trades the two ends of the window back and forth.
47218
+ const full = this.cachedBytes + this.lastFrameBytes > this.cacheBudget;
47219
+ const reach = full ? this.farthestCachedDistance() : n;
47220
+ for (let d = 0; d < Math.min(n, reach); d++) {
47111
47221
  const forward = this.currentFrameIndex + d;
47112
47222
  if (forward < n && !this.frameCache[forward]) {
47113
47223
  return forward;
@@ -47119,6 +47229,68 @@ var TextureFrameSeriesAnimator;
47119
47229
  }
47120
47230
  return -1;
47121
47231
  }
47232
+ farthestCachedDistance() {
47233
+ let farthest = 0;
47234
+ for (let i = 0; i < this.frameCache.length; i++) {
47235
+ if (this.frameCache[i]) {
47236
+ farthest = Math.max(farthest, Math.abs(i - this.currentFrameIndex));
47237
+ }
47238
+ }
47239
+ return farthest;
47240
+ }
47241
+ /*
47242
+ * Drops decoded frames furthest from the playhead until the cache fits its budget again.
47243
+ */
47244
+ evictDecoded(keep) {
47245
+ // A tinted fade reads its target from the cache each tick, where a value fade holds the arrays.
47246
+ const fading = !this.valueOnly && this.fadeStartMs !== null ? this.fadeToIndex : -1;
47247
+ while (this.cachedBytes > this.cacheBudget) {
47248
+ let victim = -1;
47249
+ let distance = -1;
47250
+ for (let i = 0; i < this.frameCache.length; i++) {
47251
+ if (!this.frameCache[i] || i === keep || i === this.currentFrameIndex || i === fading) {
47252
+ continue;
47253
+ }
47254
+ const d = Math.abs(i - this.currentFrameIndex);
47255
+ if (d > distance) {
47256
+ distance = d;
47257
+ victim = i;
47258
+ }
47259
+ }
47260
+ if (victim < 0) {
47261
+ return;
47262
+ }
47263
+ this.cachedBytes -= frameBytes(this.frameCache[victim], this.valueCache[victim]);
47264
+ this.frameCache[victim] = null;
47265
+ this.valueCache[victim] = null;
47266
+ }
47267
+ }
47268
+ /*
47269
+ * Keeps a frame's fetched bytes unless they are large, as a tile major archive's are.
47270
+ */
47271
+ keepRaw(idx, buffer, base) {
47272
+ if (buffer.byteLength > MAX_RAW_FRAME_BYTES / 16) {
47273
+ return;
47274
+ }
47275
+ this.rawCache[idx] = { buffer, base };
47276
+ this.rawBytes += buffer.byteLength;
47277
+ while (this.rawBytes > MAX_RAW_FRAME_BYTES) {
47278
+ let victim = -1;
47279
+ let distance = -1;
47280
+ for (let i = 0; i < this.rawCache.length; i++) {
47281
+ const d = Math.abs(i - this.currentFrameIndex);
47282
+ if (this.rawCache[i] && i !== idx && d > distance) {
47283
+ distance = d;
47284
+ victim = i;
47285
+ }
47286
+ }
47287
+ if (victim < 0) {
47288
+ return;
47289
+ }
47290
+ this.rawBytes -= this.rawCache[victim].buffer.byteLength;
47291
+ this.rawCache[victim] = null;
47292
+ }
47293
+ }
47122
47294
  pumpFetchQueue() {
47123
47295
  if (this.disposed || this.inFlightIndex !== -1) {
47124
47296
  return;
@@ -47162,10 +47334,18 @@ var TextureFrameSeriesAnimator;
47162
47334
  lo = Math.min(lo, at.ByteOffset);
47163
47335
  hi = Math.max(hi, at.ByteOffset + at.ByteLength);
47164
47336
  }
47165
- const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
47166
- const buffer = await response.arrayBuffer();
47167
- if (this.disposed) {
47168
- return;
47337
+ let buffer;
47338
+ const raw = this.rawCache[idx];
47339
+ if (raw && raw.base === lo) {
47340
+ buffer = raw.buffer;
47341
+ }
47342
+ else {
47343
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
47344
+ buffer = await response.arrayBuffer();
47345
+ if (this.disposed) {
47346
+ return;
47347
+ }
47348
+ this.keepRaw(idx, buffer, lo);
47169
47349
  }
47170
47350
  // Awaited before decoding rather than applied later, so no frame is ever cached unmasked and
47171
47351
  // then corrected once the mask lands.
@@ -47177,9 +47357,15 @@ var TextureFrameSeriesAnimator;
47177
47357
  if (this.disposed) {
47178
47358
  return;
47179
47359
  }
47360
+ if (this.frameCache[idx]) {
47361
+ this.cachedBytes -= frameBytes(this.frameCache[idx], this.valueCache[idx]);
47362
+ }
47180
47363
  this.frameCache[idx] = decoded.pixels;
47181
47364
  this.frameDims[idx] = { width: decoded.width, height: decoded.height };
47182
47365
  this.valueCache[idx] = decoded.valuePixels || null;
47366
+ this.lastFrameBytes = frameBytes(decoded.pixels, decoded.valuePixels || null);
47367
+ this.cachedBytes += this.lastFrameBytes;
47368
+ this.evictDecoded(idx);
47183
47369
  if (this.shouldPresent(idx, isTarget)) {
47184
47370
  this.beginCrossfadeTo(idx);
47185
47371
  }
@@ -47249,6 +47435,13 @@ var TextureFrameSeriesAnimator;
47249
47435
  */
47250
47436
  tint(imageData) {
47251
47437
  const canvas = { width: imageData.width, height: imageData.height };
47438
+ if (this.valueOnly) {
47439
+ // One array serves as both, since nothing will ever display the colour.
47440
+ const values = imageData.data;
47441
+ this.applyBaselineMask(values, undefined, canvas.width, canvas.height);
47442
+ DilateValues(values, canvas.width, canvas.height, VALUE_DILATE_PASSES, IsPackedValue(this.metadata));
47443
+ return { pixels: values, width: canvas.width, height: canvas.height, valuePixels: values };
47444
+ }
47252
47445
  // Copied before tinting: the ramp is a lerp between two colours, so the value cannot be
47253
47446
  // recovered from the tinted pixels afterwards.
47254
47447
  const valuePixels = this.produceValueCanvas ? new Uint8ClampedArray(imageData.data) : undefined;
@@ -47269,7 +47462,7 @@ var TextureFrameSeriesAnimator;
47269
47462
  }
47270
47463
  this.applyBaselineMask(imageData.data, valuePixels, canvas.width, canvas.height);
47271
47464
  if (valuePixels) {
47272
- dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES, IsPackedValue(this.metadata));
47465
+ DilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES, IsPackedValue(this.metadata));
47273
47466
  }
47274
47467
  return { pixels: imageData.data, width: canvas.width, height: canvas.height, valuePixels };
47275
47468
  }
@@ -47527,6 +47720,10 @@ var TextureFrameSeriesAnimator;
47527
47720
  }
47528
47721
  }
47529
47722
  beginCrossfadeTo(idx) {
47723
+ if (this.valueOnly) {
47724
+ this.beginValueFadeTo(idx);
47725
+ return;
47726
+ }
47530
47727
  const to = this.frameCache[idx];
47531
47728
  this.fadeFromValuePixels = this.displayedValuePixels === null && this.valueCache[idx]
47532
47729
  ? new Uint8ClampedArray(this.valueCache[idx].length)
@@ -47536,12 +47733,53 @@ var TextureFrameSeriesAnimator;
47536
47733
  this.fadeToDims = this.frameDims[idx];
47537
47734
  this.fadeStartMs = Now();
47538
47735
  }
47736
+ /*
47737
+ * Presents a value frame at once and leaves the fade to the consumer, which blends on the GPU.
47738
+ */
47739
+ beginValueFadeTo(idx) {
47740
+ const to = this.valueCache[idx];
47741
+ if (!to || to === this.displayedValuePixels) {
47742
+ return;
47743
+ }
47744
+ // A new target mid fade keeps whichever frame is mostly on screen and carries on from the current
47745
+ // mix, so frames landing in quick succession do not jump the sheet back.
47746
+ const midFade = this.fadeStartMs !== null && Boolean(this.fadeFromValuePixels);
47747
+ if (midFade && this.fadeMix < 0.5) {
47748
+ this.fadeMixStart = this.fadeMix;
47749
+ }
47750
+ else {
47751
+ this.fadeFromValuePixels = this.displayedValuePixels;
47752
+ this.fadeMixStart = 0;
47753
+ }
47754
+ this.displayedValuePixels = to;
47755
+ this.valueDims = this.frameDims[idx];
47756
+ this.fadeToIndex = idx;
47757
+ this.fadeMix = this.fadeFromValuePixels ? this.fadeMixStart : 1;
47758
+ this.fadeStartMs = this.fadeFromValuePixels ? Now() : null;
47759
+ this.presentVersion++;
47760
+ this.requestRender();
47761
+ }
47762
+ requestRender() {
47763
+ if (this.viewer && this.viewer.scene && this.viewer.scene.requestRenderMode) {
47764
+ this.viewer.scene.requestRender();
47765
+ }
47766
+ }
47539
47767
  tickCrossfade() {
47540
47768
  if (this.disposed || this.fadeStartMs === null) {
47541
47769
  return;
47542
47770
  }
47543
47771
  const t = Math.min(1, (Now() - this.fadeStartMs) / this.crossfadeMs);
47544
47772
  const eased = easeInOutQuad(t);
47773
+ if (this.valueOnly) {
47774
+ this.fadeMix = this.fadeMixStart + (1 - this.fadeMixStart) * eased;
47775
+ if (t >= 1) {
47776
+ this.fadeStartMs = null;
47777
+ this.fadeFromValuePixels = null;
47778
+ this.fadeMix = 1;
47779
+ }
47780
+ this.requestRender();
47781
+ return;
47782
+ }
47545
47783
  const from = this.fadeFromPixels;
47546
47784
  const to = this.frameCache[this.fadeToIndex];
47547
47785
  const n = Math.min(from.length, to.length);
@@ -47581,9 +47819,7 @@ var TextureFrameSeriesAnimator;
47581
47819
  this.valueDims = dims;
47582
47820
  }
47583
47821
  this.presentVersion++;
47584
- if (this.viewer && this.viewer.scene && this.viewer.scene.requestRenderMode) {
47585
- this.viewer.scene.requestRender();
47586
- }
47822
+ this.requestRender();
47587
47823
  }
47588
47824
  }
47589
47825
  TextureFrameSeriesAnimator.Animator = Animator;
@@ -47606,13 +47842,15 @@ var TextureValueLabels;
47606
47842
  const MIN_VISIBLE_PIXELS = 60;
47607
47843
  const MAX_STRIDE = 512;
47608
47844
  const STRIDE_HYSTERESIS = 1.4;
47609
- const DEFAULT_MIN_SPACING_PIXELS = 78;
47610
- const DEFAULT_MAX_LABELS = 240;
47611
- const LABEL_RADIUS = 4;
47612
- const LABEL_FONT = "600 11px ui-monospace, SFMono-Regular, Menlo, monospace";
47613
- // Clear space either side of a label box, so neighbours read as separate.
47614
- const LABEL_GAP_PIXELS = 16;
47615
- const LABEL_ANCHOR_COLOR = "rgba(255,255,255,0.85)";
47845
+ // Scene labels carry a leader and are wider than a value, so they want room and not many of them.
47846
+ const DEFAULT_MIN_SPACING_PIXELS = 140;
47847
+ const DEFAULT_MAX_LABELS = 60;
47848
+ // How far past the edge of the view an anchor may sit before its label is dropped.
47849
+ const LABEL_EDGE_MARGIN_PIXELS = 120;
47850
+ // Distance discount for a label already up when the cap forces a choice.
47851
+ const LABEL_KEEP_BIAS = 0.6;
47852
+ // How long a label outlives its cell leaving the wanted set, so one back a frame later does not blink.
47853
+ const LABEL_LINGER_MS = 300;
47616
47854
  // 8 rather than 5: below this a cell reads as texture rather than as a boundary, and it also
47617
47855
  // bounds the worst case, since a full screen of 8 px cells fits inside MAX_GRID_CELLS.
47618
47856
  const DEFAULT_MIN_GRID_CELL_PIXELS = 8;
@@ -47626,7 +47864,8 @@ var TextureValueLabels;
47626
47864
  const TILE_OUTLINE_ALPHA = 0.45;
47627
47865
  // Cells per grid square at the coarsest. Beyond this a tile is outlined instead.
47628
47866
  const MAX_GRID_STRIDE = 256;
47629
- const LINE_HEIGHT = 13;
47867
+ // Times the pointer's ground hit is moved onto the sheet's own height before reading a cell.
47868
+ const HOVER_REFINE_STEPS = 3;
47630
47869
  /**
47631
47870
  * A clip outline from a projected ring, or null when the ring cannot be drawn.
47632
47871
  * @param projected one entry per ring vertex, null where the vertex has no window coordinate
@@ -47720,11 +47959,25 @@ var TextureValueLabels;
47720
47959
  this.sourceMap = null;
47721
47960
  this.source = null;
47722
47961
  this.painted = null;
47962
+ this.paintedRamp = null;
47723
47963
  this.floor = null;
47724
47964
  this.ceiling = null;
47725
47965
  this.disposed = false;
47726
- this.drawn = 0;
47727
47966
  this.stride = 0;
47967
+ this.cellLabels = new Map();
47968
+ this.lingerTimer = null;
47969
+ // What the last draw was made from, so a frame that changes none of it draws nothing.
47970
+ this.drawnView = null;
47971
+ this.drawnInputs = [];
47972
+ this.scratchWorld = new Cartesian3();
47973
+ this.scratchWindow = new Cartesian2();
47974
+ this.scratchClip = new Cartesian4();
47975
+ // The camera's view and projection as one matrix, rebuilt per draw, or null outside 3D.
47976
+ this.viewProjection = null;
47977
+ this.viewProjectionScratch = new Matrix4();
47978
+ this.hovering = false;
47979
+ this.hoverFrame = 0;
47980
+ this.hoverAt = null;
47728
47981
  this.viewer = options.viewer;
47729
47982
  this.extent = options.extent;
47730
47983
  this.settings = options.settings || {};
@@ -47734,6 +47987,9 @@ var TextureValueLabels;
47734
47987
  this.sourceMap = TextureFrameSeriesAnimator.SourceMap(options.metadata);
47735
47988
  this.tiles = options.tiles;
47736
47989
  this.clipRing = options.clipRing;
47990
+ this.heightAt = options.heightAt;
47991
+ this.heightKey = options.heightKey;
47992
+ this.hover = options.hover;
47737
47993
  this.canvas = document.createElement("canvas");
47738
47994
  this.canvas.style.position = "absolute";
47739
47995
  this.canvas.style.inset = "0";
@@ -47743,12 +47999,31 @@ var TextureValueLabels;
47743
47999
  parent.appendChild(this.canvas);
47744
48000
  }
47745
48001
  this.ctx = this.canvas.getContext("2d");
48002
+ this.onPointerMove = (event) => {
48003
+ const bounds = this.viewer.canvas.getBoundingClientRect();
48004
+ this.hoverAt = { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
48005
+ // At most once a frame, since a pointer reports far faster than anything is drawn.
48006
+ if (!this.hoverFrame) {
48007
+ this.hoverFrame = requestAnimationFrame(() => {
48008
+ this.hoverFrame = 0;
48009
+ this.updateHover();
48010
+ });
48011
+ }
48012
+ };
48013
+ this.onPointerLeave = () => {
48014
+ this.hoverAt = null;
48015
+ this.releaseHover();
48016
+ };
48017
+ if (this.hover && this.hover.register) {
48018
+ this.viewer.canvas.addEventListener("pointermove", this.onPointerMove, { passive: true });
48019
+ this.viewer.canvas.addEventListener("pointerleave", this.onPointerLeave);
48020
+ }
47746
48021
  }
47747
48022
  SetExtent(extent) {
47748
48023
  this.extent = extent;
47749
48024
  }
47750
48025
  /**
47751
- * The current frame's decoded values, as the animator's value canvas already provides them.
48026
+ * The current frame's decoded values.
47752
48027
  * @param source value pixels, one texel per cell
47753
48028
  */
47754
48029
  SetSource(source) {
@@ -47756,11 +48031,17 @@ var TextureValueLabels;
47756
48031
  }
47757
48032
  /**
47758
48033
  * The tinted pixels of the same frame, so the grid can skip cells the ramp painted nothing in.
47759
- * @param painted RGBA of the presented frame, or null to fall back to coverage alone
48034
+ * @param painted RGBA of the presented frame, or null to fall back to the ramp or coverage alone
47760
48035
  */
47761
48036
  SetPainted(painted) {
47762
48037
  this.painted = painted;
47763
48038
  }
48039
+ /**
48040
+ * The surface's 256 step RGBA ramp, for telling whether a cell was painted without a tinted frame.
48041
+ */
48042
+ SetPaintedRamp(ramp) {
48043
+ this.paintedRamp = ramp;
48044
+ }
47764
48045
  /**
47765
48046
  * The archive's per-cell extremes, when it publishes them. Without these a label can still
47766
48047
  * print the current reading, just not the range behind it.
@@ -47770,21 +48051,28 @@ var TextureValueLabels;
47770
48051
  this.ceiling = ceiling;
47771
48052
  }
47772
48053
  GetDrawnCount() {
47773
- return this.drawn;
48054
+ return this.cellLabels.size;
47774
48055
  }
47775
48056
  Clear() {
47776
48057
  this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
47777
- this.drawn = 0;
48058
+ this.drawnView = null;
47778
48059
  }
47779
48060
  Dispose() {
47780
48061
  this.disposed = true;
48062
+ this.clearCellLabels();
48063
+ this.releaseHover();
48064
+ if (this.hoverFrame) {
48065
+ cancelAnimationFrame(this.hoverFrame);
48066
+ this.hoverFrame = 0;
48067
+ }
48068
+ this.viewer.canvas.removeEventListener("pointermove", this.onPointerMove);
48069
+ this.viewer.canvas.removeEventListener("pointerleave", this.onPointerLeave);
47781
48070
  if (this.canvas.parentElement) {
47782
48071
  this.canvas.parentElement.removeChild(this.canvas);
47783
48072
  }
47784
48073
  }
47785
48074
  /**
47786
- * Redraws every visible label. Cheap enough to call per rendered frame, because the cell
47787
- * count it considers is bounded by the view rather than by the texture.
48075
+ * Redraws the grid and syncs the value labels, when anything they are drawn from has changed.
47788
48076
  */
47789
48077
  Render() {
47790
48078
  var _a, _b, _c;
@@ -47792,67 +48080,296 @@ var TextureValueLabels;
47792
48080
  return;
47793
48081
  }
47794
48082
  const view = this.viewer.canvas;
48083
+ const inputs = [this.source, this.painted, this.paintedRamp, this.floor, this.ceiling,
48084
+ view.clientWidth, view.clientHeight, this.heightKey ? this.heightKey() : null];
48085
+ const camera = this.viewer.camera.viewMatrix;
48086
+ if (this.drawnView &&
48087
+ Matrix4.equals(this.drawnView, camera) &&
48088
+ inputs.every((input, i) => input === this.drawnInputs[i])) {
48089
+ return;
48090
+ }
47795
48091
  if (this.canvas.width !== view.clientWidth || this.canvas.height !== view.clientHeight) {
47796
48092
  this.canvas.width = view.clientWidth;
47797
48093
  this.canvas.height = view.clientHeight;
47798
48094
  }
47799
48095
  this.Clear();
48096
+ this.drawnView = Matrix4.clone(camera, this.drawnView || undefined);
48097
+ this.drawnInputs = inputs;
48098
+ const projection = this.viewer.camera.frustum.projectionMatrix;
48099
+ this.viewProjection = this.viewer.scene.mode === SceneMode.SCENE3D
48100
+ ? Matrix4.multiply(projection, camera, this.viewProjectionScratch)
48101
+ : null;
47800
48102
  const cols = this.source.width;
47801
48103
  const rows = this.source.height;
47802
48104
  const window = this.visibleTexels(cols, rows);
47803
48105
  if (!window) {
48106
+ this.syncCellLabels([]);
47804
48107
  return;
47805
48108
  }
47806
- const midCol = (window.col0 + window.col1) / 2;
47807
- const midRow = (window.row0 + window.row1) / 2;
47808
- const a = this.project(this.lonAt(midCol, cols), this.latAt(midRow, rows));
47809
- const b = this.project(this.lonAt(midCol + 1, cols), this.latAt(midRow + 1, rows));
48109
+ // Measured where the view is looking, since in an oblique view up close the middle of the visible
48110
+ // texels sits out toward the horizon, where cells are a fraction of the size.
48111
+ const focus = this.groundAtScreenCentre();
48112
+ const at = focus ? this.texelAt(focus.lon, focus.lat) : null;
48113
+ const focused = (Boolean(at) &&
48114
+ at.col >= window.col0 &&
48115
+ at.col <= window.col1 &&
48116
+ at.row >= window.row0 &&
48117
+ at.row <= window.row1);
48118
+ const midCol = focused ? at.col : (window.col0 + window.col1) / 2;
48119
+ const midRow = focused ? at.row : (window.row0 + window.row1) / 2;
48120
+ const a = this.projectCell(midCol, midRow, cols, rows);
48121
+ const b = this.projectCell(midCol + 1, midRow + 1, cols, rows);
47810
48122
  if (!a || !b) {
48123
+ // Left as they are, since one frame where the focus will not project is not the data leaving.
47811
48124
  return;
47812
48125
  }
47813
48126
  const cellPixels = Math.min(Math.abs(b.x - a.x), Math.abs(b.y - a.y));
47814
48127
  // Ahead of the label threshold on purpose: a grid still reads at a cell size where a
47815
48128
  // number no longer fits.
47816
48129
  this.drawGrid(window, cols, rows, cellPixels);
47817
- if (cellPixels < ((_a = this.settings.minCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_CELL_PIXELS)) {
48130
+ // By area rather than the shorter side, which an oblique view foreshortens to a fraction of the other.
48131
+ const across = this.projectCell(midCol + 1, midRow, cols, rows);
48132
+ const down = this.projectCell(midCol, midRow + 1, cols, rows);
48133
+ const cellSize = across && down
48134
+ ? Math.sqrt(Math.abs((across.x - a.x) * (down.y - a.y) - (across.y - a.y) * (down.x - a.x)))
48135
+ : cellPixels;
48136
+ const visibleWidth = cellSize * (window.col1 - window.col0 + 1);
48137
+ const visibleHeight = cellSize * (window.row1 - window.row0 + 1);
48138
+ if (cellSize < ((_a = this.settings.minCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_CELL_PIXELS) ||
48139
+ Math.max(visibleWidth, visibleHeight) < MIN_VISIBLE_PIXELS) {
48140
+ this.syncCellLabels([]);
47818
48141
  return;
47819
48142
  }
47820
- const visibleWidth = cellPixels * (window.col1 - window.col0 + 1);
47821
- const visibleHeight = cellPixels * (window.row1 - window.row0 + 1);
47822
- if (Math.max(visibleWidth, visibleHeight) < MIN_VISIBLE_PIXELS) {
47823
- return;
47824
- }
47825
- const parts = this.settings.parts || ["value", "floor", "ceiling"];
47826
- this.ctx.font = LABEL_FONT;
47827
- this.ctx.textAlign = "center";
47828
48143
  const maxLabels = (_b = this.settings.maxLabels) !== null && _b !== void 0 ? _b : DEFAULT_MAX_LABELS;
47829
48144
  this.stride = ChooseLabelStride({
47830
- cellPixels,
48145
+ cellPixels: cellSize,
47831
48146
  visibleCols: window.col1 - window.col0 + 1,
47832
48147
  visibleRows: window.row1 - window.row0 + 1,
47833
48148
  maxLabels,
47834
- // Measured, not assumed: the spacing that keeps labels apart is whatever the widest
47835
- // one is, so changing what a label says cannot quietly start overlapping them.
47836
- minSpacingPixels: Math.max((_c = this.settings.minSpacingPixels) !== null && _c !== void 0 ? _c : DEFAULT_MIN_SPACING_PIXELS, this.widestLabel(parts)),
48149
+ minSpacingPixels: (_c = this.settings.minSpacingPixels) !== null && _c !== void 0 ? _c : DEFAULT_MIN_SPACING_PIXELS,
47837
48150
  held: this.stride
47838
48151
  });
47839
- const stride = this.stride;
48152
+ const parts = this.settings.parts || ["value", "floor", "ceiling"];
48153
+ this.syncCellLabels(this.labelCells(window, cols, rows, this.stride, maxLabels, parts));
48154
+ }
48155
+ /*
48156
+ * One labelled cell per lattice block, keyed by the block's origin so it survives frame and stride changes.
48157
+ */
48158
+ labelCells(window, cols, rows, stride, max, parts) {
48159
+ const width = this.canvas.width;
48160
+ const height = this.canvas.height;
47840
48161
  const firstRow = Math.floor(window.row0 / stride) * stride;
47841
48162
  const firstCol = Math.floor(window.col0 / stride) * stride;
47842
- for (let row = firstRow; row <= window.row1 && this.drawn < maxLabels; row += stride) {
47843
- for (let col = firstCol; col <= window.col1 && this.drawn < maxLabels; col += stride) {
48163
+ const candidates = [];
48164
+ for (let row = firstRow; row <= window.row1; row += stride) {
48165
+ for (let col = firstCol; col <= window.col1; col += stride) {
47844
48166
  const found = this.readNear(col, row, cols, rows, stride);
47845
48167
  if (!found) {
47846
48168
  continue;
47847
48169
  }
47848
- const point = this.project(this.lonAt(found.col, cols), this.latAt(found.row, rows));
47849
- if (!point || point.x < 0 || point.y < 0
47850
- || point.x > this.canvas.width || point.y > this.canvas.height) {
48170
+ const point = this.projectCell(found.col, found.row, cols, rows);
48171
+ // A margin past the edge, so a label does not drop the moment its anchor does.
48172
+ const margin = LABEL_EDGE_MARGIN_PIXELS;
48173
+ if (!point ||
48174
+ point.x < -margin ||
48175
+ point.y < -margin ||
48176
+ point.x > width + margin ||
48177
+ point.y > height + margin) {
48178
+ continue;
48179
+ }
48180
+ const text = this.labelLines(found.reading, parts).join("\n");
48181
+ if (!text) {
47851
48182
  continue;
47852
48183
  }
47853
- this.drawLabel(point, found.reading, parts, this.cellOutline(found.col, found.row, cols, rows));
48184
+ const key = col + "," + row;
48185
+ const distance = Math.hypot(point.x - width / 2, point.y - height / 2);
48186
+ candidates.push({
48187
+ key,
48188
+ lon: this.lonAt(found.col, cols),
48189
+ lat: this.latAt(found.row, rows),
48190
+ text,
48191
+ // Labels already up outrank new ones, so the capped set does not shuffle as the view pans.
48192
+ rank: this.cellLabels.has(key) ? distance * LABEL_KEEP_BIAS : distance
48193
+ });
47854
48194
  }
47855
48195
  }
48196
+ if (candidates.length > max) {
48197
+ candidates.sort((x, y) => x.rank - y.rank);
48198
+ candidates.length = max;
48199
+ }
48200
+ return candidates;
48201
+ }
48202
+ /*
48203
+ * Keeps one scene label per wanted cell, reusing those that are still wanted.
48204
+ */
48205
+ syncCellLabels(wanted) {
48206
+ var _a;
48207
+ const keep = new Set(wanted.map((w) => w.key));
48208
+ const now = Date.now();
48209
+ let lingering = false;
48210
+ for (const [key, entry] of this.cellLabels) {
48211
+ if (keep.has(key)) {
48212
+ entry.missingSince = undefined;
48213
+ continue;
48214
+ }
48215
+ entry.missingSince = (_a = entry.missingSince) !== null && _a !== void 0 ? _a : now;
48216
+ if (now - entry.missingSince >= LABEL_LINGER_MS) {
48217
+ entry.label.Dispose();
48218
+ this.cellLabels.delete(key);
48219
+ }
48220
+ else {
48221
+ lingering = true;
48222
+ }
48223
+ }
48224
+ // A still camera draws nothing more, so a lingering label is given its own later look.
48225
+ if (lingering && !this.lingerTimer) {
48226
+ this.lingerTimer = setTimeout(() => {
48227
+ this.lingerTimer = null;
48228
+ if (!this.disposed) {
48229
+ this.drawnView = null;
48230
+ this.viewer.scene.requestRender();
48231
+ }
48232
+ }, LABEL_LINGER_MS);
48233
+ }
48234
+ for (const want of wanted) {
48235
+ const pos3d = Cartesian3.fromDegrees(want.lon, want.lat, this.surfaceHeight(want.lon, want.lat));
48236
+ const options = {
48237
+ presentation: "text",
48238
+ text: want.text,
48239
+ leader: true,
48240
+ selectOnClick: false
48241
+ };
48242
+ const existing = this.cellLabels.get(want.key);
48243
+ if (existing) {
48244
+ if (!Cartesian3.equalsEpsilon(existing.label.pos3d, pos3d, 0, 0.01)) {
48245
+ existing.label.pos3d = pos3d;
48246
+ }
48247
+ if (existing.text !== want.text) {
48248
+ existing.label.Configure(options);
48249
+ existing.text = want.text;
48250
+ }
48251
+ continue;
48252
+ }
48253
+ const id = (this.hover ? this.hover.entityId : "texture-value") + "#cell:" + want.key;
48254
+ const label = new EntityLabel.Label({
48255
+ rego: {
48256
+ entityId: id,
48257
+ menuItemId: this.hover ? this.hover.menuItemId : undefined,
48258
+ name: want.text,
48259
+ priority: 0,
48260
+ // No graphic, since a point or billboard visual pulls a label back to itself on rebuild.
48261
+ visual: new Entity({ position: pos3d })
48262
+ },
48263
+ viewer: this.viewer,
48264
+ pos3d,
48265
+ options,
48266
+ persistent: true,
48267
+ visible: () => !this.disposed
48268
+ });
48269
+ this.cellLabels.set(want.key, { label, text: want.text });
48270
+ }
48271
+ }
48272
+ clearCellLabels() {
48273
+ if (this.lingerTimer) {
48274
+ clearTimeout(this.lingerTimer);
48275
+ this.lingerTimer = null;
48276
+ }
48277
+ for (const entry of this.cellLabels.values()) {
48278
+ entry.label.Dispose();
48279
+ }
48280
+ this.cellLabels.clear();
48281
+ }
48282
+ /*
48283
+ * Shows the cell under the pointer through the register's hover label, unless something else holds it.
48284
+ */
48285
+ updateHover() {
48286
+ const hover = this.hover;
48287
+ if (this.disposed || !hover || !hover.register || !this.source || !this.hoverAt) {
48288
+ return;
48289
+ }
48290
+ const register = hover.register;
48291
+ const target = register.GetLabelHoverTarget();
48292
+ const ours = Boolean(target) && register.IsLabelHovered(hover.entityId, hover.menuItemId);
48293
+ if (target && !ours) {
48294
+ this.hovering = false;
48295
+ return;
48296
+ }
48297
+ const under = this.readingUnder(this.hoverAt.x, this.hoverAt.y);
48298
+ if (!under) {
48299
+ this.releaseHover();
48300
+ return;
48301
+ }
48302
+ register.SetLabelHover({
48303
+ entityId: hover.entityId,
48304
+ menuItemId: hover.menuItemId,
48305
+ position: { x: this.hoverAt.x, y: this.hoverAt.y },
48306
+ text: under.text,
48307
+ pos3d: under.pos3d
48308
+ });
48309
+ this.hovering = true;
48310
+ }
48311
+ releaseHover() {
48312
+ const hover = this.hover;
48313
+ if (!this.hovering || !hover || !hover.register) {
48314
+ return;
48315
+ }
48316
+ this.hovering = false;
48317
+ if (hover.register.IsLabelHovered(hover.entityId, hover.menuItemId)) {
48318
+ hover.register.ClearLabelHover();
48319
+ }
48320
+ }
48321
+ /*
48322
+ * The text for the painted cell under a window position and its centre on the sheet, or null.
48323
+ */
48324
+ readingUnder(x, y) {
48325
+ const at = this.cellUnder(x, y);
48326
+ if (!at) {
48327
+ return null;
48328
+ }
48329
+ const cols = this.source.width;
48330
+ const found = this.readAt(at.col, at.row, cols);
48331
+ if (!found || !this.paintedAt(at.col, at.row, cols)) {
48332
+ return null;
48333
+ }
48334
+ const text = this.labelLines(found, this.settings.parts || ["value", "floor", "ceiling"]).join("\n");
48335
+ if (!text) {
48336
+ return null;
48337
+ }
48338
+ const lon = this.lonAt(at.col, cols);
48339
+ const lat = this.latAt(at.row, this.source.height);
48340
+ return { text, pos3d: Cartesian3.fromDegrees(lon, lat, this.surfaceHeight(lon, lat)) };
48341
+ }
48342
+ /*
48343
+ * The cell under a window position, found on the sheet rather than on the ground beneath it.
48344
+ */
48345
+ cellUnder(x, y) {
48346
+ const scene = this.viewer.scene;
48347
+ const camera = this.viewer.camera;
48348
+ const screen = new Cartesian2(x, y);
48349
+ const ray = camera.getPickRay(screen);
48350
+ if (!ray) {
48351
+ return null;
48352
+ }
48353
+ let hit = (scene.globe && scene.globe.pick(ray, scene)) || camera.pickEllipsoid(screen);
48354
+ if (!hit) {
48355
+ return null;
48356
+ }
48357
+ const radii = Ellipsoid.WGS84.radii;
48358
+ for (let i = 0; i < HOVER_REFINE_STEPS && this.heightAt; i++) {
48359
+ const carto = Cartographic.fromCartesian(hit);
48360
+ const height = this.heightAt(Math$1.toDegrees(carto.longitude), Math$1.toDegrees(carto.latitude));
48361
+ if (height == null) {
48362
+ break;
48363
+ }
48364
+ const inflated = new Ellipsoid(radii.x + height, radii.y + height, radii.z + height);
48365
+ const interval = IntersectionTests.rayEllipsoid(ray, inflated);
48366
+ if (!interval) {
48367
+ break;
48368
+ }
48369
+ hit = Ray.getPoint(ray, interval.start);
48370
+ }
48371
+ const carto = Cartographic.fromCartesian(hit);
48372
+ return this.texelAt(Math$1.toDegrees(carto.longitude), Math$1.toDegrees(carto.latitude));
47856
48373
  }
47857
48374
  /*
47858
48375
  * Outlines each covered cell, from a projected lattice of its corners.
@@ -47880,44 +48397,25 @@ var TextureValueLabels;
47880
48397
  if (wide * high > MAX_GRID_CELLS) {
47881
48398
  return;
47882
48399
  }
47883
- const lattice = new Array((wide + 1) * (high + 1));
47884
- for (let r = 0; r <= high; r++) {
47885
- for (let c = 0; c <= wide; c++) {
47886
- // Corners sit half a texel out from the centres lonAt/latAt return.
47887
- const lon = this.lonAt(window.col0 + c - 0.5, cols);
47888
- const lat = this.latAt(window.row0 + r - 0.5, rows);
47889
- lattice[r * (wide + 1) + c] = this.project(lon, lat);
47890
- }
47891
- }
47892
- const path = new Path2D();
47893
- let drew = false;
48400
+ const lattice = new Lattice(wide + 1, high + 1, (c, r) => {
48401
+ // Corners sit half a texel out from the centres lonAt/latAt return.
48402
+ const lon = this.lonAt(window.col0 + c - 0.5, cols);
48403
+ const lat = this.latAt(window.row0 + r - 0.5, rows);
48404
+ return this.projectInto(lon, lat);
48405
+ });
48406
+ const covered = new Uint8Array(wide * high);
47894
48407
  for (let r = 0; r < high; r++) {
47895
48408
  for (let c = 0; c < wide; c++) {
47896
- if (!this.isCovered(window.col0 + c, window.row0 + r, cols)) {
47897
- continue;
47898
- }
47899
- const tl = lattice[r * (wide + 1) + c];
47900
- const tr = lattice[r * (wide + 1) + c + 1];
47901
- const bl = lattice[(r + 1) * (wide + 1) + c];
47902
- const br = lattice[(r + 1) * (wide + 1) + c + 1];
47903
- if (!tl || !tr || !bl || !br) {
47904
- continue;
47905
- }
47906
- path.moveTo(tl.x, tl.y);
47907
- path.lineTo(tr.x, tr.y);
47908
- path.lineTo(br.x, br.y);
47909
- path.lineTo(bl.x, bl.y);
47910
- path.closePath();
47911
- drew = true;
48409
+ covered[r * wide + c] = this.isCovered(window.col0 + c, window.row0 + r, cols) ? 1 : 0;
47912
48410
  }
47913
48411
  }
47914
- if (!drew) {
48412
+ const path = new Path2D();
48413
+ if (!lattice.Trace(path, covered, wide, high)) {
47915
48414
  return;
47916
48415
  }
47917
48416
  this.ctx.strokeStyle = color;
47918
48417
  this.ctx.lineWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
47919
48418
  this.ctx.stroke(path);
47920
- +" alpha" + this.ctx.globalAlpha;
47921
48419
  }
47922
48420
  /*
47923
48421
  * Restricts drawing to the polygon, so nothing is ruled over ground it does not cover.
@@ -47939,33 +48437,11 @@ var TextureValueLabels;
47939
48437
  path.closePath();
47940
48438
  this.ctx.clip(path);
47941
48439
  }
47942
- /*
47943
- * The cell of whichever tile covers a point, as screen corners.
47944
- */
47945
- tileCellOutline(lon, lat) {
47946
- for (const tile of this.tiles) {
47947
- if (lon < tile.West || lon > tile.East || lat < tile.South || lat > tile.North) {
47948
- continue;
47949
- }
47950
- const c = (Math.min(tile.ResolutionX - 1, Math.max(0, Math.floor((lon - tile.West) / (tile.East - tile.West) * tile.ResolutionX))));
47951
- const r = (Math.min(tile.ResolutionY - 1, Math.max(0, Math.floor((tile.North - lat) / (tile.North - tile.South) * tile.ResolutionY))));
47952
- const w0 = tile.West + (c / tile.ResolutionX) * (tile.East - tile.West);
47953
- const w1 = tile.West + ((c + 1) / tile.ResolutionX) * (tile.East - tile.West);
47954
- const n0 = tile.North - (r / tile.ResolutionY) * (tile.North - tile.South);
47955
- const n1 = tile.North - ((r + 1) / tile.ResolutionY) * (tile.North - tile.South);
47956
- const corners = [
47957
- this.project(w0, n0), this.project(w1, n0),
47958
- this.project(w1, n1), this.project(w0, n1)
47959
- ];
47960
- return corners.every((q) => q) ? corners : null;
47961
- }
47962
- return null;
47963
- }
47964
48440
  /*
47965
48441
  * Outlines each tile's own cells, so cell size still shows where the mesh is refined.
47966
48442
  */
47967
48443
  drawTileGrid(color) {
47968
- var _a, _b, _c;
48444
+ var _a, _b;
47969
48445
  const view = this.viewer.camera.computeViewRectangle(Ellipsoid.WGS84);
47970
48446
  if (!view) {
47971
48447
  return;
@@ -47994,7 +48470,7 @@ var TextureValueLabels;
47994
48470
  coarse.lineTo(tr.x, tr.y);
47995
48471
  coarse.lineTo(br.x, br.y);
47996
48472
  coarse.lineTo(bl.x, bl.y);
47997
- coarse.closePath();
48473
+ coarse.lineTo(tl.x, tl.y);
47998
48474
  drewCoarse = true;
47999
48475
  };
48000
48476
  // Planned for every visible tile BEFORE any is drawn.
@@ -48055,31 +48531,29 @@ var TextureValueLabels;
48055
48531
  // moves, and so each step up is a subset of the one below it.
48056
48532
  const firstR = Math.floor(r0 / step) * step;
48057
48533
  const firstC = Math.floor(c0 / step) * step;
48058
- for (let r = firstR; r < r1; r += step) {
48059
- const n0 = tile.North - (r / tile.ResolutionY) * (tile.North - tile.South);
48060
- const n1 = tile.North
48061
- - (Math.min(r + step, tile.ResolutionY) / tile.ResolutionY) * (tile.North - tile.South);
48062
- for (let c = firstC; c < c1; c += step) {
48063
- const w0 = tile.West + (c / tile.ResolutionX) * (tile.East - tile.West);
48064
- const w1 = tile.West
48065
- + (Math.min(c + step, tile.ResolutionX) / tile.ResolutionX) * (tile.East - tile.West);
48066
- if (!this.coveredAt((w0 + w1) / 2, (n0 + n1) / 2)) {
48067
- continue;
48068
- }
48069
- const tl = this.project(w0, n0);
48070
- const tr = this.project(w1, n0);
48071
- const br = this.project(w1, n1);
48072
- const bl = this.project(w0, n1);
48073
- if (!tl || !tr || !br || !bl) {
48074
- continue;
48075
- }
48076
- path.moveTo(tl.x, tl.y);
48077
- path.lineTo(tr.x, tr.y);
48078
- path.lineTo(br.x, br.y);
48079
- path.lineTo(bl.x, bl.y);
48080
- path.closePath();
48081
- drew = true;
48082
- }
48534
+ const wide = Math.ceil((c1 - firstC) / step);
48535
+ const high = Math.ceil((r1 - firstR) / step);
48536
+ const lonOf = (c) => tile.West
48537
+ + (Math.min(firstC + c * step, tile.ResolutionX) / tile.ResolutionX) * (tile.East - tile.West);
48538
+ const latOf = (r) => tile.North
48539
+ - (Math.min(firstR + r * step, tile.ResolutionY) / tile.ResolutionY) * (tile.North - tile.South);
48540
+ const covered = new Uint8Array(wide * high);
48541
+ let any = false;
48542
+ for (let r = 0; r < high; r++) {
48543
+ for (let c = 0; c < wide; c++) {
48544
+ if (this.coveredAt((lonOf(c) + lonOf(c + 1)) / 2, (latOf(r) + latOf(r + 1)) / 2)) {
48545
+ covered[r * wide + c] = 1;
48546
+ any = true;
48547
+ }
48548
+ }
48549
+ }
48550
+ if (!any) {
48551
+ continue;
48552
+ }
48553
+ // Projected on demand, since a sparse raster leaves most corners belonging to no cell.
48554
+ const lattice = new Lattice(wide + 1, high + 1, (c, r) => this.projectInto(lonOf(c), latOf(r)));
48555
+ if (lattice.Trace(path, covered, wide, high)) {
48556
+ drew = true;
48083
48557
  }
48084
48558
  }
48085
48559
  const gridWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
@@ -48097,7 +48571,7 @@ var TextureValueLabels;
48097
48571
  return;
48098
48572
  }
48099
48573
  this.ctx.strokeStyle = color;
48100
- this.ctx.lineWidth = (_c = this.settings.gridWidthPixels) !== null && _c !== void 0 ? _c : DEFAULT_GRID_WIDTH_PIXELS;
48574
+ this.ctx.lineWidth = gridWidth;
48101
48575
  this.ctx.stroke(path);
48102
48576
  }
48103
48577
  /*
@@ -48117,7 +48591,7 @@ var TextureValueLabels;
48117
48591
  };
48118
48592
  }
48119
48593
  /*
48120
- * Whether the composited raster holds data at a lon/lat.
48594
+ * Whether the composited raster holds painted data at a lon/lat.
48121
48595
  */
48122
48596
  coveredAt(lon, lat) {
48123
48597
  if (!this.source) {
@@ -48130,60 +48604,20 @@ var TextureValueLabels;
48130
48604
  if (col < 0 || row < 0 || col >= cols || row >= rows) {
48131
48605
  return false;
48132
48606
  }
48133
- const at = (row * cols + col) * 4 + 3;
48134
48607
  // Painted, not merely covered: a dry cell holds a reading the ramp deliberately hides, and
48135
48608
  // ruling a grid over it draws detail where the picture shows none.
48136
- if (this.painted && this.painted.length === cols * rows * 4) {
48137
- return this.painted[at] > 0;
48609
+ if (this.painted || this.paintedRamp) {
48610
+ return this.paintedAt(col, row, cols);
48138
48611
  }
48139
- return this.source.data[at] >= 128;
48612
+ return this.source.data[(row * cols + col) * 4 + 3] >= 128;
48140
48613
  }
48141
48614
  isCovered(col, row, cols) {
48142
48615
  return Boolean(this.source && this.source.data[(row * cols + col) * 4 + 3] >= 128);
48143
48616
  }
48144
48617
  /*
48145
- * Width of the widest label this overlay could draw, in pixels.
48146
- */
48147
- widestLabel(parts) {
48148
- const sample = [];
48149
- if (parts.indexOf("value") >= 0) {
48150
- sample.push(valueLine("-000.00", LabelUnits(this.metadata)));
48151
- }
48152
- const range = [];
48153
- const showsRange = !TextureFrameSeriesAnimator.IsTransformed(this.metadata);
48154
- if (showsRange && parts.indexOf("floor") >= 0) {
48155
- range.push("min: -000.00");
48156
- }
48157
- if (showsRange && parts.indexOf("ceiling") >= 0) {
48158
- range.push("max: -000.00");
48159
- }
48160
- if (range.length) {
48161
- sample.push(range.join(" - "));
48162
- }
48163
- if (!sample.length) {
48164
- return 0;
48165
- }
48166
- const widest = Math.max(...sample.map((line) => this.ctx.measureText(line).width));
48167
- return widest + LABEL_GAP_PIXELS;
48168
- }
48169
- /*
48170
- * The four screen corners of one cell, or null if any of them will not project.
48618
+ * What a label says about one reading, a line at a time.
48171
48619
  */
48172
- cellOutline(col, row, cols, rows) {
48173
- // On a pyramid the composited texel is not a cell. Outlining it would draw a second,
48174
- // uniform lattice over the tile grid, misaligned with it everywhere.
48175
- if (this.tiles && this.tiles.length) {
48176
- return this.tileCellOutline(this.lonAt(col, cols), this.latAt(row, rows));
48177
- }
48178
- const corners = [
48179
- this.project(this.lonAt(col - 0.5, cols), this.latAt(row - 0.5, rows)),
48180
- this.project(this.lonAt(col + 0.5, cols), this.latAt(row - 0.5, rows)),
48181
- this.project(this.lonAt(col + 0.5, cols), this.latAt(row + 0.5, rows)),
48182
- this.project(this.lonAt(col - 0.5, cols), this.latAt(row + 0.5, rows))
48183
- ];
48184
- return corners.every((c) => c) ? corners : null;
48185
- }
48186
- drawLabel(point, reading, parts, outline) {
48620
+ labelLines(reading, parts) {
48187
48621
  var _a;
48188
48622
  const dp = (_a = this.settings.decimals) !== null && _a !== void 0 ? _a : (Math.abs(reading.value) < 10 ? 2 : 1);
48189
48623
  const lines = [];
@@ -48202,32 +48636,7 @@ var TextureValueLabels;
48202
48636
  if (range.length) {
48203
48637
  lines.push(range.join(" - "));
48204
48638
  }
48205
- if (!lines.length) {
48206
- return;
48207
- }
48208
- if (outline) {
48209
- this.ctx.strokeStyle = LABEL_ANCHOR_COLOR;
48210
- this.ctx.lineWidth = 1.5;
48211
- this.ctx.beginPath();
48212
- this.ctx.moveTo(outline[0].x, outline[0].y);
48213
- for (let i = 1; i < outline.length; i++) {
48214
- this.ctx.lineTo(outline[i].x, outline[i].y);
48215
- }
48216
- this.ctx.closePath();
48217
- this.ctx.stroke();
48218
- }
48219
- const width = Math.max(...lines.map((l) => this.ctx.measureText(l).width)) + 10;
48220
- const height = lines.length * LINE_HEIGHT + 6;
48221
- const top = point.y - height / 2;
48222
- this.ctx.fillStyle = "rgba(16,16,14,0.72)";
48223
- this.ctx.beginPath();
48224
- this.ctx.roundRect(point.x - width / 2, top, width, height, LABEL_RADIUS);
48225
- this.ctx.fill();
48226
- lines.forEach((line, i) => {
48227
- this.ctx.fillStyle = i === 0 ? "#ffffff" : "#a8a89f";
48228
- this.ctx.fillText(line, point.x, top + LINE_HEIGHT * (i + 1) - 1);
48229
- });
48230
- this.drawn++;
48639
+ return lines;
48231
48640
  }
48232
48641
  /*
48233
48642
  * The first cell with a reading at or near a lattice point, searched within its own block.
@@ -48254,11 +48663,24 @@ var TextureValueLabels;
48254
48663
  return null;
48255
48664
  }
48256
48665
  paintedAt(col, row, cols) {
48257
- if (!this.painted) {
48258
- return true;
48666
+ const at = (row * cols + col) * 4;
48667
+ if (this.painted && this.painted.length === this.source.data.length) {
48668
+ return this.painted[at + 3] > 0;
48669
+ }
48670
+ if (this.paintedRamp) {
48671
+ const data = this.source.data;
48672
+ if (data[at + 3] === 0) {
48673
+ return false;
48674
+ }
48675
+ // The 8 bit grey the ramp is indexed on, which is what a tinted frame would have used.
48676
+ const grey = TextureFrameSeriesAnimator.IsPackedValue(this.metadata)
48677
+ ? Math.round((data[at] * 256 + data[at + 1]) / 65535 * 255)
48678
+ : data[at];
48679
+ const steps = this.paintedRamp.length / 4;
48680
+ const index = Math.min(steps - 1, Math.round(grey / 255 * (steps - 1)));
48681
+ return this.paintedRamp[index * 4 + 3] > 0;
48259
48682
  }
48260
- const at = (row * cols + col) * 4 + 3;
48261
- return this.painted[at] > 0;
48683
+ return true;
48262
48684
  }
48263
48685
  // Decodes one cell, skipping anything the texture marks as having no data.
48264
48686
  readAt(col, row, cols) {
@@ -48274,6 +48696,16 @@ var TextureValueLabels;
48274
48696
  sourceMap: this.sourceMap
48275
48697
  });
48276
48698
  }
48699
+ /*
48700
+ * The texel holding a lon/lat, or null outside the raster.
48701
+ */
48702
+ texelAt(lon, lat) {
48703
+ const cols = this.source.width;
48704
+ const rows = this.source.height;
48705
+ const col = Math.floor((lon - this.extent.West) / (this.extent.East - this.extent.West) * cols);
48706
+ const row = Math.floor((this.extent.North - lat) / (this.extent.North - this.extent.South) * rows);
48707
+ return col < 0 || row < 0 || col >= cols || row >= rows ? null : { col, row };
48708
+ }
48277
48709
  lonAt(col, cols) {
48278
48710
  return this.extent.West + ((col + 0.5) / cols) * (this.extent.East - this.extent.West);
48279
48711
  }
@@ -48302,15 +48734,102 @@ var TextureValueLabels;
48302
48734
  row1: Math.min(rows - 1, Math.ceil((this.extent.North - south) / spanLat * rows) + 1)
48303
48735
  };
48304
48736
  }
48737
+ // Metres the sheet sits at over a point, or the ellipsoid when nothing says otherwise.
48738
+ surfaceHeight(lon, lat) {
48739
+ const height = this.heightAt ? this.heightAt(lon, lat) : null;
48740
+ return height == null || !isFinite(height) ? 0 : height;
48741
+ }
48742
+ projectCell(col, row, cols, rows) {
48743
+ return this.project(this.lonAt(col, cols), this.latAt(row, rows));
48744
+ }
48745
+ // Projected at the sheet's own height, since up close the ellipsoid is metres from where it is drawn.
48305
48746
  project(lon, lat) {
48306
- const world = Cartesian3.fromDegrees(lon, lat, 0);
48747
+ return this.projectInto(lon, lat) ? { x: this.scratchWindow.x, y: this.scratchWindow.y } : null;
48748
+ }
48749
+ /*
48750
+ * Projects into the shared scratch window position, returning whether it landed anywhere.
48751
+ */
48752
+ projectInto(lon, lat) {
48753
+ const world = Cartesian3.fromDegrees(lon, lat, this.surfaceHeight(lon, lat), Ellipsoid.WGS84, this.scratchWorld);
48754
+ // One matrix multiply in 3D, where the general path rederives the viewport and frustum per point.
48755
+ if (this.viewProjection) {
48756
+ const clip = Matrix4.multiplyByVector(this.viewProjection, Cartesian4.fromElements(world.x, world.y, world.z, 1, this.scratchClip), this.scratchClip);
48757
+ if (!(clip.w > 0)) {
48758
+ return null;
48759
+ }
48760
+ const view = this.viewer.canvas;
48761
+ this.scratchWindow.x = (clip.x / clip.w * 0.5 + 0.5) * view.clientWidth;
48762
+ this.scratchWindow.y = (0.5 - clip.y / clip.w * 0.5) * view.clientHeight;
48763
+ return this.scratchWindow;
48764
+ }
48307
48765
  const transforms = SceneTransforms;
48308
48766
  // Renamed in newer Cesium, and this library runs on whatever the host app supplies.
48309
48767
  const fn = transforms.worldToWindowCoordinates || transforms.wgs84ToWindowCoordinates;
48310
- return fn ? fn.call(transforms, this.viewer.scene, world) : null;
48768
+ return fn ? fn.call(transforms, this.viewer.scene, world, this.scratchWindow) || null : null;
48311
48769
  }
48312
48770
  }
48313
48771
  TextureValueLabels.Labels = Labels;
48772
+ /*
48773
+ * A grid of window positions projected at most once each, and the cell edges traced through them.
48774
+ */
48775
+ class Lattice {
48776
+ constructor(wide, high, projectAt) {
48777
+ this.wide = wide;
48778
+ this.projectAt = projectAt;
48779
+ this.xs = new Float64Array(wide * high);
48780
+ this.ys = new Float64Array(wide * high);
48781
+ this.state = new Uint8Array(wide * high);
48782
+ }
48783
+ at(c, r) {
48784
+ const i = r * this.wide + c;
48785
+ if (this.state[i] === 0) {
48786
+ const p = this.projectAt(c, r);
48787
+ if (p) {
48788
+ this.xs[i] = p.x;
48789
+ this.ys[i] = p.y;
48790
+ this.state[i] = 1;
48791
+ }
48792
+ else {
48793
+ this.state[i] = 2;
48794
+ }
48795
+ }
48796
+ return this.state[i] === 1 ? i : -1;
48797
+ }
48798
+ edge(path, a, b) {
48799
+ if (a < 0 || b < 0) {
48800
+ return false;
48801
+ }
48802
+ path.moveTo(this.xs[a], this.ys[a]);
48803
+ path.lineTo(this.xs[b], this.ys[b]);
48804
+ return true;
48805
+ }
48806
+ /*
48807
+ * Adds each covered cell's edges to a path, a shared edge once, and says whether any were added.
48808
+ */
48809
+ Trace(path, covered, cellsWide, cellsHigh) {
48810
+ let drew = false;
48811
+ for (let r = 0; r < cellsHigh; r++) {
48812
+ for (let c = 0; c < cellsWide; c++) {
48813
+ if (!covered[r * cellsWide + c]) {
48814
+ continue;
48815
+ }
48816
+ const tl = this.at(c, r);
48817
+ const tr = this.at(c + 1, r);
48818
+ const bl = this.at(c, r + 1);
48819
+ if (!(r > 0 && covered[(r - 1) * cellsWide + c])) {
48820
+ drew = this.edge(path, tl, tr) || drew;
48821
+ }
48822
+ if (!(c > 0 && covered[r * cellsWide + c - 1])) {
48823
+ drew = this.edge(path, tl, bl) || drew;
48824
+ }
48825
+ const br = this.at(c + 1, r + 1);
48826
+ drew = this.edge(path, tr, br) || drew;
48827
+ drew = this.edge(path, bl, br) || drew;
48828
+ }
48829
+ }
48830
+ return drew;
48831
+ }
48832
+ }
48314
48833
  /*
48315
48834
  * The value line. Units go on it alone: repeating them on the range underneath doubles the widest
48316
48835
  * label to say nothing, and the two lines are plainly the same quantity.
@@ -48411,6 +48930,9 @@ in vec2 st;
48411
48930
  in float skirt;
48412
48931
 
48413
48932
  uniform sampler2D u_valueTexture;
48933
+ // The frame being faded away from, and how far the fade has got. The same texture at 1 when settled.
48934
+ uniform sampler2D u_valueTextureFrom;
48935
+ uniform float u_valueFade;
48414
48936
  uniform sampler2D u_groundTexture;
48415
48937
  uniform float u_valuePacked;
48416
48938
  uniform float u_valueMin;
@@ -48531,18 +49053,22 @@ float decodeValue(vec4 texel) {
48531
49053
  }
48532
49054
 
48533
49055
  void main() {
48534
- vec4 texel = texture(u_valueTexture, st);
49056
+ vec4 texelTo = texture(u_valueTexture, st);
49057
+ vec4 texelFrom = texture(u_valueTextureFrom, st);
48535
49058
  v_st = st;
49059
+ // Decoded before mixing, since blending a packed pair byte by byte reads as some other value
49060
+ // halfway through the fade.
49061
+ float raw = mix(decodeValue(texelFrom), decodeValue(texelTo), u_valueFade);
49062
+ float alpha = mix(texelFrom.a, texelTo.a, u_valueFade);
48536
49063
 
48537
49064
  // Inside a region the scene knows is water, a texel with no value still has to paint, or the
48538
49065
  // hole the region exists to close is still a hole.
48539
49066
  // The value it paints is the shallowest the ramp is not transparent at.
48540
49067
  float inMask = u_maskWeight * texture(u_maskTexture, st).a;
48541
- float hasData = step(u_coverageCutoffVertex, texel.a);
48542
- float raw = decodeValue(texel);
49068
+ float hasData = step(u_coverageCutoffVertex, alpha);
48543
49069
  float filled = mix(u_maskFillValue, max(raw, u_maskFillValue), hasData);
48544
49070
  v_value = mix(raw, filled, inMask * u_maskFill);
48545
- v_coverage = max(texel.a, inMask * u_maskFill);
49071
+ v_coverage = max(alpha, inMask * u_maskFill);
48546
49072
 
48547
49073
  // Exaggeration stretches the span above the archive's minimum rather than the height itself. On an
48548
49074
  // elevation the height is measured from the ellipsoid, tens of metres from any of the data, so
@@ -48585,6 +49111,8 @@ void main() {
48585
49111
  `;
48586
49112
  const FRAGMENT_SHADER_GLSL300 = `
48587
49113
  uniform sampler2D u_valueTexture;
49114
+ uniform sampler2D u_valueTextureFrom;
49115
+ uniform float u_valueFade;
48588
49116
  uniform sampler2D u_rampTexture;
48589
49117
  uniform float u_coverageCutoff;
48590
49118
  uniform vec2 u_texelSize;
@@ -48621,12 +49149,15 @@ in vec2 v_waveMetres;
48621
49149
  * Scaled per channel for the same reason the vertex shader's decode is: reassembling the pair reaches
48622
49150
  * 65535, which mediump does not have to represent, and this is the shader that may not get highp.
48623
49151
  */
48624
- float valueAt(vec2 uv) {
48625
- vec4 texel = texture(u_valueTexture, uv);
49152
+ float decodeValue(vec4 texel) {
48626
49153
  float packed16 = texel.r * (255.0 * 256.0 / 65535.0) + texel.g * (255.0 / 65535.0);
48627
49154
  return mix(texel.r, packed16, u_valuePacked);
48628
49155
  }
48629
49156
 
49157
+ float valueAt(vec2 uv) {
49158
+ return mix(decodeValue(texture(u_valueTextureFrom, uv)), decodeValue(texture(u_valueTexture, uv)), u_valueFade);
49159
+ }
49160
+
48630
49161
  //#water
48631
49162
  /*
48632
49163
  * Slope of the ripple field at a point, in metres of rise per metre travelled.
@@ -49073,6 +49604,13 @@ void main() {
49073
49604
  this.rampTexture = null;
49074
49605
  this.texture = null;
49075
49606
  this.textureDirty = true;
49607
+ // Fed from a fading source: the frame being left, and which pixels each texture holds.
49608
+ this.fade = null;
49609
+ this.textureFrom = null;
49610
+ this.uploadedTo = null;
49611
+ this.uploadedFrom = null;
49612
+ // Size of whichever value raster is bound, canvas or pixels.
49613
+ this.sourceDims = null;
49076
49614
  this.shaderProgram = null;
49077
49615
  this.shaderProgramWater = false;
49078
49616
  this.renderState = null;
@@ -49243,6 +49781,65 @@ void main() {
49243
49781
  this.maskGroundTexture = null;
49244
49782
  }
49245
49783
  }
49784
+ /**
49785
+ * Metres above the ellipsoid the sheet sits at over a point, or null unless a fading source is bound.
49786
+ */
49787
+ HeightAt(lon, lat) {
49788
+ const pixels = this.fade ? this.fade.to : null;
49789
+ const dims = this.sourceDims;
49790
+ if (!pixels || !dims) {
49791
+ return null;
49792
+ }
49793
+ const fx = (lon - this.extent.West) / (this.extent.East - this.extent.West);
49794
+ const fy = (this.extent.North - lat) / (this.extent.North - this.extent.South);
49795
+ if (!(fx >= 0 && fx <= 1 && fy >= 0 && fy <= 1)) {
49796
+ return null;
49797
+ }
49798
+ const col = Math.min(dims.width - 1, Math.floor(fx * dims.width));
49799
+ const row = Math.min(dims.height - 1, Math.floor(fy * dims.height));
49800
+ const at = (row * dims.width + col) * 4;
49801
+ const coverage = pixels[at + 3] / 255;
49802
+ const normalised = this.packedValue
49803
+ ? (pixels[at] * 256 + pixels[at + 1]) / 65535
49804
+ : pixels[at] / 255;
49805
+ const metres = this.valueMin + normalised * (this.valueMax - this.valueMin) * this.exaggeration;
49806
+ const following = this.followGround && Boolean(this.groundPixels);
49807
+ const ground = following ? this.groundAt(fx, fy) : 0;
49808
+ return this.baseHeight + ground + metres * (following ? coverage : 1);
49809
+ }
49810
+ /**
49811
+ * Changes whenever HeightAt would answer differently for the same frame.
49812
+ */
49813
+ GetPlacementKey() {
49814
+ return [this.groundPixels ? 1 : 0, this.baseHeight, this.exaggeration, this.followGround ? 1 : 0].join("|");
49815
+ }
49816
+ /**
49817
+ * The ramp the sheet is painted with, as 256 RGBA steps across the value range.
49818
+ */
49819
+ GetRampPixels() {
49820
+ return this.rampPixels;
49821
+ }
49822
+ /*
49823
+ * The packed ground raster read the way sampleGround reads it, bilinear between texel centres.
49824
+ */
49825
+ groundAt(u, v) {
49826
+ const size = GROUND_SAMPLES_PER_SIDE;
49827
+ const pixels = this.groundPixels;
49828
+ const read = (x, y) => {
49829
+ const i = (Math.min(size - 1, Math.max(0, y)) * size + Math.min(size - 1, Math.max(0, x))) * 4;
49830
+ const packed = (pixels[i] << 16) + (pixels[i + 1] << 8) + pixels[i + 2];
49831
+ return GROUND_MIN_METRES + (packed / 16777215) * (GROUND_MAX_METRES - GROUND_MIN_METRES);
49832
+ };
49833
+ const x = u * size - 0.5;
49834
+ const y = v * size - 0.5;
49835
+ const x0 = Math.floor(x);
49836
+ const y0 = Math.floor(y);
49837
+ const fx = x - x0;
49838
+ const fy = y - y0;
49839
+ const top = read(x0, y0) + (read(x0 + 1, y0) - read(x0, y0)) * fx;
49840
+ const bottom = read(x0, y0 + 1) + (read(x0 + 1, y0 + 1) - read(x0, y0 + 1)) * fx;
49841
+ return top + (bottom - top) * fy;
49842
+ }
49246
49843
  /**
49247
49844
  * Feeds the surface from an animated frame series.
49248
49845
  * Height and colour advance with the clock without the caller having to push each frame in.
@@ -49261,23 +49858,39 @@ void main() {
49261
49858
  return;
49262
49859
  }
49263
49860
  const context = frameState.context;
49264
- if (this.valueSource) {
49265
- const canvas = this.valueSource.GetValueCanvas();
49266
- if (!canvas) {
49267
- // No frame decoded yet, so there is nothing to displace against.
49268
- return;
49269
- }
49270
- const version = this.valueSource.GetPresentVersion();
49271
- if (canvas !== this.source || version !== this.lastPresentVersion) {
49272
- this.source = canvas;
49273
- this.lastPresentVersion = version;
49274
- this.textureDirty = true;
49861
+ const fade = this.valueSource && this.valueSource.GetValueFade
49862
+ ? this.valueSource.GetValueFade()
49863
+ : null;
49864
+ if (fade) {
49865
+ this.fade = fade;
49866
+ this.sourceDims = { width: fade.width, height: fade.height };
49867
+ }
49868
+ else {
49869
+ this.fade = null;
49870
+ if (this.valueSource) {
49871
+ const canvas = this.valueSource.GetValueCanvas();
49872
+ if (!canvas) {
49873
+ // No frame decoded yet, so there is nothing to displace against.
49874
+ return;
49875
+ }
49876
+ const version = this.valueSource.GetPresentVersion();
49877
+ if (canvas !== this.source || version !== this.lastPresentVersion) {
49878
+ this.source = canvas;
49879
+ this.lastPresentVersion = version;
49880
+ this.textureDirty = true;
49881
+ }
49275
49882
  }
49883
+ this.sourceDims = this.source ? { width: this.source.width, height: this.source.height } : null;
49276
49884
  }
49277
- if (!this.source || !this.source.width || !this.source.height) {
49885
+ if (!this.sourceDims || !this.sourceDims.width || !this.sourceDims.height) {
49278
49886
  return;
49279
49887
  }
49280
- this.syncTexture(context);
49888
+ if (this.fade) {
49889
+ this.syncFadeTextures(context);
49890
+ }
49891
+ else {
49892
+ this.syncTexture(context);
49893
+ }
49281
49894
  this.syncRampTexture(context);
49282
49895
  this.requestGround();
49283
49896
  this.syncGroundTexture(context);
@@ -49325,6 +49938,10 @@ void main() {
49325
49938
  this.texture.destroy();
49326
49939
  this.texture = null;
49327
49940
  }
49941
+ if (this.textureFrom) {
49942
+ this.textureFrom.destroy();
49943
+ this.textureFrom = null;
49944
+ }
49328
49945
  if (this.rampTexture) {
49329
49946
  this.rampTexture.destroy();
49330
49947
  this.rampTexture = null;
@@ -49592,7 +50209,7 @@ void main() {
49592
50209
  const southWest = Cartesian3.fromDegrees(this.extent.West, this.extent.South, 0);
49593
50210
  const southEast = Cartesian3.fromDegrees(this.extent.East, this.extent.South, 0);
49594
50211
  const width = Cartesian3.distance(southWest, southEast);
49595
- return Math.max(1, width / Math.max(1, this.source ? this.source.width : 1));
50212
+ return Math.max(1, width / Math.max(1, this.sourceDims ? this.sourceDims.width : 1));
49596
50213
  }
49597
50214
  syncRampTexture(context) {
49598
50215
  if (this.rampTexture) {
@@ -49613,6 +50230,67 @@ void main() {
49613
50230
  })
49614
50231
  });
49615
50232
  }
50233
+ valueSampler() {
50234
+ // Nearest for a packed value, because filtering would blend the two bytes of the
50235
+ // packing rather than the values they encode, the same trap sampleGround avoids.
50236
+ const minification = this.packedValue
50237
+ ? TextureMinificationFilter.NEAREST
50238
+ : TextureMinificationFilter.LINEAR;
50239
+ const magnification = this.packedValue
50240
+ ? TextureMagnificationFilter.NEAREST
50241
+ : TextureMagnificationFilter.LINEAR;
50242
+ return new Sampler({
50243
+ wrapS: TextureWrap.CLAMP_TO_EDGE,
50244
+ wrapT: TextureWrap.CLAMP_TO_EDGE,
50245
+ minificationFilter: minification,
50246
+ magnificationFilter: magnification
50247
+ });
50248
+ }
50249
+ /*
50250
+ * Uploads value pixels into a texture, reusing it when the size still fits.
50251
+ */
50252
+ uploadPixels(context, texture, pixels, width, height) {
50253
+ const view = new Uint8Array(pixels.buffer, pixels.byteOffset, pixels.byteLength);
50254
+ const source = { width, height, arrayBufferView: view };
50255
+ if (texture && texture.width === width && texture.height === height) {
50256
+ texture.copyFrom({ source });
50257
+ return texture;
50258
+ }
50259
+ if (texture) {
50260
+ texture.destroy();
50261
+ }
50262
+ return new Texture({
50263
+ context,
50264
+ width,
50265
+ height,
50266
+ pixelFormat: PixelFormat.RGBA,
50267
+ pixelDatatype: PixelDatatype.UNSIGNED_BYTE,
50268
+ source,
50269
+ // Row 0 is the archive's north edge, and has to stay at the top, as the canvas path does.
50270
+ flipY: false,
50271
+ sampler: this.valueSampler()
50272
+ });
50273
+ }
50274
+ /*
50275
+ * Keeps a texture each side of the fade, swapping them on a new frame so only that one is uploaded.
50276
+ */
50277
+ syncFadeTextures(context) {
50278
+ const fade = this.fade;
50279
+ if (fade.to !== this.uploadedTo) {
50280
+ if (fade.from && fade.from === this.uploadedTo && this.texture) {
50281
+ const previous = this.textureFrom;
50282
+ this.textureFrom = this.texture;
50283
+ this.uploadedFrom = this.uploadedTo;
50284
+ this.texture = previous;
50285
+ }
50286
+ this.texture = this.uploadPixels(context, this.texture, fade.to, fade.width, fade.height);
50287
+ this.uploadedTo = fade.to;
50288
+ }
50289
+ if (fade.from && fade.from !== this.uploadedFrom) {
50290
+ this.textureFrom = this.uploadPixels(context, this.textureFrom, fade.from, fade.width, fade.height);
50291
+ this.uploadedFrom = fade.from;
50292
+ }
50293
+ }
49616
50294
  syncTexture(context) {
49617
50295
  const sizeChanged = this.texture
49618
50296
  && (this.texture.width !== this.source.width || this.texture.height !== this.source.height);
@@ -49620,27 +50298,15 @@ void main() {
49620
50298
  if (this.texture) {
49621
50299
  this.texture.destroy();
49622
50300
  }
49623
- // Nearest for a packed value, because filtering would blend the two bytes of the
49624
- // packing rather than the values they encode, the same trap sampleGround avoids.
49625
- const minification = this.packedValue
49626
- ? TextureMinificationFilter.NEAREST
49627
- : TextureMinificationFilter.LINEAR;
49628
- const magnification = this.packedValue
49629
- ? TextureMagnificationFilter.NEAREST
49630
- : TextureMagnificationFilter.LINEAR;
49631
50301
  this.texture = new Texture({
49632
50302
  context,
49633
50303
  source: this.source,
49634
50304
  // Cesium flips canvas rows on upload by default, which would put the archive's
49635
50305
  // row 0 (its north edge) at the south edge and mirror the whole surface.
49636
50306
  flipY: false,
49637
- sampler: new Sampler({
49638
- wrapS: TextureWrap.CLAMP_TO_EDGE,
49639
- wrapT: TextureWrap.CLAMP_TO_EDGE,
49640
- minificationFilter: minification,
49641
- magnificationFilter: magnification
49642
- })
50307
+ sampler: this.valueSampler()
49643
50308
  });
50309
+ this.uploadedTo = null;
49644
50310
  this.textureDirty = false;
49645
50311
  return;
49646
50312
  }
@@ -49703,7 +50369,7 @@ void main() {
49703
50369
  * carry, so the ladder is capped one step above the texel count rather than run to its top.
49704
50370
  */
49705
50371
  tierCeiling() {
49706
- const source = this.source;
50372
+ const source = this.sourceDims;
49707
50373
  if (!source || !source.width || !source.height) {
49708
50374
  return TIERS[TIERS.length - 1];
49709
50375
  }
@@ -49781,6 +50447,10 @@ void main() {
49781
50447
  owner: this,
49782
50448
  uniformMap: {
49783
50449
  u_valueTexture: () => self.texture,
50450
+ u_valueTextureFrom: () => (self.fade && self.fade.from && self.textureFrom
50451
+ ? self.textureFrom
50452
+ : self.texture),
50453
+ u_valueFade: () => (self.fade && self.fade.from ? self.fade.mix : 1),
49784
50454
  u_groundTexture: () => self.groundTexture,
49785
50455
  u_valuePacked: () => (self.packedValue ? 1 : 0),
49786
50456
  u_groundWeight: () => (self.followGround && self.groundPixels ? 1 : 0),
@@ -49818,7 +50488,7 @@ void main() {
49818
50488
  : 0,
49819
50489
  u_coverageCutoff: () => COVERAGE_CUTOFF,
49820
50490
  u_rampTexture: () => self.rampTexture,
49821
- u_texelSize: () => new Cartesian2(1 / self.source.width, 1 / self.source.height),
50491
+ u_texelSize: () => new Cartesian2(1 / self.sourceDims.width, 1 / self.sourceDims.height),
49822
50492
  u_waterOn: () => (self.water ? 1 : 0),
49823
50493
  u_waterWavelength: () => (self.water && self.water.wavelength)
49824
50494
  || DEFAULT_WATER_WAVELENGTH_METRES,
@@ -50190,6 +50860,10 @@ var EntityRenderEnginePolygon;
50190
50860
  syncTextureValueLabels({
50191
50861
  cEntity,
50192
50862
  viewer: params.viewer,
50863
+ surface: drawsDisplacedSurface ? cEntity[DISPLACED_SURFACE_KEY] : null,
50864
+ hover: params.visualRegister
50865
+ ? { register: params.visualRegister, entityId: String(entity.Bruce.ID), menuItemId: params.menuItemId }
50866
+ : undefined,
50193
50867
  settings: overlaySettings(style, entity, params.tags),
50194
50868
  extent: labelArchive
50195
50869
  ? (drawsDisplacedSurface ? archiveExtent(labelArchive.metadata, posses) : drapeExtent)
@@ -50496,6 +51170,7 @@ var EntityRenderEnginePolygon;
50496
51170
  // rendered: null
50497
51171
  rendered: (_c = params.rendered) === null || _c === void 0 ? void 0 : _c.get(entity.Bruce.ID),
50498
51172
  menuItemId: params.menuItemId,
51173
+ visualRegister: params.visualRegister,
50499
51174
  onSeriesDiscovered: params.onSeriesDiscovered
50500
51175
  });
50501
51176
  }
@@ -50933,8 +51608,8 @@ function wantsLabels(style) {
50933
51608
  */
50934
51609
  const LABEL_GRID_COLOR = "rgba(255,255,255,0.55)";
50935
51610
  const LABEL_GRID_WIDTH_PIXELS = 1;
50936
- const LABEL_MIN_SPACING_PIXELS = 78;
50937
- const LABEL_MAX_LABELS = 240;
51611
+ const LABEL_MIN_SPACING_PIXELS = 140;
51612
+ const LABEL_MAX_LABELS = 60;
50938
51613
  function overlaySettings(style, entity, tags) {
50939
51614
  if (!wantsLabels(style)) {
50940
51615
  return undefined;
@@ -50954,6 +51629,7 @@ function overlaySettings(style, entity, tags) {
50954
51629
  */
50955
51630
  function syncTextureValueLabels(params) {
50956
51631
  const { cEntity, viewer, settings, extent, animator, metadata } = params;
51632
+ const surface = params.surface && !params.surface.isDestroyed() ? params.surface : null;
50957
51633
  const existing = cEntity[TEXTURE_VALUE_LABELS_KEY];
50958
51634
  // Offline has no extent to rebuild the overlay from, so an existing one is left running.
50959
51635
  if (params.offline && existing) {
@@ -50972,8 +51648,13 @@ function syncTextureValueLabels(params) {
50972
51648
  West: tile.West, East: tile.East, South: tile.South, North: tile.North,
50973
51649
  ResolutionX: tile.ResolutionX, ResolutionY: tile.ResolutionY
50974
51650
  })),
50975
- clipRing: params.clipRing
51651
+ clipRing: params.clipRing,
51652
+ heightAt: surface ? (lon, lat) => surface.HeightAt(lon, lat) : undefined,
51653
+ heightKey: surface ? () => surface.GetPlacementKey() : undefined,
51654
+ hover: params.hover
50976
51655
  });
51656
+ // A surface keeps no tinted frame, so whether a cell was painted is read off its ramp.
51657
+ labels.SetPaintedRamp(surface ? surface.GetRampPixels() : null);
50977
51658
  let lastVersion = -1;
50978
51659
  let appliedFloor = null;
50979
51660
  let appliedCeiling = null;
@@ -50985,15 +51666,15 @@ function syncTextureValueLabels(params) {
50985
51666
  if (animator.IsDisposed()) {
50986
51667
  return;
50987
51668
  }
50988
- const canvas = animator.GetValueCanvas();
50989
- if (!canvas) {
50990
- return;
50991
- }
50992
51669
  const version = animator.GetPresentVersion();
50993
51670
  if (version !== lastVersion) {
51671
+ // Read from the frame cache, since reading the value canvas back copies a whole frame.
51672
+ const raster = animator.GetValueRaster();
51673
+ if (!raster) {
51674
+ return;
51675
+ }
50994
51676
  lastVersion = version;
50995
- const ctx = canvas.getContext("2d");
50996
- labels.SetSource(ctx.getImageData(0, 0, canvas.width, canvas.height));
51677
+ labels.SetSource(raster);
50997
51678
  labels.SetPainted(animator.GetDisplayedPixels());
50998
51679
  }
50999
51680
  // Tracked by reference rather than latched on the first one to arrive: a tiled archive
@@ -52732,7 +53413,7 @@ var StyleUtils;
52732
53413
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
52733
53414
  })(StyleUtils || (StyleUtils = {}));
52734
53415
 
52735
- const VERSION = "7.5.8";
53416
+ const VERSION = "7.6.0";
52736
53417
  /**
52737
53418
  * Updates the environment instance used by bruce-cesium to one specified.
52738
53419
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.