bruce-cesium 7.2.1 → 7.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9033,7 +9033,8 @@
9033
9033
  entityId: rego.entityId,
9034
9034
  menuItemId: rego.menuItemId,
9035
9035
  doUpdate: false,
9036
- requestRender: false
9036
+ requestRender: false,
9037
+ keepVisual: rego.visual
9037
9038
  });
9038
9039
  }
9039
9040
  const entityId = rego.entityId;
@@ -9326,7 +9327,9 @@
9326
9327
  exports.EntityLabel.Detatch({
9327
9328
  rego
9328
9329
  });
9329
- if (doRemove != false) {
9330
+ // Never tear down the graphic the caller is registering: the renderers hand back
9331
+ // the same object when they reuse it, so removing it here destroys what replaces it.
9332
+ if (doRemove != false && rego.visual !== params.keepVisual) {
9330
9333
  removeEntity(this.viewer, rego.visual);
9331
9334
  }
9332
9335
  rego.visual = null;
@@ -9380,7 +9383,9 @@
9380
9383
  exports.EntityLabel.Detatch({
9381
9384
  rego
9382
9385
  });
9383
- if (doRemove != false) {
9386
+ // Never tear down the graphic the caller is registering: the renderers hand back
9387
+ // the same object when they reuse it, so removing it here destroys what replaces it.
9388
+ if (doRemove != false && rego.visual !== params.keepVisual) {
9384
9389
  removeEntity(this.viewer, rego.visual);
9385
9390
  }
9386
9391
  rego.visual = null;
@@ -40171,6 +40176,107 @@
40171
40176
  data[i + 3] = Math.round(srcAlpha * maskAlpha * 255);
40172
40177
  }
40173
40178
  }
40179
+ /**
40180
+ * The colour a ramp gives a normalised value.
40181
+ * @param sorted stops in ascending position, normalised into the texture's value range
40182
+ */
40183
+ function SampleRamp(sorted, t) {
40184
+ let upper = 0;
40185
+ while (upper < sorted.length && sorted[upper].position < t) {
40186
+ upper++;
40187
+ }
40188
+ if (upper === 0) {
40189
+ return sorted[0].color;
40190
+ }
40191
+ if (upper >= sorted.length) {
40192
+ return sorted[sorted.length - 1].color;
40193
+ }
40194
+ const a = sorted[upper - 1];
40195
+ const b = sorted[upper];
40196
+ const span = b.position - a.position;
40197
+ // Equal positions are a hard step rather than a division by zero, which is how a band
40198
+ // gets a crisp edge instead of a gradient into its neighbour.
40199
+ const f = span > 0 ? (t - a.position) / span : 1;
40200
+ return {
40201
+ red: a.color.red + (b.color.red - a.color.red) * f,
40202
+ green: a.color.green + (b.color.green - a.color.green) * f,
40203
+ blue: a.color.blue + (b.color.blue - a.color.blue) * f,
40204
+ alpha: a.color.alpha + (b.color.alpha - a.color.alpha) * f
40205
+ };
40206
+ }
40207
+ /**
40208
+ * A ramp baked into RGBA bytes, for a shader that cannot walk stops per fragment.
40209
+ * @param steps entries in the lookup, each covering an equal slice of the value range
40210
+ */
40211
+ function RampLookup(stops, low, high, steps = 256) {
40212
+ const sorted = stops && stops.length > 0
40213
+ ? stops.slice().sort((a, b) => a.position - b.position)
40214
+ : null;
40215
+ const out = new Uint8Array(steps * 4);
40216
+ for (let i = 0; i < steps; i++) {
40217
+ const t = steps > 1 ? i / (steps - 1) : 0;
40218
+ const color = sorted
40219
+ ? SampleRamp(sorted, t)
40220
+ : {
40221
+ red: low.red + (high.red - low.red) * t,
40222
+ green: low.green + (high.green - low.green) * t,
40223
+ blue: low.blue + (high.blue - low.blue) * t,
40224
+ alpha: low.alpha + (high.alpha - low.alpha) * t
40225
+ };
40226
+ out[i * 4] = Math.round(color.red);
40227
+ out[i * 4 + 1] = Math.round(color.green);
40228
+ out[i * 4 + 2] = Math.round(color.blue);
40229
+ out[i * 4 + 3] = Math.round(Math.min(1, Math.max(0, color.alpha)) * 255);
40230
+ }
40231
+ return out;
40232
+ }
40233
+ function ApplyGradientStops(imageData, stops) {
40234
+ if (!stops || stops.length === 0) {
40235
+ return;
40236
+ }
40237
+ const sorted = stops.slice().sort((a, b) => a.position - b.position);
40238
+ const data = imageData.data;
40239
+ for (let i = 0; i < data.length; i += 4) {
40240
+ const t = data[i] / 255;
40241
+ const color = SampleRamp(sorted, t);
40242
+ data[i] = Math.round(color.red);
40243
+ data[i + 1] = Math.round(color.green);
40244
+ data[i + 2] = Math.round(color.blue);
40245
+ const srcAlpha = data[i + 3] / 255;
40246
+ data[i + 3] = Math.round(srcAlpha * color.alpha * 255);
40247
+ }
40248
+ }
40249
+ /**
40250
+ * Draws the texture's own cell grid into the tinted pixels.
40251
+ *
40252
+ * Drawn in texture space rather than screen space so it stays with the cells it describes and needs
40253
+ * no per-frame work as the camera moves.
40254
+ */
40255
+ function ApplyCellBorders(imageData, cellTexels, widthTexels, color) {
40256
+ if (!(cellTexels > 0) || !(widthTexels > 0) || !color || color.alpha <= 0) {
40257
+ return;
40258
+ }
40259
+ const { width, height, data } = imageData;
40260
+ const edge = Math.max(1, Math.round(widthTexels));
40261
+ for (let y = 0; y < height; y++) {
40262
+ const onRow = (y % cellTexels) < edge;
40263
+ for (let x = 0; x < width; x++) {
40264
+ if (!onRow && (x % cellTexels) >= edge) {
40265
+ continue;
40266
+ }
40267
+ const i = (y * width + x) * 4;
40268
+ // Only where something is already drawn, so a border cannot invent coverage the data
40269
+ // does not have.
40270
+ if (data[i + 3] === 0) {
40271
+ continue;
40272
+ }
40273
+ const a = color.alpha;
40274
+ data[i] = Math.round(data[i] * (1 - a) + color.red * a);
40275
+ data[i + 1] = Math.round(data[i + 1] * (1 - a) + color.green * a);
40276
+ data[i + 2] = Math.round(data[i + 2] * (1 - a) + color.blue * a);
40277
+ }
40278
+ }
40279
+ }
40174
40280
  function loadImage(src) {
40175
40281
  return new Promise((res, rej) => {
40176
40282
  const image = new Image();
@@ -40182,6 +40288,87 @@
40182
40288
 
40183
40289
  var TextureFrameSeriesAnimator;
40184
40290
  (function (TextureFrameSeriesAnimator) {
40291
+ // Values are measured up from the ground beneath them.
40292
+ TextureFrameSeriesAnimator.QUANTITY_THICKNESS = "thickness";
40293
+ // Values are heights against a vertical datum, so they need that datum to be placed.
40294
+ TextureFrameSeriesAnimator.QUANTITY_ELEVATION = "elevation";
40295
+ TextureFrameSeriesAnimator.LAYOUT_TILES = "tiles";
40296
+ /**
40297
+ * The generator that produced an archive, for telling a stale one from a current one.
40298
+ *
40299
+ * Read the version rather than sniffing for fields: an archive can legitimately omit Tiles or
40300
+ * GeoidSeparation and still be current, so their absence says nothing about its age.
40301
+ * @param metadata the archive's Data.generation
40302
+ */
40303
+ function GeneratorVersion(metadata) {
40304
+ if (!metadata || typeof metadata.GeneratorVersion !== "number") {
40305
+ return 1;
40306
+ }
40307
+ return metadata.GeneratorVersion;
40308
+ }
40309
+ TextureFrameSeriesAnimator.GeneratorVersion = GeneratorVersion;
40310
+ /**
40311
+ * Whether an archive is an adaptive pyramid rather than one raster.
40312
+ * @param metadata the archive's Data.generation
40313
+ */
40314
+ function IsTiledLayout(metadata) {
40315
+ return (Boolean(metadata && metadata.Layout === TextureFrameSeriesAnimator.LAYOUT_TILES &&
40316
+ metadata.Tiles && metadata.Tiles.length > 0));
40317
+ }
40318
+ TextureFrameSeriesAnimator.IsTiledLayout = IsTiledLayout;
40319
+ // Frames carry the value as the source reported it.
40320
+ TextureFrameSeriesAnimator.MEASURE_ABSOLUTE = "absolute";
40321
+ // Frames carry the value minus each cell's own minimum over the series.
40322
+ TextureFrameSeriesAnimator.MEASURE_ANOMALY = "anomaly";
40323
+ /**
40324
+ * Turns a value in the attribute's own units into the 0 to 1 position the ramp is indexed by.
40325
+ *
40326
+ * Stops are authored in real units, since "hide anything under 0.1 m" is the sentence a user
40327
+ * actually has, and the archive's own published range is what makes that expressible.
40328
+ * @param metadata the archive's Data.generation
40329
+ * @param value in the attribute's units
40330
+ */
40331
+ function NormalisePosition(metadata, value) {
40332
+ const lo = metadata && typeof metadata.ValueMin === "number" ? metadata.ValueMin : 0;
40333
+ const hi = metadata && typeof metadata.ValueMax === "number" ? metadata.ValueMax : 1;
40334
+ const span = hi - lo;
40335
+ if (!(span > 0)) {
40336
+ return 0;
40337
+ }
40338
+ return Math.min(1, Math.max(0, (value - lo) / span));
40339
+ }
40340
+ TextureFrameSeriesAnimator.NormalisePosition = NormalisePosition;
40341
+ /**
40342
+ * Whether the frames are a departure from each cell's own normal rather than a raw reading.
40343
+ *
40344
+ * Worth asking before labelling anything: the two measures need different words for the same
40345
+ * number, and calling an anomaly "water depth" is a worse error than the coarse ramp it replaced.
40346
+ * @param metadata the archive's Data.generation
40347
+ */
40348
+ function IsAnomalyMeasure(metadata) {
40349
+ return Boolean(metadata && metadata.Measure === TextureFrameSeriesAnimator.MEASURE_ANOMALY);
40350
+ }
40351
+ TextureFrameSeriesAnimator.IsAnomalyMeasure = IsAnomalyMeasure;
40352
+ /**
40353
+ * How much the value moves anywhere in the series, in source units.
40354
+ *
40355
+ * Falls back to the published range, which is the right answer for an anomaly archive (its range
40356
+ * IS the movement) and the only available one for an archive predating AnomalyMax.
40357
+ * @param metadata the archive's Data.generation
40358
+ */
40359
+ function MovingRange(metadata) {
40360
+ if (!metadata) {
40361
+ return undefined;
40362
+ }
40363
+ if (typeof metadata.AnomalyMax === "number" && metadata.AnomalyMax > 0) {
40364
+ return metadata.AnomalyMax;
40365
+ }
40366
+ if (typeof metadata.ValueMin === "number" && typeof metadata.ValueMax === "number") {
40367
+ return Math.abs(metadata.ValueMax - metadata.ValueMin);
40368
+ }
40369
+ return undefined;
40370
+ }
40371
+ TextureFrameSeriesAnimator.MovingRange = MovingRange;
40185
40372
  /**
40186
40373
  * Detects whether a ClientFile's `Data.generation` metadata describes a frame archive rather than a single static image,
40187
40374
  * so a caller can decide whether to construct an Animator or fall back to the existing static-texture path.
@@ -40191,15 +40378,51 @@
40191
40378
  return Boolean(data && data.generation && Array.isArray(data.generation.Frames) && data.generation.Frames.length > 0);
40192
40379
  }
40193
40380
  TextureFrameSeriesAnimator.IsFrameArchiveMetadata = IsFrameArchiveMetadata;
40194
- // How far covered values bleed outward into uncovered texels before the GPU samples them.
40381
+ // First resolvable static number out of a calculator field list, since that is all a border needs.
40382
+ function resolveNumber(fields) {
40383
+ if (!fields) {
40384
+ return 0;
40385
+ }
40386
+ for (const field of fields) {
40387
+ const value = Number(field && field.value);
40388
+ if (Number.isFinite(value)) {
40389
+ return value;
40390
+ }
40391
+ }
40392
+ return 0;
40393
+ }
40394
+ // First resolvable static colour out of a calculator field list.
40395
+ function resolveColor(fields) {
40396
+ if (!fields) {
40397
+ return null;
40398
+ }
40399
+ for (const field of fields) {
40400
+ const parsed = typeof (field === null || field === void 0 ? void 0 : field.value) === "string"
40401
+ ? BModels.Color.ColorFromStr(field.value)
40402
+ : null;
40403
+ if (parsed) {
40404
+ return parsed;
40405
+ }
40406
+ }
40407
+ return null;
40408
+ }
40409
+ /*
40410
+ * Everything about an Animator that a style can change, as a comparable string.
40411
+ */
40412
+ function AppearanceSignature(options) {
40413
+ var _a, _b, _c;
40414
+ return JSON.stringify([
40415
+ (_a = options.textureColorMask) !== null && _a !== void 0 ? _a : null,
40416
+ (_b = options.cellBorder) !== null && _b !== void 0 ? _b : null,
40417
+ (_c = options.cellTexels) !== null && _c !== void 0 ? _c : null,
40418
+ Boolean(options.maskBaseline)
40419
+ ]);
40420
+ }
40421
+ TextureFrameSeriesAnimator.AppearanceSignature = AppearanceSignature;
40422
+ const MAX_COMPOSITE_TEXELS = 2048;
40195
40423
  const VALUE_DILATE_PASSES = 2;
40196
40424
  /*
40197
40425
  * Bleeds covered values outward into uncovered texels, leaving alpha untouched.
40198
- *
40199
- * The GPU samples the value texture with linear filtering, so an uncovered texel's RGB still gets
40200
- * averaged into the vertices next to it even though its alpha is zero. Land sits at one end of the
40201
- * ramp, so without this the coastline grows a row of spikes exactly where the data stops. Masking the
40202
- * baseline creates more of these edges, which is what makes this necessary rather than cosmetic.
40203
40426
  */
40204
40427
  function dilateValues(value, width, height, passes) {
40205
40428
  const texels = width * height;
@@ -40245,7 +40468,7 @@
40245
40468
  const DEFAULT_HIGH_COLOR = { red: 21, green: 96, blue: 196, alpha: 0.92 };
40246
40469
  class Animator {
40247
40470
  constructor(options) {
40248
- var _a, _b;
40471
+ var _a, _b, _c;
40249
40472
  this.removeOnTick = null;
40250
40473
  this.removeCrossfadeTick = null;
40251
40474
  this.disposed = false;
@@ -40271,8 +40494,12 @@
40271
40494
  // forcing Cesium to re-upload the texture only when something changed.
40272
40495
  this.pool = [document.createElement("canvas"), document.createElement("canvas")];
40273
40496
  this.poolIdx = 0;
40497
+ this.scratch = null;
40274
40498
  this.valueDims = null;
40275
40499
  this.presentVersion = 0;
40500
+ this.extremesLoad = null;
40501
+ this.floorPixels = null;
40502
+ this.ceilingPixels = null;
40276
40503
  if (!options.entity.polygon) {
40277
40504
  throw new Error("TextureFrameSeriesAnimator requires an entity with polygon graphics.");
40278
40505
  }
@@ -40284,16 +40511,41 @@
40284
40511
  this.archiveUrl = options.archiveUrl;
40285
40512
  this.frames = options.frames;
40286
40513
  this.crossfadeMs = (_a = options.crossfadeMs) !== null && _a !== void 0 ? _a : DEFAULT_CROSSFADE_MS;
40514
+ this.appearance = AppearanceSignature(options);
40287
40515
  const mask = options.textureColorMask;
40288
40516
  this.lowColor = (mask && BModels.Color.ColorFromStr(mask.minColor)) || DEFAULT_LOW_COLOR;
40289
40517
  this.highColor = (mask && BModels.Color.ColorFromStr(mask.maxColor)) || DEFAULT_HIGH_COLOR;
40518
+ // Positions are authored in the attribute's units and normalised once here, so the
40519
+ // per-pixel loop stays a comparison against 0 to 1 like the two-colour path.
40520
+ const points = mask === null || mask === void 0 ? void 0 : mask.points;
40521
+ this.rampStops = (points && points.length > 0)
40522
+ ? points.map((p) => ({
40523
+ position: NormalisePosition(options.metadata, p.position),
40524
+ color: BModels.Color.ColorFromStr(p.color) || DEFAULT_LOW_COLOR
40525
+ }))
40526
+ : null;
40527
+ const border = options.cellBorder;
40528
+ const borderWidth = border ? resolveNumber(border.width) : 0;
40529
+ const borderColor = border ? resolveColor(border.color) : null;
40530
+ // Zero width or a fully transparent colour means no grid, matching how the polygon's own
40531
+ // outline behaves, so an existing style that wants no border keeps getting none.
40532
+ this.cellBorder = (border && borderWidth > 0 && borderColor && borderColor.alpha > 0)
40533
+ ? {
40534
+ cellTexels: Math.max(1, Math.round((_b = options.cellTexels) !== null && _b !== void 0 ? _b : 1)),
40535
+ widthTexels: borderWidth,
40536
+ color: borderColor
40537
+ }
40538
+ : null;
40290
40539
  this.frameDates = this.frames.map((f) => Cesium.JulianDate.fromIso8601(f.Timestamp));
40291
40540
  this.frameCache = new Array(this.frames.length).fill(null);
40292
40541
  this.frameDims = new Array(this.frames.length).fill(null);
40293
40542
  this.valueCache = new Array(this.frames.length).fill(null);
40294
40543
  this.produceValueCanvas = Boolean(options.produceValueCanvas);
40544
+ this.metadata = options.metadata;
40545
+ this.drapeExtent = options.drapeExtent;
40546
+ this.tiles = IsTiledLayout(options.metadata) ? options.metadata.Tiles : undefined;
40295
40547
  this.baselineMaskEntry = options.baselineMask || null;
40296
- this.maskBaseline = (_b = options.maskBaseline) !== null && _b !== void 0 ? _b : Boolean(options.baselineMask);
40548
+ this.maskBaseline = (_c = options.maskBaseline) !== null && _c !== void 0 ? _c : Boolean(options.baselineMask);
40297
40549
  this.valueCanvas = this.produceValueCanvas ? document.createElement("canvas") : null;
40298
40550
  this.driveMaterial = options.driveMaterial !== false;
40299
40551
  this.originalMaterial = options.entity.polygon.material;
@@ -40329,6 +40581,10 @@
40329
40581
  IsDisposed() {
40330
40582
  return this.disposed;
40331
40583
  }
40584
+ // What this Animator was built to look like, for deciding whether it can be reused.
40585
+ GetAppearanceSignature() {
40586
+ return this.appearance;
40587
+ }
40332
40588
  /**
40333
40589
  * The archive URL this instance was constructed with,
40334
40590
  * lets a caller re-rendering the same entity tell whether an existing instance is already correct, without reaching into private state.
@@ -40343,6 +40599,15 @@
40343
40599
  GetImageProperty() {
40344
40600
  return this.imageProperty;
40345
40601
  }
40602
+ /**
40603
+ * The presented frame's TINTED pixels, where alpha is what the ramp actually painted.
40604
+ *
40605
+ * Distinct from the value canvas, whose alpha only says a texel has data. A cell can hold a
40606
+ * reading and still be painted nothing, which is exactly what a hidden floor band does.
40607
+ */
40608
+ GetDisplayedPixels() {
40609
+ return this.displayedPixels;
40610
+ }
40346
40611
  /**
40347
40612
  * The untinted canvas carrying the presented frame's value in RGB and its coverage in alpha,
40348
40613
  * or null unless the instance was constructed with produceValueCanvas.
@@ -40455,11 +40720,33 @@
40455
40720
  });
40456
40721
  }
40457
40722
  async fetchAndTint(idx) {
40458
- const entry = this.frames[idx];
40459
- const start = entry.ByteOffset;
40460
- const end = entry.ByteOffset + entry.ByteLength - 1;
40461
- const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${start}-${end}` } });
40462
- const buffer = await response.arrayBuffer();
40723
+ const tiles = this.tiles;
40724
+ let buffer;
40725
+ let span = 0;
40726
+ if (tiles) {
40727
+ // Every tile's frame for one timestep sits contiguously, because the generator writes
40728
+ // frame major. So a tiled frame is still ONE range request, not one per tile.
40729
+ let lo = Number.MAX_SAFE_INTEGER;
40730
+ let hi = 0;
40731
+ for (const tile of tiles) {
40732
+ const at = tile.Frames[idx];
40733
+ if (!at) {
40734
+ continue;
40735
+ }
40736
+ lo = Math.min(lo, at.ByteOffset);
40737
+ hi = Math.max(hi, at.ByteOffset + at.ByteLength);
40738
+ }
40739
+ span = lo;
40740
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
40741
+ buffer = await response.arrayBuffer();
40742
+ }
40743
+ else {
40744
+ const entry = this.frames[idx];
40745
+ const start = entry.ByteOffset;
40746
+ const end = entry.ByteOffset + entry.ByteLength - 1;
40747
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${start}-${end}` } });
40748
+ buffer = await response.arrayBuffer();
40749
+ }
40463
40750
  if (this.disposed) {
40464
40751
  return;
40465
40752
  }
@@ -40469,7 +40756,9 @@
40469
40756
  if (this.disposed) {
40470
40757
  return;
40471
40758
  }
40472
- const decoded = await this.decodeAndTint(buffer);
40759
+ const decoded = tiles
40760
+ ? await this.composeTiles(tiles, idx, buffer, span)
40761
+ : await this.decodeAndTint(buffer);
40473
40762
  if (this.disposed) {
40474
40763
  return;
40475
40764
  }
@@ -40498,21 +40787,438 @@
40498
40787
  canvas.height = image.height;
40499
40788
  const ctx = canvas.getContext("2d");
40500
40789
  ctx.drawImage(image, 0, 0);
40501
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
40790
+ return this.tint(ctx.getImageData(0, 0, canvas.width, canvas.height));
40791
+ }
40792
+ /*
40793
+ * Draws every tile of one frame into a single raster covering the drape extent.
40794
+ */
40795
+ async composeTiles(tiles, idx, buffer, spanStart) {
40796
+ const target = this.compositeExtent();
40797
+ const size = this.compositeSize(tiles, target);
40798
+ const canvas = document.createElement("canvas");
40799
+ canvas.width = size.width;
40800
+ canvas.height = size.height;
40801
+ const ctx = canvas.getContext("2d");
40802
+ ctx.imageSmoothingEnabled = false;
40803
+ ctx.clearRect(0, 0, size.width, size.height);
40804
+ const images = await this.decodeSlices(buffer, spanStart, tiles.map((tile) => tile.Frames[idx]));
40805
+ for (let i = 0; i < tiles.length; i++) {
40806
+ const image = images[i];
40807
+ if (!image || this.disposed) {
40808
+ continue;
40809
+ }
40810
+ const at = this.tileRect(tiles[i], target, size);
40811
+ ctx.drawImage(image, at.x, at.y, at.w, at.h);
40812
+ }
40813
+ return this.tint(ctx.getImageData(0, 0, size.width, size.height));
40814
+ }
40815
+ /*
40816
+ * Where a composited frame is draped, which is the polygon's rectangle when there is one.
40817
+ */
40818
+ compositeExtent() {
40819
+ if (this.drapeExtent) {
40820
+ return this.drapeExtent;
40821
+ }
40822
+ const m = this.metadata;
40823
+ return {
40824
+ West: m && m.West != null ? m.West : -180,
40825
+ East: m && m.East != null ? m.East : 180,
40826
+ South: m && m.South != null ? m.South : -90,
40827
+ North: m && m.North != null ? m.North : 90
40828
+ };
40829
+ }
40830
+ /*
40831
+ * Composite resolution, capped by what can be uploaded as a texture every frame.
40832
+ */
40833
+ compositeSize(tiles, target) {
40834
+ let finest = Number.MAX_VALUE;
40835
+ for (const tile of tiles) {
40836
+ if (tile.TexelMetres > 0) {
40837
+ finest = Math.min(finest, tile.TexelMetres);
40838
+ }
40839
+ }
40840
+ if (!(finest > 0) || finest === Number.MAX_VALUE) {
40841
+ finest = 1;
40842
+ }
40843
+ const mid = (target.South + target.North) / 2 * Math.PI / 180;
40844
+ const widthM = Math.max((target.East - target.West) * 111320 * Math.cos(mid), 1);
40845
+ const heightM = Math.max((target.North - target.South) * 110574, 1);
40846
+ let width = Math.ceil(widthM / finest);
40847
+ let height = Math.ceil(heightM / finest);
40848
+ const longest = Math.max(width, height);
40849
+ if (longest > MAX_COMPOSITE_TEXELS) {
40850
+ const shrink = MAX_COMPOSITE_TEXELS / longest;
40851
+ width = Math.max(1, Math.round(width * shrink));
40852
+ height = Math.max(1, Math.round(height * shrink));
40853
+ }
40854
+ return { width: Math.max(1, width), height: Math.max(1, height) };
40855
+ }
40856
+ /*
40857
+ * Applies the ramp, borders and baseline mask to raw value pixels, whatever produced them.
40858
+ */
40859
+ tint(imageData) {
40860
+ const canvas = { width: imageData.width, height: imageData.height };
40502
40861
  // Copied before tinting: the ramp is a lerp between two colours, so the value cannot be
40503
40862
  // recovered from the tinted pixels afterwards.
40504
40863
  const valuePixels = this.produceValueCanvas ? new Uint8ClampedArray(imageData.data) : undefined;
40505
- ApplyGrayscaleColorMask(imageData, this.lowColor, this.highColor);
40864
+ // Stops win when supplied: they can express a band and a hidden floor, which a two
40865
+ // colour ramp cannot. The pair stays the fallback so an older style still draws.
40866
+ if (this.rampStops && this.rampStops.length > 0) {
40867
+ ApplyGradientStops(imageData, this.rampStops);
40868
+ }
40869
+ else {
40870
+ ApplyGrayscaleColorMask(imageData, this.lowColor, this.highColor);
40871
+ }
40872
+ if (this.cellBorder) {
40873
+ ApplyCellBorders(imageData, this.cellBorder.cellTexels, this.cellBorder.widthTexels, this.cellBorder.color);
40874
+ }
40506
40875
  this.applyBaselineMask(imageData.data, valuePixels, canvas.width, canvas.height);
40507
40876
  if (valuePixels) {
40508
40877
  dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES);
40509
40878
  }
40510
40879
  return { pixels: imageData.data, width: canvas.width, height: canvas.height, valuePixels };
40511
40880
  }
40881
+ /*
40882
+ * Paints one frame's pixels onto a canvas, resampled into the drape extent when there is one.
40883
+ */
40884
+ writeInto(canvas, pixels, dims) {
40885
+ const rect = this.sourceRect(dims);
40886
+ if (!rect) {
40887
+ canvas.width = dims.width;
40888
+ canvas.height = dims.height;
40889
+ const direct = canvas.getContext("2d");
40890
+ const image = direct.createImageData(dims.width, dims.height);
40891
+ image.data.set(pixels);
40892
+ direct.putImageData(image, 0, 0);
40893
+ return;
40894
+ }
40895
+ if (!this.scratch) {
40896
+ this.scratch = document.createElement("canvas");
40897
+ }
40898
+ this.scratch.width = dims.width;
40899
+ this.scratch.height = dims.height;
40900
+ const scratchCtx = this.scratch.getContext("2d");
40901
+ const image = scratchCtx.createImageData(dims.width, dims.height);
40902
+ image.data.set(pixels);
40903
+ scratchCtx.putImageData(image, 0, 0);
40904
+ canvas.width = dims.width;
40905
+ canvas.height = dims.height;
40906
+ const ctx = canvas.getContext("2d");
40907
+ // Nearest neighbour: a value texel has to survive the resample exactly, or a label reads
40908
+ // a blend of two cells rather than the cell it points at.
40909
+ ctx.imageSmoothingEnabled = false;
40910
+ ctx.clearRect(0, 0, dims.width, dims.height);
40911
+ ctx.drawImage(this.scratch, rect.x, rect.y, rect.width, rect.height, 0, 0, dims.width, dims.height);
40912
+ }
40913
+ /*
40914
+ * The part of the archive raster that the drape extent covers, in source pixels.
40915
+ */
40916
+ sourceRect(dims) {
40917
+ const drape = this.drapeExtent;
40918
+ const m = this.metadata;
40919
+ // A composited frame was drawn into the drape extent already, so resampling it again
40920
+ // would apply the same correction twice.
40921
+ if (this.tiles) {
40922
+ return null;
40923
+ }
40924
+ if (!drape || !m || m.West == null || m.East == null || m.South == null || m.North == null) {
40925
+ return null;
40926
+ }
40927
+ const spanLon = m.East - m.West;
40928
+ const spanLat = m.North - m.South;
40929
+ if (!(spanLon > 0) || !(spanLat > 0)) {
40930
+ return null;
40931
+ }
40932
+ const x = (drape.West - m.West) / spanLon * dims.width;
40933
+ const width = (drape.East - drape.West) / spanLon * dims.width;
40934
+ const y = (m.North - drape.North) / spanLat * dims.height;
40935
+ const height = (drape.North - drape.South) / spanLat * dims.height;
40936
+ // A drape that already matches the archive is the common case and must not pay for a
40937
+ // resample, nor lose a half pixel to rounding.
40938
+ if (Math.abs(x) < 0.01 && Math.abs(y) < 0.01
40939
+ && Math.abs(width - dims.width) < 0.01 && Math.abs(height - dims.height) < 0.01) {
40940
+ return null;
40941
+ }
40942
+ return { x, y, width, height };
40943
+ }
40944
+ /**
40945
+ * The archive's per-cell floor and ceiling rasters, once EnsureExtremes has resolved.
40946
+ */
40947
+ GetExtremes() {
40948
+ return { floor: this.floorPixels, ceiling: this.ceilingPixels };
40949
+ }
40950
+ /**
40951
+ * Fetches and decodes the floor and ceiling rasters, at most once.
40952
+ */
40953
+ EnsureExtremes() {
40954
+ if (this.extremesLoad) {
40955
+ return this.extremesLoad;
40956
+ }
40957
+ if (this.tiles) {
40958
+ this.extremesLoad = this.loadTiledExtremes();
40959
+ return this.extremesLoad;
40960
+ }
40961
+ const floor = this.metadata && this.metadata.Floor;
40962
+ const ceiling = this.metadata && this.metadata.Ceiling;
40963
+ if (!floor && !ceiling) {
40964
+ this.extremesLoad = Promise.resolve();
40965
+ return this.extremesLoad;
40966
+ }
40967
+ this.extremesLoad = Promise.all([
40968
+ floor ? this.loadImageDataAt(floor) : Promise.resolve(null),
40969
+ ceiling ? this.loadImageDataAt(ceiling) : Promise.resolve(null)
40970
+ ]).then(([f, c]) => {
40971
+ if (this.disposed) {
40972
+ return;
40973
+ }
40974
+ this.floorPixels = f;
40975
+ this.ceilingPixels = c;
40976
+ }).catch((e) => {
40977
+ // Missing extremes cost a label its range, and must not take the animation with them.
40978
+ console.warn("TextureFrameSeriesAnimator: could not load the extremes rasters.", e);
40979
+ });
40980
+ return this.extremesLoad;
40981
+ }
40982
+ /*
40983
+ * Composites each tile's baseline mask into one covering the composited frames.
40984
+ *
40985
+ * A tiled archive publishes the mask per tile, so reading the archive level BaselineMask
40986
+ * finds nothing and hiding dry cells silently does nothing at all.
40987
+ */
40988
+ async loadTiledBaselineMask() {
40989
+ const tiles = this.tiles;
40990
+ if (!tiles) {
40991
+ return;
40992
+ }
40993
+ const entries = tiles.map((tile) => tile.BaselineMask);
40994
+ let lo = Number.MAX_SAFE_INTEGER;
40995
+ let hi = 0;
40996
+ for (const at of entries) {
40997
+ if (!at) {
40998
+ continue;
40999
+ }
41000
+ lo = Math.min(lo, at.ByteOffset);
41001
+ hi = Math.max(hi, at.ByteOffset + at.ByteLength);
41002
+ }
41003
+ if (!(hi > lo)) {
41004
+ return;
41005
+ }
41006
+ try {
41007
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
41008
+ const block = await response.arrayBuffer();
41009
+ if (this.disposed) {
41010
+ return;
41011
+ }
41012
+ const images = await this.decodeSlices(block, lo, entries);
41013
+ if (this.disposed) {
41014
+ return;
41015
+ }
41016
+ const target = this.compositeExtent();
41017
+ const size = this.compositeSize(tiles, target);
41018
+ const canvas = document.createElement("canvas");
41019
+ canvas.width = size.width;
41020
+ canvas.height = size.height;
41021
+ const ctx = canvas.getContext("2d");
41022
+ ctx.imageSmoothingEnabled = false;
41023
+ ctx.clearRect(0, 0, size.width, size.height);
41024
+ for (let i = 0; i < tiles.length; i++) {
41025
+ if (!images[i]) {
41026
+ continue;
41027
+ }
41028
+ const at = this.tileRect(tiles[i], target, size);
41029
+ ctx.drawImage(images[i], at.x, at.y, at.w, at.h);
41030
+ }
41031
+ const pixels = ctx.getImageData(0, 0, size.width, size.height).data;
41032
+ const flags = new Uint8Array(size.width * size.height);
41033
+ for (let p = 0; p < flags.length; p++) {
41034
+ flags[p] = pixels[p * 4 + 3] >= 128 ? 1 : 0;
41035
+ }
41036
+ this.baselineFlags = flags;
41037
+ this.baselineDims = { width: size.width, height: size.height };
41038
+ }
41039
+ catch (e) {
41040
+ console.warn("TextureFrameSeriesAnimator: could not load the tiled baseline mask.", e);
41041
+ }
41042
+ }
41043
+ /*
41044
+ * Composites each tile's floor and ceiling into rasters matching the composited frames.
41045
+ *
41046
+ * A tiled archive has no whole-extent extremes to fetch, only per-tile ones, and a label
41047
+ * indexes them by the same texel as the value canvas, so they have to be laid out the same way.
41048
+ */
41049
+ async loadTiledExtremes() {
41050
+ const tiles = this.tiles;
41051
+ if (!tiles) {
41052
+ return;
41053
+ }
41054
+ const target = this.compositeExtent();
41055
+ const size = this.compositeSize(tiles, target);
41056
+ // One request for the whole extremes block. They sit contiguously after the frames, so
41057
+ // fetching per tile would be 164 round trips for an 82 tile pyramid.
41058
+ let lo = Number.MAX_SAFE_INTEGER;
41059
+ let hi = 0;
41060
+ for (const tile of tiles) {
41061
+ for (const at of [tile.Floor, tile.Ceiling]) {
41062
+ if (!at) {
41063
+ continue;
41064
+ }
41065
+ lo = Math.min(lo, at.ByteOffset);
41066
+ hi = Math.max(hi, at.ByteOffset + at.ByteLength);
41067
+ }
41068
+ }
41069
+ if (!(hi > lo)) {
41070
+ return;
41071
+ }
41072
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
41073
+ const block = await response.arrayBuffer();
41074
+ if (this.disposed) {
41075
+ return;
41076
+ }
41077
+ const draw = async (pick) => {
41078
+ const images = await this.decodeSlices(block, lo, tiles.map(pick));
41079
+ if (this.disposed) {
41080
+ return null;
41081
+ }
41082
+ const canvas = document.createElement("canvas");
41083
+ canvas.width = size.width;
41084
+ canvas.height = size.height;
41085
+ const ctx = canvas.getContext("2d");
41086
+ ctx.imageSmoothingEnabled = false;
41087
+ ctx.clearRect(0, 0, size.width, size.height);
41088
+ let drew = false;
41089
+ for (let i = 0; i < tiles.length; i++) {
41090
+ const image = images[i];
41091
+ if (!image) {
41092
+ continue;
41093
+ }
41094
+ const at = this.tileRect(tiles[i], target, size);
41095
+ ctx.drawImage(image, at.x, at.y, at.w, at.h);
41096
+ drew = true;
41097
+ }
41098
+ return drew ? ctx.getImageData(0, 0, size.width, size.height) : null;
41099
+ };
41100
+ try {
41101
+ this.floorPixels = await draw((tile) => tile.Floor);
41102
+ this.ceilingPixels = await draw((tile) => tile.Ceiling);
41103
+ }
41104
+ catch (e) {
41105
+ console.warn("TextureFrameSeriesAnimator: could not load the tiled extremes.", e);
41106
+ }
41107
+ }
41108
+ /*
41109
+ * Where one tile lands on the composite, snapped to whole pixels.
41110
+ *
41111
+ * Rounding each edge rather than the origin and size is what makes neighbours meet exactly:
41112
+ * a tile's right edge and the next tile's left edge round to the same pixel, so no seam of
41113
+ * background shows between them.
41114
+ */
41115
+ tileRect(tile, target, size) {
41116
+ const spanLon = target.East - target.West;
41117
+ const spanLat = target.North - target.South;
41118
+ const x0 = Math.round((tile.West - target.West) / spanLon * size.width);
41119
+ const x1 = Math.round((tile.East - target.West) / spanLon * size.width);
41120
+ const y0 = Math.round((target.North - tile.North) / spanLat * size.height);
41121
+ const y1 = Math.round((target.North - tile.South) / spanLat * size.height);
41122
+ return { x: x0, y: y0, w: Math.max(1, x1 - x0), h: Math.max(1, y1 - y0) };
41123
+ }
41124
+ /*
41125
+ * Decodes many tile images at once.
41126
+ *
41127
+ * Serially awaiting each decode makes a frame cost as many round trips through the image
41128
+ * decoder as there are tiles, which for an 82 tile pyramid is seconds rather than one.
41129
+ */
41130
+ decodeSlices(block, base, entries) {
41131
+ return Promise.all(entries.map(async (at) => {
41132
+ if (!at) {
41133
+ return null;
41134
+ }
41135
+ const slice = block.slice(at.ByteOffset - base, at.ByteOffset - base + at.ByteLength);
41136
+ const objectUrl = URL.createObjectURL(new Blob([slice], { type: "image/png" }));
41137
+ try {
41138
+ return await loadImage(objectUrl);
41139
+ }
41140
+ catch {
41141
+ return null;
41142
+ }
41143
+ finally {
41144
+ URL.revokeObjectURL(objectUrl);
41145
+ }
41146
+ }));
41147
+ }
41148
+ /*
41149
+ * Range GETs one entry and decodes it to an image.
41150
+ */
41151
+ async loadImageAt(entry) {
41152
+ const start = entry.ByteOffset;
41153
+ const end = entry.ByteOffset + entry.ByteLength - 1;
41154
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${start}-${end}` } });
41155
+ const buffer = await response.arrayBuffer();
41156
+ const objectUrl = URL.createObjectURL(new Blob([buffer], { type: "image/png" }));
41157
+ try {
41158
+ return await loadImage(objectUrl);
41159
+ }
41160
+ finally {
41161
+ URL.revokeObjectURL(objectUrl);
41162
+ }
41163
+ }
41164
+ /*
41165
+ * Puts a static raster through the same resample the frames get.
41166
+ */
41167
+ remap(image) {
41168
+ if (!image) {
41169
+ return null;
41170
+ }
41171
+ const rect = this.sourceRect({ width: image.width, height: image.height });
41172
+ if (!rect) {
41173
+ return image;
41174
+ }
41175
+ const from = document.createElement("canvas");
41176
+ from.width = image.width;
41177
+ from.height = image.height;
41178
+ from.getContext("2d").putImageData(image, 0, 0);
41179
+ const to = document.createElement("canvas");
41180
+ to.width = image.width;
41181
+ to.height = image.height;
41182
+ const ctx = to.getContext("2d");
41183
+ ctx.imageSmoothingEnabled = false;
41184
+ ctx.clearRect(0, 0, image.width, image.height);
41185
+ ctx.drawImage(from, rect.x, rect.y, rect.width, rect.height, 0, 0, image.width, image.height);
41186
+ return ctx.getImageData(0, 0, image.width, image.height);
41187
+ }
41188
+ /*
41189
+ * Range GETs one entry out of the blob and decodes it to pixels.
41190
+ */
41191
+ async loadImageDataAt(entry) {
41192
+ const start = entry.ByteOffset;
41193
+ const end = entry.ByteOffset + entry.ByteLength - 1;
41194
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${start}-${end}` } });
41195
+ const buffer = await response.arrayBuffer();
41196
+ const blob = new Blob([buffer], { type: "image/png" });
41197
+ const objectUrl = URL.createObjectURL(blob);
41198
+ let image;
41199
+ try {
41200
+ image = await loadImage(objectUrl);
41201
+ }
41202
+ finally {
41203
+ URL.revokeObjectURL(objectUrl);
41204
+ }
41205
+ const canvas = document.createElement("canvas");
41206
+ canvas.width = image.width;
41207
+ canvas.height = image.height;
41208
+ const ctx = canvas.getContext("2d");
41209
+ ctx.drawImage(image, 0, 0);
41210
+ return ctx.getImageData(0, 0, canvas.width, canvas.height);
41211
+ }
40512
41212
  /**
40513
41213
  * Fetches and decodes the archive's static baseline mask, at most once.
40514
41214
  */
40515
41215
  ensureBaselineMask() {
41216
+ if (this.maskBaseline && this.tiles) {
41217
+ if (!this.baselineLoad) {
41218
+ this.baselineLoad = this.loadTiledBaselineMask();
41219
+ }
41220
+ return this.baselineLoad;
41221
+ }
40516
41222
  if (!this.maskBaseline || !this.baselineMaskEntry) {
40517
41223
  return Promise.resolve();
40518
41224
  }
@@ -40625,24 +41331,17 @@
40625
41331
  presentPixels(pixels, dims, valuePixels) {
40626
41332
  this.poolIdx = 1 - this.poolIdx;
40627
41333
  const target = this.pool[this.poolIdx];
40628
- target.width = dims.width;
40629
- target.height = dims.height;
40630
- const ctx = target.getContext("2d");
40631
- const imgData = ctx.createImageData(dims.width, dims.height);
40632
- imgData.data.set(pixels);
40633
- ctx.putImageData(imgData, 0, 0);
41334
+ this.writeInto(target, pixels, dims);
40634
41335
  this.displayedPixels = pixels;
40635
41336
  if (this.valueCanvas && valuePixels) {
40636
- this.valueCanvas.width = dims.width;
40637
- this.valueCanvas.height = dims.height;
40638
- const valueCtx = this.valueCanvas.getContext("2d");
40639
- const valueImgData = valueCtx.createImageData(dims.width, dims.height);
40640
- valueImgData.data.set(valuePixels);
40641
- valueCtx.putImageData(valueImgData, 0, 0);
41337
+ this.writeInto(this.valueCanvas, valuePixels, dims);
40642
41338
  this.displayedValuePixels = valuePixels;
40643
41339
  this.valueDims = dims;
40644
41340
  }
40645
41341
  this.presentVersion++;
41342
+ if (this.viewer && this.viewer.scene && this.viewer.scene.requestRenderMode) {
41343
+ this.viewer.scene.requestRender();
41344
+ }
40646
41345
  }
40647
41346
  }
40648
41347
  TextureFrameSeriesAnimator.Animator = Animator;
@@ -40654,6 +41353,684 @@
40654
41353
  }
40655
41354
  })(TextureFrameSeriesAnimator || (TextureFrameSeriesAnimator = {}));
40656
41355
 
41356
+ (function (TextureValueLabels) {
41357
+ // Off by default: a caller can still set a floor, but thinning is the mechanism that keeps a
41358
+ // distant view readable.
41359
+ const DEFAULT_MIN_CELL_PIXELS = 0;
41360
+ const MIN_VISIBLE_PIXELS = 60;
41361
+ const MAX_STRIDE = 512;
41362
+ const STRIDE_HYSTERESIS = 1.4;
41363
+ const DEFAULT_MIN_SPACING_PIXELS = 78;
41364
+ const DEFAULT_MAX_LABELS = 240;
41365
+ const LABEL_RADIUS = 4;
41366
+ const LABEL_FONT = "600 11px ui-monospace, SFMono-Regular, Menlo, monospace";
41367
+ // Clear space either side of a label box, so neighbours read as separate.
41368
+ const LABEL_GAP_PIXELS = 16;
41369
+ const LABEL_ANCHOR_COLOR = "rgba(255,255,255,0.85)";
41370
+ // 8 rather than 5: below this a cell reads as texture rather than as a boundary, and it also
41371
+ // bounds the worst case, since a full screen of 8 px cells fits inside MAX_GRID_CELLS.
41372
+ const DEFAULT_MIN_GRID_CELL_PIXELS = 8;
41373
+ const DEFAULT_GRID_WIDTH_PIXELS = 1;
41374
+ // Enough for a full screen at the minimum legible cell size, with headroom for an oblique view
41375
+ // whose texel window is larger than the screen. The budget is shared across tiles, so sizing it
41376
+ // too tightly made the grid depend on which tile happened to be walked first.
41377
+ const MAX_GRID_CELLS = 40000;
41378
+ // Probes per side when hunting a labellable cell inside one lattice block.
41379
+ const MAX_LABEL_PROBES_PER_SIDE = 8;
41380
+ const TILE_OUTLINE_ALPHA = 0.45;
41381
+ // Cells per grid square at the coarsest. Beyond this a tile is outlined instead.
41382
+ const MAX_GRID_STRIDE = 256;
41383
+ const LINE_HEIGHT = 13;
41384
+ /**
41385
+ * A clip outline from a projected ring, or null when the ring cannot be drawn.
41386
+ * @param projected one entry per ring vertex, null where the vertex has no window coordinate
41387
+ */
41388
+ function ClipOutline(projected) {
41389
+ if (projected.length < 3) {
41390
+ return null;
41391
+ }
41392
+ const out = [];
41393
+ for (const at of projected) {
41394
+ if (!at) {
41395
+ return null;
41396
+ }
41397
+ out.push(at);
41398
+ }
41399
+ return out;
41400
+ }
41401
+ TextureValueLabels.ClipOutline = ClipOutline;
41402
+ /**
41403
+ * How many cells apart labels should sit, as a power of two.
41404
+ * @param options cellPixels is one cell's size on screen, held is the stride already in use
41405
+ */
41406
+ function ChooseLabelStride(options) {
41407
+ const { cellPixels, visibleCols, visibleRows, maxLabels, minSpacingPixels } = options;
41408
+ const fits = (s, slack) => cellPixels * s >= minSpacingPixels / slack
41409
+ && Math.ceil(visibleCols / s) * Math.ceil(visibleRows / s) <= maxLabels * slack;
41410
+ let stride = 1;
41411
+ while (stride < MAX_STRIDE && !fits(stride, 1)) {
41412
+ stride *= 2;
41413
+ }
41414
+ const held = options.held;
41415
+ if (held && fits(held, STRIDE_HYSTERESIS)) {
41416
+ if (held === stride / 2 && cellPixels * held >= minSpacingPixels / STRIDE_HYSTERESIS) {
41417
+ return held;
41418
+ }
41419
+ if (held === stride * 2 && cellPixels * stride < minSpacingPixels * STRIDE_HYSTERESIS) {
41420
+ return held;
41421
+ }
41422
+ }
41423
+ return stride;
41424
+ }
41425
+ TextureValueLabels.ChooseLabelStride = ChooseLabelStride;
41426
+ class Labels {
41427
+ constructor(options) {
41428
+ this.source = null;
41429
+ this.painted = null;
41430
+ this.floor = null;
41431
+ this.ceiling = null;
41432
+ this.disposed = false;
41433
+ this.drawn = 0;
41434
+ this.stride = 0;
41435
+ this.viewer = options.viewer;
41436
+ this.extent = options.extent;
41437
+ this.settings = options.settings || {};
41438
+ this.metadata = options.metadata;
41439
+ this.tiles = options.tiles;
41440
+ this.clipRing = options.clipRing;
41441
+ this.canvas = document.createElement("canvas");
41442
+ this.canvas.style.position = "absolute";
41443
+ this.canvas.style.inset = "0";
41444
+ this.canvas.style.pointerEvents = "none";
41445
+ const parent = this.viewer.canvas.parentElement;
41446
+ if (parent) {
41447
+ parent.appendChild(this.canvas);
41448
+ }
41449
+ this.ctx = this.canvas.getContext("2d");
41450
+ }
41451
+ SetExtent(extent) {
41452
+ this.extent = extent;
41453
+ }
41454
+ /**
41455
+ * The current frame's decoded values, as the animator's value canvas already provides them.
41456
+ * @param source value pixels, one texel per cell
41457
+ */
41458
+ SetSource(source) {
41459
+ this.source = source;
41460
+ }
41461
+ /**
41462
+ * The tinted pixels of the same frame, so the grid can skip cells the ramp painted nothing in.
41463
+ * @param painted RGBA of the presented frame, or null to fall back to coverage alone
41464
+ */
41465
+ SetPainted(painted) {
41466
+ this.painted = painted;
41467
+ }
41468
+ /**
41469
+ * The archive's per-cell extremes, when it publishes them. Without these a label can still
41470
+ * print the current reading, just not the range behind it.
41471
+ */
41472
+ SetExtremes(floor, ceiling) {
41473
+ this.floor = floor;
41474
+ this.ceiling = ceiling;
41475
+ }
41476
+ GetDrawnCount() {
41477
+ return this.drawn;
41478
+ }
41479
+ Clear() {
41480
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
41481
+ this.drawn = 0;
41482
+ }
41483
+ Dispose() {
41484
+ this.disposed = true;
41485
+ if (this.canvas.parentElement) {
41486
+ this.canvas.parentElement.removeChild(this.canvas);
41487
+ }
41488
+ }
41489
+ /**
41490
+ * Redraws every visible label. Cheap enough to call per rendered frame, because the cell
41491
+ * count it considers is bounded by the view rather than by the texture.
41492
+ */
41493
+ Render() {
41494
+ var _a, _b, _c;
41495
+ if (this.disposed || !this.source) {
41496
+ return;
41497
+ }
41498
+ const view = this.viewer.canvas;
41499
+ if (this.canvas.width !== view.clientWidth || this.canvas.height !== view.clientHeight) {
41500
+ this.canvas.width = view.clientWidth;
41501
+ this.canvas.height = view.clientHeight;
41502
+ }
41503
+ this.Clear();
41504
+ const cols = this.source.width;
41505
+ const rows = this.source.height;
41506
+ const window = this.visibleTexels(cols, rows);
41507
+ if (!window) {
41508
+ return;
41509
+ }
41510
+ const midCol = (window.col0 + window.col1) / 2;
41511
+ const midRow = (window.row0 + window.row1) / 2;
41512
+ const a = this.project(this.lonAt(midCol, cols), this.latAt(midRow, rows));
41513
+ const b = this.project(this.lonAt(midCol + 1, cols), this.latAt(midRow + 1, rows));
41514
+ if (!a || !b) {
41515
+ return;
41516
+ }
41517
+ const cellPixels = Math.min(Math.abs(b.x - a.x), Math.abs(b.y - a.y));
41518
+ // Ahead of the label threshold on purpose: a grid still reads at a cell size where a
41519
+ // number no longer fits.
41520
+ this.drawGrid(window, cols, rows, cellPixels);
41521
+ if (cellPixels < ((_a = this.settings.minCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_CELL_PIXELS)) {
41522
+ return;
41523
+ }
41524
+ const visibleWidth = cellPixels * (window.col1 - window.col0 + 1);
41525
+ const visibleHeight = cellPixels * (window.row1 - window.row0 + 1);
41526
+ if (Math.max(visibleWidth, visibleHeight) < MIN_VISIBLE_PIXELS) {
41527
+ return;
41528
+ }
41529
+ const parts = this.settings.parts || ["value", "floor", "ceiling"];
41530
+ this.ctx.font = LABEL_FONT;
41531
+ this.ctx.textAlign = "center";
41532
+ const maxLabels = (_b = this.settings.maxLabels) !== null && _b !== void 0 ? _b : DEFAULT_MAX_LABELS;
41533
+ this.stride = ChooseLabelStride({
41534
+ cellPixels,
41535
+ visibleCols: window.col1 - window.col0 + 1,
41536
+ visibleRows: window.row1 - window.row0 + 1,
41537
+ maxLabels,
41538
+ // Measured, not assumed: the spacing that keeps labels apart is whatever the widest
41539
+ // one is, so changing what a label says cannot quietly start overlapping them.
41540
+ minSpacingPixels: Math.max((_c = this.settings.minSpacingPixels) !== null && _c !== void 0 ? _c : DEFAULT_MIN_SPACING_PIXELS, this.widestLabel(parts)),
41541
+ held: this.stride
41542
+ });
41543
+ const stride = this.stride;
41544
+ const firstRow = Math.floor(window.row0 / stride) * stride;
41545
+ const firstCol = Math.floor(window.col0 / stride) * stride;
41546
+ for (let row = firstRow; row <= window.row1 && this.drawn < maxLabels; row += stride) {
41547
+ for (let col = firstCol; col <= window.col1 && this.drawn < maxLabels; col += stride) {
41548
+ const found = this.readNear(col, row, cols, rows, stride);
41549
+ if (!found) {
41550
+ continue;
41551
+ }
41552
+ const point = this.project(this.lonAt(found.col, cols), this.latAt(found.row, rows));
41553
+ if (!point || point.x < 0 || point.y < 0
41554
+ || point.x > this.canvas.width || point.y > this.canvas.height) {
41555
+ continue;
41556
+ }
41557
+ this.drawLabel(point, found.reading, parts, this.cellOutline(found.col, found.row, cols, rows));
41558
+ }
41559
+ }
41560
+ }
41561
+ /*
41562
+ * Outlines each covered cell, from a projected lattice of its corners.
41563
+ */
41564
+ drawGrid(window, cols, rows, cellPixels) {
41565
+ var _a, _b;
41566
+ const color = this.settings.gridColor;
41567
+ if (!color) {
41568
+ return;
41569
+ }
41570
+ if (this.tiles && this.tiles.length) {
41571
+ this.ctx.save();
41572
+ this.applyRingClip();
41573
+ this.drawTileGrid(color);
41574
+ this.ctx.restore();
41575
+ return;
41576
+ }
41577
+ if (cellPixels < ((_a = this.settings.minGridCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_GRID_CELL_PIXELS)) {
41578
+ return;
41579
+ }
41580
+ // Corners are shared by four cells, so projecting the lattice once rather than per cell
41581
+ // cuts the work by about four and keeps neighbouring outlines exactly coincident.
41582
+ const wide = window.col1 - window.col0 + 1;
41583
+ const high = window.row1 - window.row0 + 1;
41584
+ if (wide * high > MAX_GRID_CELLS) {
41585
+ return;
41586
+ }
41587
+ const lattice = new Array((wide + 1) * (high + 1));
41588
+ for (let r = 0; r <= high; r++) {
41589
+ for (let c = 0; c <= wide; c++) {
41590
+ // Corners sit half a texel out from the centres lonAt/latAt return.
41591
+ const lon = this.lonAt(window.col0 + c - 0.5, cols);
41592
+ const lat = this.latAt(window.row0 + r - 0.5, rows);
41593
+ lattice[r * (wide + 1) + c] = this.project(lon, lat);
41594
+ }
41595
+ }
41596
+ const path = new Path2D();
41597
+ let drew = false;
41598
+ for (let r = 0; r < high; r++) {
41599
+ for (let c = 0; c < wide; c++) {
41600
+ if (!this.isCovered(window.col0 + c, window.row0 + r, cols)) {
41601
+ continue;
41602
+ }
41603
+ const tl = lattice[r * (wide + 1) + c];
41604
+ const tr = lattice[r * (wide + 1) + c + 1];
41605
+ const bl = lattice[(r + 1) * (wide + 1) + c];
41606
+ const br = lattice[(r + 1) * (wide + 1) + c + 1];
41607
+ if (!tl || !tr || !bl || !br) {
41608
+ continue;
41609
+ }
41610
+ path.moveTo(tl.x, tl.y);
41611
+ path.lineTo(tr.x, tr.y);
41612
+ path.lineTo(br.x, br.y);
41613
+ path.lineTo(bl.x, bl.y);
41614
+ path.closePath();
41615
+ drew = true;
41616
+ }
41617
+ }
41618
+ if (!drew) {
41619
+ return;
41620
+ }
41621
+ this.ctx.strokeStyle = color;
41622
+ this.ctx.lineWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
41623
+ this.ctx.stroke(path);
41624
+ +" alpha" + this.ctx.globalAlpha;
41625
+ }
41626
+ /*
41627
+ * Restricts drawing to the polygon, so nothing is ruled over ground it does not cover.
41628
+ */
41629
+ applyRingClip() {
41630
+ const ring = this.clipRing;
41631
+ if (!ring || ring.length < 3) {
41632
+ return;
41633
+ }
41634
+ const outline = ClipOutline(ring.map(point => this.project(point.lon, point.lat)));
41635
+ if (!outline) {
41636
+ return;
41637
+ }
41638
+ const path = new Path2D();
41639
+ path.moveTo(outline[0].x, outline[0].y);
41640
+ for (let i = 1; i < outline.length; i++) {
41641
+ path.lineTo(outline[i].x, outline[i].y);
41642
+ }
41643
+ path.closePath();
41644
+ this.ctx.clip(path);
41645
+ }
41646
+ /*
41647
+ * The cell of whichever tile covers a point, as screen corners.
41648
+ */
41649
+ tileCellOutline(lon, lat) {
41650
+ for (const tile of this.tiles) {
41651
+ if (lon < tile.West || lon > tile.East || lat < tile.South || lat > tile.North) {
41652
+ continue;
41653
+ }
41654
+ const c = (Math.min(tile.ResolutionX - 1, Math.max(0, Math.floor((lon - tile.West) / (tile.East - tile.West) * tile.ResolutionX))));
41655
+ const r = (Math.min(tile.ResolutionY - 1, Math.max(0, Math.floor((tile.North - lat) / (tile.North - tile.South) * tile.ResolutionY))));
41656
+ const w0 = tile.West + (c / tile.ResolutionX) * (tile.East - tile.West);
41657
+ const w1 = tile.West + ((c + 1) / tile.ResolutionX) * (tile.East - tile.West);
41658
+ const n0 = tile.North - (r / tile.ResolutionY) * (tile.North - tile.South);
41659
+ const n1 = tile.North - ((r + 1) / tile.ResolutionY) * (tile.North - tile.South);
41660
+ const corners = [
41661
+ this.project(w0, n0), this.project(w1, n0),
41662
+ this.project(w1, n1), this.project(w0, n1)
41663
+ ];
41664
+ return corners.every((q) => q) ? corners : null;
41665
+ }
41666
+ return null;
41667
+ }
41668
+ /*
41669
+ * Outlines each tile's own cells, so cell size still shows where the mesh is refined.
41670
+ */
41671
+ drawTileGrid(color) {
41672
+ var _a, _b, _c;
41673
+ const view = this.viewer.camera.computeViewRectangle(Cesium.Ellipsoid.WGS84);
41674
+ if (!view) {
41675
+ return;
41676
+ }
41677
+ const west = Cesium.Math.toDegrees(view.west);
41678
+ const east = Cesium.Math.toDegrees(view.east);
41679
+ const south = Cesium.Math.toDegrees(view.south);
41680
+ const north = Cesium.Math.toDegrees(view.north);
41681
+ const minCell = (_a = this.settings.minGridCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_GRID_CELL_PIXELS;
41682
+ const focus = this.groundAtScreenCentre();
41683
+ const path = new Path2D();
41684
+ // A tile too fine to rule, or one the budget cannot afford, contributes its own outline
41685
+ // instead of vanishing. Dropping such tiles entirely is what made whole regions blink out.
41686
+ const coarse = new Path2D();
41687
+ let drew = false;
41688
+ let drewCoarse = false;
41689
+ const outlineTile = (tile) => {
41690
+ const tl = this.project(tile.West, tile.North);
41691
+ const tr = this.project(tile.East, tile.North);
41692
+ const br = this.project(tile.East, tile.South);
41693
+ const bl = this.project(tile.West, tile.South);
41694
+ if (!tl || !tr || !br || !bl) {
41695
+ return;
41696
+ }
41697
+ coarse.moveTo(tl.x, tl.y);
41698
+ coarse.lineTo(tr.x, tr.y);
41699
+ coarse.lineTo(br.x, br.y);
41700
+ coarse.lineTo(bl.x, bl.y);
41701
+ coarse.closePath();
41702
+ drewCoarse = true;
41703
+ };
41704
+ // Planned for every visible tile BEFORE any is drawn.
41705
+ const planned = [];
41706
+ for (const tile of this.tiles) {
41707
+ if (tile.East < west || tile.West > east || tile.North < south || tile.South > north) {
41708
+ continue;
41709
+ }
41710
+ const c0 = Math.max(0, Math.floor((Math.max(west, tile.West) - tile.West)
41711
+ / (tile.East - tile.West) * tile.ResolutionX));
41712
+ const c1 = Math.min(tile.ResolutionX, Math.ceil((Math.min(east, tile.East) - tile.West)
41713
+ / (tile.East - tile.West) * tile.ResolutionX));
41714
+ const r0 = Math.max(0, Math.floor((tile.North - Math.min(north, tile.North))
41715
+ / (tile.North - tile.South) * tile.ResolutionY));
41716
+ const r1 = Math.min(tile.ResolutionY, Math.ceil((tile.North - Math.max(south, tile.South))
41717
+ / (tile.North - tile.South) * tile.ResolutionY));
41718
+ if (c1 <= c0 || r1 <= r0) {
41719
+ continue;
41720
+ }
41721
+ const focusC = focus
41722
+ ? Math.round((focus.lon - tile.West) / (tile.East - tile.West) * tile.ResolutionX)
41723
+ : Math.floor((c0 + c1) / 2);
41724
+ const focusR = focus
41725
+ ? Math.round((tile.North - focus.lat) / (tile.North - tile.South) * tile.ResolutionY)
41726
+ : Math.floor((r0 + r1) / 2);
41727
+ const midC = Math.min(c1 - 1, Math.max(c0, focusC));
41728
+ const midR = Math.min(r1 - 1, Math.max(r0, focusR));
41729
+ const cellLon = (tile.East - tile.West) / tile.ResolutionX;
41730
+ const cellLat = (tile.North - tile.South) / tile.ResolutionY;
41731
+ const at = this.project(tile.West + midC * cellLon, tile.North - midR * cellLat);
41732
+ const next = (this.project(tile.West + (midC + 1) * cellLon, tile.North - (midR + 1) * cellLat));
41733
+ if (!at || !next) {
41734
+ outlineTile(tile);
41735
+ continue;
41736
+ }
41737
+ const onScreen = Math.min(Math.abs(next.x - at.x), Math.abs(next.y - at.y));
41738
+ let step = 1;
41739
+ while (step < MAX_GRID_STRIDE && onScreen * step < minCell) {
41740
+ step *= 2;
41741
+ }
41742
+ if (onScreen * step < minCell) {
41743
+ outlineTile(tile);
41744
+ continue;
41745
+ }
41746
+ planned.push({ tile, c0, c1, r0, r1, step });
41747
+ }
41748
+ // Coarsened together rather than dropped one by one, so a busy view thins out evenly.
41749
+ const blocksOf = (p) => Math.ceil((p.c1 - p.c0) / p.step) * Math.ceil((p.r1 - p.r0) / p.step);
41750
+ let total = planned.reduce((sum, p) => sum + blocksOf(p), 0);
41751
+ while (total > MAX_GRID_CELLS && planned.some((p) => p.step < MAX_GRID_STRIDE)) {
41752
+ for (const p of planned) {
41753
+ p.step = Math.min(MAX_GRID_STRIDE, p.step * 2);
41754
+ }
41755
+ total = planned.reduce((sum, p) => sum + blocksOf(p), 0);
41756
+ }
41757
+ for (const { tile, c0, c1, r0, r1, step } of planned) {
41758
+ // Anchored to the tile's own origin so lines stay on the same boundaries as the camera
41759
+ // moves, and so each step up is a subset of the one below it.
41760
+ const firstR = Math.floor(r0 / step) * step;
41761
+ const firstC = Math.floor(c0 / step) * step;
41762
+ for (let r = firstR; r < r1; r += step) {
41763
+ const n0 = tile.North - (r / tile.ResolutionY) * (tile.North - tile.South);
41764
+ const n1 = tile.North
41765
+ - (Math.min(r + step, tile.ResolutionY) / tile.ResolutionY) * (tile.North - tile.South);
41766
+ for (let c = firstC; c < c1; c += step) {
41767
+ const w0 = tile.West + (c / tile.ResolutionX) * (tile.East - tile.West);
41768
+ const w1 = tile.West
41769
+ + (Math.min(c + step, tile.ResolutionX) / tile.ResolutionX) * (tile.East - tile.West);
41770
+ if (!this.coveredAt((w0 + w1) / 2, (n0 + n1) / 2)) {
41771
+ continue;
41772
+ }
41773
+ const tl = this.project(w0, n0);
41774
+ const tr = this.project(w1, n0);
41775
+ const br = this.project(w1, n1);
41776
+ const bl = this.project(w0, n1);
41777
+ if (!tl || !tr || !br || !bl) {
41778
+ continue;
41779
+ }
41780
+ path.moveTo(tl.x, tl.y);
41781
+ path.lineTo(tr.x, tr.y);
41782
+ path.lineTo(br.x, br.y);
41783
+ path.lineTo(bl.x, bl.y);
41784
+ path.closePath();
41785
+ drew = true;
41786
+ }
41787
+ }
41788
+ }
41789
+ const gridWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
41790
+ if (drewCoarse) {
41791
+ this.ctx.save();
41792
+ // Dimmer than a cell edge, so a tile boundary reads as "there is finer data here"
41793
+ // rather than as a cell in its own right.
41794
+ this.ctx.globalAlpha = TILE_OUTLINE_ALPHA;
41795
+ this.ctx.strokeStyle = color;
41796
+ this.ctx.lineWidth = gridWidth;
41797
+ this.ctx.stroke(coarse);
41798
+ this.ctx.restore();
41799
+ }
41800
+ if (!drew) {
41801
+ return;
41802
+ }
41803
+ this.ctx.strokeStyle = color;
41804
+ this.ctx.lineWidth = (_c = this.settings.gridWidthPixels) !== null && _c !== void 0 ? _c : DEFAULT_GRID_WIDTH_PIXELS;
41805
+ this.ctx.stroke(path);
41806
+ }
41807
+ /*
41808
+ * Lon/lat of the ground at the centre of the view, or null when the centre misses the globe.
41809
+ */
41810
+ groundAtScreenCentre() {
41811
+ const scene = this.viewer.scene;
41812
+ const centre = new Cesium.Cartesian2(this.viewer.canvas.clientWidth / 2, this.viewer.canvas.clientHeight / 2);
41813
+ const hit = scene.camera.pickEllipsoid(centre, Cesium.Ellipsoid.WGS84);
41814
+ if (!hit) {
41815
+ return null;
41816
+ }
41817
+ const carto = Cesium.Cartographic.fromCartesian(hit);
41818
+ return {
41819
+ lon: Cesium.Math.toDegrees(carto.longitude),
41820
+ lat: Cesium.Math.toDegrees(carto.latitude)
41821
+ };
41822
+ }
41823
+ /*
41824
+ * Whether the composited raster holds data at a lon/lat.
41825
+ */
41826
+ coveredAt(lon, lat) {
41827
+ if (!this.source) {
41828
+ return false;
41829
+ }
41830
+ const cols = this.source.width;
41831
+ const rows = this.source.height;
41832
+ const col = Math.floor((lon - this.extent.West) / (this.extent.East - this.extent.West) * cols);
41833
+ const row = Math.floor((this.extent.North - lat) / (this.extent.North - this.extent.South) * rows);
41834
+ if (col < 0 || row < 0 || col >= cols || row >= rows) {
41835
+ return false;
41836
+ }
41837
+ const at = (row * cols + col) * 4 + 3;
41838
+ // Painted, not merely covered: a dry cell holds a reading the ramp deliberately hides, and
41839
+ // ruling a grid over it draws detail where the picture shows none.
41840
+ if (this.painted && this.painted.length === cols * rows * 4) {
41841
+ return this.painted[at] > 0;
41842
+ }
41843
+ return this.source.data[at] >= 128;
41844
+ }
41845
+ isCovered(col, row, cols) {
41846
+ return Boolean(this.source && this.source.data[(row * cols + col) * 4 + 3] >= 128);
41847
+ }
41848
+ /*
41849
+ * Width of the widest label this overlay could draw, in pixels.
41850
+ */
41851
+ widestLabel(parts) {
41852
+ const sample = [];
41853
+ if (parts.indexOf("value") >= 0) {
41854
+ sample.push("-000.00");
41855
+ }
41856
+ const range = [];
41857
+ if (parts.indexOf("floor") >= 0) {
41858
+ range.push("min: -000.00");
41859
+ }
41860
+ if (parts.indexOf("ceiling") >= 0) {
41861
+ range.push("max: -000.00");
41862
+ }
41863
+ if (range.length) {
41864
+ sample.push(range.join(" - "));
41865
+ }
41866
+ if (!sample.length) {
41867
+ return 0;
41868
+ }
41869
+ const widest = Math.max(...sample.map((line) => this.ctx.measureText(line).width));
41870
+ return widest + LABEL_GAP_PIXELS;
41871
+ }
41872
+ /*
41873
+ * The four screen corners of one cell, or null if any of them will not project.
41874
+ */
41875
+ cellOutline(col, row, cols, rows) {
41876
+ // On a pyramid the composited texel is not a cell. Outlining it would draw a second,
41877
+ // uniform lattice over the tile grid, misaligned with it everywhere.
41878
+ if (this.tiles && this.tiles.length) {
41879
+ return this.tileCellOutline(this.lonAt(col, cols), this.latAt(row, rows));
41880
+ }
41881
+ const corners = [
41882
+ this.project(this.lonAt(col - 0.5, cols), this.latAt(row - 0.5, rows)),
41883
+ this.project(this.lonAt(col + 0.5, cols), this.latAt(row - 0.5, rows)),
41884
+ this.project(this.lonAt(col + 0.5, cols), this.latAt(row + 0.5, rows)),
41885
+ this.project(this.lonAt(col - 0.5, cols), this.latAt(row + 0.5, rows))
41886
+ ];
41887
+ return corners.every((c) => c) ? corners : null;
41888
+ }
41889
+ drawLabel(point, reading, parts, outline) {
41890
+ var _a;
41891
+ const dp = (_a = this.settings.decimals) !== null && _a !== void 0 ? _a : (Math.abs(reading.value) < 10 ? 2 : 1);
41892
+ const lines = [];
41893
+ if (parts.indexOf("value") >= 0) {
41894
+ lines.push(reading.value.toFixed(dp));
41895
+ }
41896
+ // Named rather than a bare pair: two numbers under a third say nothing about which is
41897
+ // the series low and which is its high.
41898
+ const range = [];
41899
+ if (parts.indexOf("floor") >= 0 && reading.floor !== undefined) {
41900
+ range.push(`min: ${reading.floor.toFixed(dp)}`);
41901
+ }
41902
+ if (parts.indexOf("ceiling") >= 0 && reading.ceiling !== undefined) {
41903
+ range.push(`max: ${reading.ceiling.toFixed(dp)}`);
41904
+ }
41905
+ if (range.length) {
41906
+ lines.push(range.join(" - "));
41907
+ }
41908
+ if (!lines.length) {
41909
+ return;
41910
+ }
41911
+ if (outline) {
41912
+ this.ctx.strokeStyle = LABEL_ANCHOR_COLOR;
41913
+ this.ctx.lineWidth = 1.5;
41914
+ this.ctx.beginPath();
41915
+ this.ctx.moveTo(outline[0].x, outline[0].y);
41916
+ for (let i = 1; i < outline.length; i++) {
41917
+ this.ctx.lineTo(outline[i].x, outline[i].y);
41918
+ }
41919
+ this.ctx.closePath();
41920
+ this.ctx.stroke();
41921
+ }
41922
+ const width = Math.max(...lines.map((l) => this.ctx.measureText(l).width)) + 10;
41923
+ const height = lines.length * LINE_HEIGHT + 6;
41924
+ const top = point.y - height / 2;
41925
+ this.ctx.fillStyle = "rgba(16,16,14,0.72)";
41926
+ this.ctx.beginPath();
41927
+ this.ctx.roundRect(point.x - width / 2, top, width, height, LABEL_RADIUS);
41928
+ this.ctx.fill();
41929
+ lines.forEach((line, i) => {
41930
+ this.ctx.fillStyle = i === 0 ? "#ffffff" : "#a8a89f";
41931
+ this.ctx.fillText(line, point.x, top + LINE_HEIGHT * (i + 1) - 1);
41932
+ });
41933
+ this.drawn++;
41934
+ }
41935
+ /*
41936
+ * The first cell with a reading at or near a lattice point, searched within its own block.
41937
+ */
41938
+ readNear(col, row, cols, rows, stride) {
41939
+ const direct = this.readAt(col, row, cols);
41940
+ if (direct && this.paintedAt(col, row, cols)) {
41941
+ return { col, row, reading: direct };
41942
+ }
41943
+ if (stride <= 1) {
41944
+ return null;
41945
+ }
41946
+ // Bounded, and stepped rather than exhaustive, so a large stride cannot make this the
41947
+ // expensive part of a frame.
41948
+ const step = Math.max(1, Math.floor(stride / MAX_LABEL_PROBES_PER_SIDE));
41949
+ for (let r = row; r < Math.min(rows, row + stride); r += step) {
41950
+ for (let c = col; c < Math.min(cols, col + stride); c += step) {
41951
+ const reading = this.readAt(c, r, cols);
41952
+ if (reading && this.paintedAt(c, r, cols)) {
41953
+ return { col: c, row: r, reading };
41954
+ }
41955
+ }
41956
+ }
41957
+ return null;
41958
+ }
41959
+ paintedAt(col, row, cols) {
41960
+ if (!this.painted) {
41961
+ return true;
41962
+ }
41963
+ const at = (row * cols + col) * 4 + 3;
41964
+ return this.painted[at] > 0;
41965
+ }
41966
+ // Decodes one cell, skipping anything the texture marks as having no data.
41967
+ readAt(col, row, cols) {
41968
+ var _a, _b;
41969
+ const i = (row * cols + col) * 4;
41970
+ if (!this.source || this.source.data[i + 3] < 128) {
41971
+ return null;
41972
+ }
41973
+ const lo = this.metadata && typeof this.metadata.ValueMin === "number" ? this.metadata.ValueMin : 0;
41974
+ const hi = this.metadata && typeof this.metadata.ValueMax === "number" ? this.metadata.ValueMax : 1;
41975
+ const value = lo + (this.source.data[i] / 255) * (hi - lo);
41976
+ const reading = { value };
41977
+ const eLo = (_a = this.metadata) === null || _a === void 0 ? void 0 : _a.ExtremesValueMin;
41978
+ const eHi = (_b = this.metadata) === null || _b === void 0 ? void 0 : _b.ExtremesValueMax;
41979
+ if (this.floor && typeof eLo === "number" && typeof eHi === "number") {
41980
+ reading.floor = eLo + decode16(this.floor.data, i) * (eHi - eLo);
41981
+ // An anomaly archive's frames are already floor-relative, so the reading a user
41982
+ // wants is the absolute one. This is the whole reason the floor is published.
41983
+ if (TextureFrameSeriesAnimator.IsAnomalyMeasure(this.metadata)) {
41984
+ reading.value += reading.floor;
41985
+ }
41986
+ }
41987
+ if (this.ceiling && typeof eLo === "number" && typeof eHi === "number") {
41988
+ reading.ceiling = eLo + decode16(this.ceiling.data, i) * (eHi - eLo);
41989
+ }
41990
+ return reading;
41991
+ }
41992
+ lonAt(col, cols) {
41993
+ return this.extent.West + ((col + 0.5) / cols) * (this.extent.East - this.extent.West);
41994
+ }
41995
+ latAt(row, rows) {
41996
+ return this.extent.North - ((row + 0.5) / rows) * (this.extent.North - this.extent.South);
41997
+ }
41998
+ // The texel range overlapping the camera's view, so an off-screen texture costs nothing.
41999
+ visibleTexels(cols, rows) {
42000
+ const view = this.viewer.camera.computeViewRectangle(Cesium.Ellipsoid.WGS84);
42001
+ if (!view) {
42002
+ return null;
42003
+ }
42004
+ const west = Math.max(this.extent.West, Cesium.Math.toDegrees(view.west));
42005
+ const east = Math.min(this.extent.East, Cesium.Math.toDegrees(view.east));
42006
+ const south = Math.max(this.extent.South, Cesium.Math.toDegrees(view.south));
42007
+ const north = Math.min(this.extent.North, Cesium.Math.toDegrees(view.north));
42008
+ if (west >= east || south >= north) {
42009
+ return null;
42010
+ }
42011
+ const spanLon = this.extent.East - this.extent.West;
42012
+ const spanLat = this.extent.North - this.extent.South;
42013
+ return {
42014
+ col0: Math.max(0, Math.floor((west - this.extent.West) / spanLon * cols) - 1),
42015
+ col1: Math.min(cols - 1, Math.ceil((east - this.extent.West) / spanLon * cols) + 1),
42016
+ row0: Math.max(0, Math.floor((this.extent.North - north) / spanLat * rows) - 1),
42017
+ row1: Math.min(rows - 1, Math.ceil((this.extent.North - south) / spanLat * rows) + 1)
42018
+ };
42019
+ }
42020
+ project(lon, lat) {
42021
+ const world = Cesium.Cartesian3.fromDegrees(lon, lat, 0);
42022
+ const transforms = Cesium.SceneTransforms;
42023
+ // Renamed in newer Cesium, and this library runs on whatever the host app supplies.
42024
+ const fn = transforms.worldToWindowCoordinates || transforms.wgs84ToWindowCoordinates;
42025
+ return fn ? fn.call(transforms, this.viewer.scene, world) : null;
42026
+ }
42027
+ }
42028
+ TextureValueLabels.Labels = Labels;
42029
+ function decode16(data, i) {
42030
+ return (data[i] * 256 + data[i + 1]) / 65535;
42031
+ }
42032
+ })(exports.TextureValueLabels || (exports.TextureValueLabels = {}));
42033
+
40657
42034
  (function (DisplacedSurfacePrimitive) {
40658
42035
  // Grid densities a tile can draw at, coarsest first.
40659
42036
  const TIERS = [32, 64, 128, 192];
@@ -40671,6 +42048,9 @@
40671
42048
  const COVERAGE_CUTOFF = 0.35;
40672
42049
  // With no exaggeration stated, the full value range is drawn as this fraction of the extent's shorter side.
40673
42050
  const AUTO_RELIEF_FRACTION = 0.02;
42051
+ // Ceiling on the derived relief. Sizing purely off the extent is fine for a parcel and absurd for
42052
+ // a whole estuary: 4 m of water over an 82 km extent came out 1.6 km tall.
42053
+ const AUTO_MAX_RELIEF_METRES = 250.0;
40674
42054
  // Terrain is sampled on this grid and interpolated between samples.
40675
42055
  // Per-texel sampling would be hundreds of thousands of lookups for a surface whose own data is far coarser than that.
40676
42056
  const GROUND_SAMPLES_PER_SIDE = 64;
@@ -40749,8 +42129,7 @@ void main() {
40749
42129
  `;
40750
42130
  const FRAGMENT_SHADER_GLSL300 = `
40751
42131
  uniform sampler2D u_valueTexture;
40752
- uniform vec4 u_lowColor;
40753
- uniform vec4 u_highColor;
42132
+ uniform sampler2D u_rampTexture;
40754
42133
  uniform float u_coverageCutoff;
40755
42134
  uniform vec2 u_texelSize;
40756
42135
  uniform float u_valueRange;
@@ -40765,7 +42144,13 @@ void main() {
40765
42144
  if (v_coverage < u_coverageCutoff) {
40766
42145
  discard;
40767
42146
  }
40768
- vec4 color = mix(u_lowColor, u_highColor, v_value);
42147
+ // The ramp is baked to a lookup rather than lerped between two colours, so a banded style with a
42148
+ // hidden floor reads the same here as it does on the flat drape.
42149
+ // Without it this surface painted water the style hides, and the cell grid then correctly refused to outline any of it.
42150
+ vec4 color = texture(u_rampTexture, vec2(clamp(v_value, 0.0, 1.0), 0.5));
42151
+ if (color.a <= 0.0) {
42152
+ discard;
42153
+ }
40769
42154
 
40770
42155
  // Relief shading from the value gradient, so the displacement reads at a distance without
40771
42156
  // recomputing vertex normals every time the texture changes.
@@ -40964,6 +42349,7 @@ void main() {
40964
42349
  this.show = true;
40965
42350
  this.valueSource = null;
40966
42351
  this.lastPresentVersion = -1;
42352
+ this.rampTexture = null;
40967
42353
  this.texture = null;
40968
42354
  this.textureDirty = true;
40969
42355
  this.shaderProgram = null;
@@ -40985,8 +42371,7 @@ void main() {
40985
42371
  this.baseHeight = (_d = options.baseHeight) !== null && _d !== void 0 ? _d : 0;
40986
42372
  this.pixelPlacement = options.pixelPlacement;
40987
42373
  this.tileSkirts = Boolean(options.tileSkirts);
40988
- this.lowColor = toCesiumColor(options.lowColor || DEFAULT_LOW_COLOR);
40989
- this.highColor = toCesiumColor(options.highColor || DEFAULT_HIGH_COLOR);
42374
+ this.rampPixels = RampLookup(options.rampStops || null, options.lowColor || DEFAULT_LOW_COLOR, options.highColor || DEFAULT_HIGH_COLOR);
40990
42375
  this.tiles = buildTiles(hasCoverage);
40991
42376
  }
40992
42377
  GetFollowsGround() {
@@ -41045,6 +42430,7 @@ void main() {
41045
42430
  return;
41046
42431
  }
41047
42432
  this.syncTexture(context);
42433
+ this.syncRampTexture(context);
41048
42434
  this.requestGround();
41049
42435
  this.syncGroundTexture(context);
41050
42436
  // A tile needs one mesh before it has a bounding sphere to measure against.
@@ -41083,6 +42469,10 @@ void main() {
41083
42469
  this.texture.destroy();
41084
42470
  this.texture = null;
41085
42471
  }
42472
+ if (this.rampTexture) {
42473
+ this.rampTexture.destroy();
42474
+ this.rampTexture = null;
42475
+ }
41086
42476
  if (this.groundTexture) {
41087
42477
  this.groundTexture.destroy();
41088
42478
  this.groundTexture = null;
@@ -41176,6 +42566,25 @@ void main() {
41176
42566
  const width = Cesium.Cartesian3.distance(southWest, southEast);
41177
42567
  return Math.max(1, width / Math.max(1, this.source ? this.source.width : 1));
41178
42568
  }
42569
+ syncRampTexture(context) {
42570
+ if (this.rampTexture) {
42571
+ return;
42572
+ }
42573
+ this.rampTexture = new Cesium.Texture({
42574
+ context,
42575
+ pixelFormat: Cesium.PixelFormat.RGBA,
42576
+ source: { width: this.rampPixels.length / 4, height: 1,
42577
+ arrayBufferView: this.rampPixels },
42578
+ flipY: false,
42579
+ sampler: new Cesium.Sampler({
42580
+ wrapS: Cesium.TextureWrap.CLAMP_TO_EDGE,
42581
+ wrapT: Cesium.TextureWrap.CLAMP_TO_EDGE,
42582
+ // Linear across the ramp, so a gradient between stops stays smooth.
42583
+ minificationFilter: Cesium.TextureMinificationFilter.LINEAR,
42584
+ magnificationFilter: Cesium.TextureMagnificationFilter.LINEAR
42585
+ })
42586
+ });
42587
+ }
41179
42588
  syncTexture(context) {
41180
42589
  const sizeChanged = this.texture
41181
42590
  && (this.texture.width !== this.source.width || this.texture.height !== this.source.height);
@@ -41306,8 +42715,7 @@ void main() {
41306
42715
  ? Math.max(2, Math.abs(self.valueMax - self.valueMin) * self.exaggeration * SKIRT_FRACTION)
41307
42716
  : 0,
41308
42717
  u_coverageCutoff: () => COVERAGE_CUTOFF,
41309
- u_lowColor: () => self.lowColor,
41310
- u_highColor: () => self.highColor,
42718
+ u_rampTexture: () => self.rampTexture,
41311
42719
  u_texelSize: () => new Cesium.Cartesian2(1 / self.source.width, 1 / self.source.height)
41312
42720
  }
41313
42721
  });
@@ -41368,12 +42776,10 @@ void main() {
41368
42776
  if (!(shorterSide > 0)) {
41369
42777
  return 1;
41370
42778
  }
41371
- return (shorterSide * AUTO_RELIEF_FRACTION) / range;
42779
+ const relief = Math.min(shorterSide * AUTO_RELIEF_FRACTION, AUTO_MAX_RELIEF_METRES);
42780
+ return relief / range;
41372
42781
  }
41373
42782
  DisplacedSurfacePrimitive.autoExaggeration = autoExaggeration;
41374
- function toCesiumColor(color) {
41375
- return new Cesium.Color(color.red / 255, color.green / 255, color.blue / 255, color.alpha);
41376
- }
41377
42783
  function now() {
41378
42784
  return typeof performance !== "undefined" ? performance.now() : Date.now();
41379
42785
  }
@@ -41381,10 +42787,11 @@ void main() {
41381
42787
 
41382
42788
  const TEXTURE_FRAME_SERIES_ANIMATOR_KEY = "TextureFrameSeriesAnimator.Animator";
41383
42789
  const EXTRUSION_ANIMATOR_KEY = "TextureFrameSeriesAnimator.ExtrusionAnimator";
42790
+ const TEXTURE_VALUE_LABELS_KEY = "TextureValueLabels.Overlay";
41384
42791
  const DISPLACED_SURFACE_KEY = "DisplacedSurfacePrimitive.Surface";
41385
42792
  (function (EntityRenderEnginePolygon) {
41386
42793
  async function Render(params) {
41387
- var _a, _b, _c, _d;
42794
+ var _a, _b, _c, _d, _e;
41388
42795
  const entity = params.entity;
41389
42796
  const style = params.style;
41390
42797
  const pRings = BModels.Entity.GetValue({
@@ -41435,18 +42842,22 @@ void main() {
41435
42842
  console.error(`Polygon.Render: failed to resolve extrusion texture for entity ${((_b = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _b === void 0 ? void 0 : _b.ID) || "<NONE>"}:`, e);
41436
42843
  }
41437
42844
  }
42845
+ const retained = params.offline
42846
+ ? (_c = params.rendered) === null || _c === void 0 ? void 0 : _c[TEXTURE_FRAME_SERIES_ANIMATOR_KEY]
42847
+ : null;
42848
+ const keepsAnimator = Boolean(retained && !retained.IsDisposed());
41438
42849
  const drawsDisplacedSurface = Boolean(extrusionArchive);
41439
42850
  // The displaced surface IS the entity's fill, drawn as its own primitive at the value's height.
41440
42851
  // Leaving the flat polygon filled as well shows a second copy of the parcel on the ground under it.
41441
42852
  const hasFill = (drawsDisplacedSurface ? true
41442
42853
  : fillType === BModels.Style.EPolygonFillType.Texture
41443
- ? Boolean(textureDataUri || frameArchive || cFillColor.alpha > 0)
42854
+ ? Boolean(textureDataUri || frameArchive || keepsAnimator || cFillColor.alpha > 0)
41444
42855
  : cFillColor.alpha > 0);
41445
42856
  const fillMaterial = drawsDisplacedSurface
41446
42857
  ? Cesium.Color.WHITE.withAlpha(0)
41447
42858
  : textureDataUri
41448
42859
  ? new Cesium.ImageMaterialProperty({ image: textureDataUri, transparent: true })
41449
- : frameArchive
42860
+ : (frameArchive || keepsAnimator)
41450
42861
  ? Cesium.Color.WHITE.withAlpha(0)
41451
42862
  : cFillColor;
41452
42863
  const lineColorTrace = style.lineColor ? BModels.Calculator.TraceGetColor(style.lineColor, entity, params.tags) : { value: null, effective: null };
@@ -41616,7 +43027,7 @@ void main() {
41616
43027
  }
41617
43028
  // Must run before any material is baked below: the Animator takes over polygon.material, so a bake
41618
43029
  // has to know whether it is baking over a live animation or over a disposed one's restored material.
41619
- const animator = syncTextureFrameArchive(cEntity, drawsDisplacedSurface ? null : frameArchive, params.viewer, style.textureColorMask, style.maskTextureBaseline);
43030
+ const animator = syncTextureFrameArchive(cEntity, drawsDisplacedSurface ? null : frameArchive, params.viewer, style.textureColorMask, style.maskTextureBaseline, undefined, undefined, wantsLabels(style), params.offline, polygonBbox(posses));
41620
43031
  syncDisplacedSurface({
41621
43032
  cEntity,
41622
43033
  extrusionArchive,
@@ -41624,7 +43035,30 @@ void main() {
41624
43035
  style,
41625
43036
  entity,
41626
43037
  heightRef,
41627
- outerRingPosses: posses
43038
+ outerRingPosses: posses,
43039
+ offline: params.offline
43040
+ });
43041
+ const drapeExtent = polygonBbox(posses);
43042
+ const labelArchive = drawsDisplacedSurface ? extrusionArchive : frameArchive;
43043
+ syncTextureValueLabels({
43044
+ cEntity,
43045
+ viewer: params.viewer,
43046
+ settings: overlaySettings(style, entity, params.tags),
43047
+ extent: labelArchive
43048
+ ? (drawsDisplacedSurface ? archiveExtent(labelArchive.metadata, posses) : drapeExtent)
43049
+ : null,
43050
+ animator: drawsDisplacedSurface
43051
+ ? cEntity[EXTRUSION_ANIMATOR_KEY]
43052
+ : animator,
43053
+ metadata: labelArchive ? labelArchive.metadata : undefined,
43054
+ offline: params.offline,
43055
+ clipRing: posses.map((pos) => {
43056
+ const carto = Cesium.Cartographic.fromCartesian(pos);
43057
+ return {
43058
+ lon: Cesium.Math.toDegrees(carto.longitude),
43059
+ lat: Cesium.Math.toDegrees(carto.latitude)
43060
+ };
43061
+ })
41628
43062
  });
41629
43063
  if (animator) {
41630
43064
  exports.CesiumEntityStyler.SetDefaultTextureImage({
@@ -41685,7 +43119,7 @@ void main() {
41685
43119
  }
41686
43120
  }
41687
43121
  let borderPosses = posses.map(x => x.clone ? x.clone() : { ...x });
41688
- let cEntityBorder = (_d = (_c = params.rendered) === null || _c === void 0 ? void 0 : _c._siblingGraphics) === null || _d === void 0 ? void 0 : _d[0];
43122
+ let cEntityBorder = (_e = (_d = params.rendered) === null || _d === void 0 ? void 0 : _d._siblingGraphics) === null || _e === void 0 ? void 0 : _e[0];
41689
43123
  cEntity._siblingGraphics = [];
41690
43124
  if (!cEntityBorder || ((!cEntityBorder.polyline && units == "px") ||
41691
43125
  (!cEntityBorder.corridor && units == "m"))) {
@@ -41936,6 +43370,11 @@ void main() {
41936
43370
  * Disposes a cEntity's TextureFrameSeriesAnimator.Animator (if any).
41937
43371
  */
41938
43372
  function DisposeTextureFrameSeriesAnimator(cEntity, viewer) {
43373
+ const labelOverlay = cEntity === null || cEntity === void 0 ? void 0 : cEntity[TEXTURE_VALUE_LABELS_KEY];
43374
+ if (labelOverlay) {
43375
+ labelOverlay.remove();
43376
+ cEntity[TEXTURE_VALUE_LABELS_KEY] = null;
43377
+ }
41939
43378
  const existing = cEntity === null || cEntity === void 0 ? void 0 : cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY];
41940
43379
  if (existing && !existing.IsDisposed()) {
41941
43380
  existing.Dispose();
@@ -42061,10 +43500,13 @@ void main() {
42061
43500
  * @param params
42062
43501
  */
42063
43502
  function syncDisplacedSurface(params) {
42064
- var _a;
42065
43503
  const { cEntity, extrusionArchive, viewer, style, entity, heightRef, outerRingPosses } = params;
42066
43504
  const existingAnimator = cEntity[EXTRUSION_ANIMATOR_KEY];
42067
43505
  const existingSurface = cEntity[DISPLACED_SURFACE_KEY];
43506
+ // Same reason as the fill animator: an unresolvable archive is not a removed one.
43507
+ if (params.offline && existingSurface && !existingSurface.isDestroyed()) {
43508
+ return;
43509
+ }
42068
43510
  // Nothing to draw and nothing left over, which is every polygon that does not use this feature.
42069
43511
  if (!extrusionArchive && !existingAnimator && !existingSurface) {
42070
43512
  return;
@@ -42080,7 +43522,7 @@ void main() {
42080
43522
  // to rebuild rather than update in place.
42081
43523
  if (reusable && existingSurface.GetFollowsGround() === placement.followGround) {
42082
43524
  existingSurface.SetBaseHeight(placement.baseHeight);
42083
- existingSurface.SetExaggeration((_a = style.extrusionExaggeration) !== null && _a !== void 0 ? _a : exports.DisplacedSurfacePrimitive.autoExaggeration(existingSurface.GetExtent(), extrusionArchive.metadata.ValueMin, extrusionArchive.metadata.ValueMax));
43525
+ existingSurface.SetExaggeration(exaggerationFor(style, extrusionArchive.metadata, existingSurface.GetExtent()));
42084
43526
  return;
42085
43527
  }
42086
43528
  disposeDisplacedSurface(cEntity, viewer);
@@ -42102,6 +43544,8 @@ void main() {
42102
43544
  textureColorMask: style.textureColorMask,
42103
43545
  driveMaterial: false,
42104
43546
  produceValueCanvas: true,
43547
+ metadata: extrusionArchive.metadata,
43548
+ cellTexels: 1,
42105
43549
  baselineMask: extrusionArchive.metadata.BaselineMask,
42106
43550
  // Off-type for the same reason as the fill path: the pinned bruce-models predates the field.
42107
43551
  maskBaseline: style.maskTextureBaseline
@@ -42118,9 +43562,15 @@ void main() {
42118
43562
  terrainProvider: viewer && viewer.terrainProvider,
42119
43563
  valueMin: extrusionArchive.metadata.ValueMin,
42120
43564
  valueMax: extrusionArchive.metadata.ValueMax,
42121
- exaggeration: style.extrusionExaggeration,
43565
+ exaggeration: exaggerationFor(style, extrusionArchive.metadata, extent),
42122
43566
  lowColor: mask ? BModels.Color.ColorFromStr(mask.minColor) : null,
42123
- highColor: mask ? BModels.Color.ColorFromStr(mask.maxColor) : null
43567
+ highColor: mask ? BModels.Color.ColorFromStr(mask.maxColor) : null,
43568
+ // Normalised the same way the animator normalises them, so the surface and the flat drape
43569
+ // agree about which values the style hides.
43570
+ rampStops: (mask && mask.points || []).map((stop) => ({
43571
+ position: TextureFrameSeriesAnimator.NormalisePosition(extrusionArchive.metadata, stop.position),
43572
+ color: BModels.Color.ColorFromStr(stop.color)
43573
+ })).filter((stop) => Boolean(stop.color))
42124
43574
  });
42125
43575
  surface.BindAnimator(animator);
42126
43576
  viewer.scene.primitives.add(surface);
@@ -42153,6 +43603,40 @@ void main() {
42153
43603
  * @param metadata
42154
43604
  * @param posses
42155
43605
  */
43606
+ /*
43607
+ * The factor a displaced surface should use, honouring an explicit style setting first.
43608
+ *
43609
+ * An elevation is a real height against a datum, so scaling it is not a display choice: it lifts the
43610
+ * surface off the datum it belongs to. Only a thickness is safe to exaggerate, which is why an
43611
+ * elevation archive defaults to 1 rather than to a derived factor.
43612
+ */
43613
+ function exaggerationFor(style, metadata, extent) {
43614
+ if (style.extrusionExaggeration != null) {
43615
+ return style.extrusionExaggeration;
43616
+ }
43617
+ if (metadata.Quantity === TextureFrameSeriesAnimator.QUANTITY_ELEVATION) {
43618
+ return 1;
43619
+ }
43620
+ if (!extent) {
43621
+ return undefined;
43622
+ }
43623
+ return exports.DisplacedSurfacePrimitive.autoExaggeration(extent, metadata.ValueMin, metadata.ValueMax);
43624
+ }
43625
+ /*
43626
+ * The lon/lat bounding rectangle of a ring, which is the frame Cesium drapes an image material in.
43627
+ */
43628
+ function polygonBbox(posses) {
43629
+ if (!posses || posses.length === 0) {
43630
+ return undefined;
43631
+ }
43632
+ const rectangle = Cesium.Rectangle.fromCartesianArray(posses);
43633
+ return {
43634
+ West: Cesium.Math.toDegrees(rectangle.west),
43635
+ East: Cesium.Math.toDegrees(rectangle.east),
43636
+ South: Cesium.Math.toDegrees(rectangle.south),
43637
+ North: Cesium.Math.toDegrees(rectangle.north)
43638
+ };
43639
+ }
42156
43640
  function archiveExtent(metadata, posses) {
42157
43641
  if (metadata.West != null && metadata.East != null && metadata.South != null && metadata.North != null) {
42158
43642
  return { West: metadata.West, East: metadata.East, South: metadata.South, North: metadata.North };
@@ -42203,9 +43687,22 @@ void main() {
42203
43687
  * @param viewer
42204
43688
  * @param textureColorMask
42205
43689
  */
42206
- function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask, maskTextureBaseline) {
43690
+ function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask, maskTextureBaseline, cellBorder, cellTexels, produceValueCanvas, offline, drapeExtent) {
42207
43691
  const existing = cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY];
42208
- if (frameArchive && existing && !existing.IsDisposed() && existing.GetArchiveUrl() === frameArchive.url) {
43692
+ // Offline says nothing about whether the archive changed, only that it could not be asked.
43693
+ if (offline && existing && !existing.IsDisposed()) {
43694
+ return existing;
43695
+ }
43696
+ // Appearance is part of the reuse test, not just the archive URL. Recolouring a texture-driven
43697
+ // polygon keeps the same archive, so matching on the URL alone kept the old Animator and the
43698
+ // style edit appeared to do nothing at all.
43699
+ const appearance = TextureFrameSeriesAnimator.AppearanceSignature({
43700
+ textureColorMask, cellBorder, cellTexels, maskBaseline: maskTextureBaseline,
43701
+ valueCanvas: produceValueCanvas, drape: drapeExtent
43702
+ });
43703
+ if (frameArchive && existing && !existing.IsDisposed()
43704
+ && existing.GetArchiveUrl() === frameArchive.url
43705
+ && existing.GetAppearanceSignature() === appearance) {
42209
43706
  return existing;
42210
43707
  }
42211
43708
  if (existing && !existing.IsDisposed()) {
@@ -42221,12 +43718,118 @@ void main() {
42221
43718
  archiveUrl: frameArchive.url,
42222
43719
  frames: frameArchive.metadata.Frames,
42223
43720
  textureColorMask,
43721
+ metadata: frameArchive.metadata,
43722
+ cellBorder,
43723
+ cellTexels,
42224
43724
  baselineMask: frameArchive.metadata.BaselineMask,
42225
- maskBaseline: maskTextureBaseline
43725
+ maskBaseline: maskTextureBaseline,
43726
+ produceValueCanvas,
43727
+ drapeExtent
42226
43728
  });
42227
43729
  cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY] = animator;
42228
43730
  return animator;
42229
43731
  }
43732
+ /**
43733
+ * Folds the cell-border and value-label style into the single overlay that draws both.
43734
+ *
43735
+ * Returns undefined when neither is asked for, which is what keeps the overlay off entirely.
43736
+ */
43737
+ function wantsLabels(style) {
43738
+ const texture = style.texture;
43739
+ return Boolean(texture && !Array.isArray(texture) && texture.label);
43740
+ }
43741
+ /*
43742
+ * How the cell grid and its values are drawn, which the style does not get a say in.
43743
+ *
43744
+ * A white hairline reads over every ramp colour without competing with it, and the spacing and cap
43745
+ * are what a parcel-sized archive needs to stay readable while the camera moves. Exposing them
43746
+ * bought nothing except thresholds an author could get wrong.
43747
+ */
43748
+ const LABEL_GRID_COLOR = "rgba(255,255,255,0.55)";
43749
+ const LABEL_GRID_WIDTH_PIXELS = 1;
43750
+ const LABEL_MIN_SPACING_PIXELS = 78;
43751
+ const LABEL_MAX_LABELS = 240;
43752
+ function overlaySettings(style, entity, tags) {
43753
+ if (!wantsLabels(style)) {
43754
+ return undefined;
43755
+ }
43756
+ return {
43757
+ gridColor: LABEL_GRID_COLOR,
43758
+ gridWidthPixels: LABEL_GRID_WIDTH_PIXELS,
43759
+ minSpacingPixels: LABEL_MIN_SPACING_PIXELS,
43760
+ maxLabels: LABEL_MAX_LABELS
43761
+ };
43762
+ }
43763
+ /**
43764
+ * Keeps a value-label overlay in step with whichever animator is driving the polygon.
43765
+ *
43766
+ * Redrawn on preRender rather than on frame changes: labels are screen-space, so panning and zooming
43767
+ * move them even when the data has not advanced at all.
43768
+ */
43769
+ function syncTextureValueLabels(params) {
43770
+ const { cEntity, viewer, settings, extent, animator, metadata } = params;
43771
+ const existing = cEntity[TEXTURE_VALUE_LABELS_KEY];
43772
+ // Offline has no extent to rebuild the overlay from, so an existing one is left running.
43773
+ if (params.offline && existing) {
43774
+ return;
43775
+ }
43776
+ if (existing) {
43777
+ existing.remove();
43778
+ cEntity[TEXTURE_VALUE_LABELS_KEY] = null;
43779
+ }
43780
+ if (!settings || !extent || !animator) {
43781
+ return;
43782
+ }
43783
+ const labels = new exports.TextureValueLabels.Labels({
43784
+ viewer, extent, settings, metadata,
43785
+ tiles: TextureFrameSeriesAnimator.IsTiledLayout(metadata)
43786
+ ? metadata.Tiles.map((tile) => ({
43787
+ West: tile.West, East: tile.East, South: tile.South, North: tile.North,
43788
+ ResolutionX: tile.ResolutionX, ResolutionY: tile.ResolutionY
43789
+ }))
43790
+ : undefined,
43791
+ clipRing: params.clipRing
43792
+ });
43793
+ let lastVersion = -1;
43794
+ let appliedFloor = null;
43795
+ let appliedCeiling = null;
43796
+ // The extremes are what turn an anomaly frame back into a reading, so they are worth two extra
43797
+ // Range GETs here even though nothing else in the render path wants them.
43798
+ animator.EnsureExtremes();
43799
+ const remove = viewer.scene.preRender.addEventListener(() => {
43800
+ if (animator.IsDisposed()) {
43801
+ return;
43802
+ }
43803
+ const canvas = animator.GetValueCanvas();
43804
+ if (!canvas) {
43805
+ return;
43806
+ }
43807
+ const version = animator.GetPresentVersion();
43808
+ if (version !== lastVersion) {
43809
+ lastVersion = version;
43810
+ const ctx = canvas.getContext("2d");
43811
+ labels.SetSource(ctx.getImageData(0, 0, canvas.width, canvas.height));
43812
+ labels.SetPainted(animator.GetDisplayedPixels());
43813
+ }
43814
+ // Tracked by reference rather than latched on the first one to arrive: a tiled archive
43815
+ // composites the floor and the ceiling separately, so latching once would leave every label
43816
+ // showing a minimum and no maximum.
43817
+ const extremes = animator.GetExtremes();
43818
+ if (extremes.floor !== appliedFloor || extremes.ceiling !== appliedCeiling) {
43819
+ appliedFloor = extremes.floor;
43820
+ appliedCeiling = extremes.ceiling;
43821
+ labels.SetExtremes(appliedFloor, appliedCeiling);
43822
+ }
43823
+ labels.Render();
43824
+ });
43825
+ cEntity[TEXTURE_VALUE_LABELS_KEY] = {
43826
+ labels,
43827
+ remove: () => {
43828
+ remove();
43829
+ labels.Dispose();
43830
+ }
43831
+ };
43832
+ }
42230
43833
  /**
42231
43834
  * Derives a min/max/label time-range segment from a resolved frame archive's Frames metadata,
42232
43835
  * for reporting up to whoever owns the menu item this entity belongs to (see IParams.onSeriesDiscovered).
@@ -43819,7 +45422,7 @@ void main() {
43819
45422
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
43820
45423
  })(exports.StyleUtils || (exports.StyleUtils = {}));
43821
45424
 
43822
- const VERSION = "7.2.1";
45425
+ const VERSION = "7.2.2";
43823
45426
  /**
43824
45427
  * Updates the environment instance used by bruce-cesium to one specified.
43825
45428
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.