bruce-cesium 7.2.2 → 7.2.3

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.
@@ -40286,40 +40286,222 @@
40286
40286
  });
40287
40287
  }
40288
40288
 
40289
- var TextureFrameSeriesAnimator;
40290
40289
  (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
40290
  TextureFrameSeriesAnimator.LAYOUT_TILES = "tiles";
40291
+ // The only generation this renderer draws. Anything else is refused rather than half read: the
40292
+ // shapes differ enough that a partial reading produces a plausible wrong surface, which is worse
40293
+ // than an empty one.
40294
+ TextureFrameSeriesAnimator.SUPPORTED_VERSION = 3;
40295
+ // Value in R at 8 bits, or high in R and low in G at 16.
40296
+ TextureFrameSeriesAnimator.FORMAT_U8 = "u8";
40297
+ TextureFrameSeriesAnimator.FORMAT_RG16 = "rg16";
40298
+ // Declared as a field with one value, so a second mode can be added later without an absent
40299
+ // Encoding having to mean anything.
40300
+ TextureFrameSeriesAnimator.ENCODING_ABSOLUTE = "absolute";
40301
+ TextureFrameSeriesAnimator.LAYER_VALUE = "Value";
40302
+ TextureFrameSeriesAnimator.LAYER_VALUE_MIN = "ValueMin";
40303
+ TextureFrameSeriesAnimator.LAYER_VALUE_MAX = "ValueMax";
40304
+ TextureFrameSeriesAnimator.LAYER_BASELINE = "Baseline";
40305
+ // The per-texel correction that was applied, published only on an archive that was transformed.
40306
+ TextureFrameSeriesAnimator.LAYER_VALUE_SHIFT = "ValueShift";
40296
40307
  /**
40297
- * The generator that produced an archive, for telling a stale one from a current one.
40308
+ * Turns a published value map into the flat shape this renderer works in, or null when it cannot be drawn.
40309
+ * @param published the archive's Data.generation
40310
+ */
40311
+ function Adapt(published) {
40312
+ if (!published || published.Version !== TextureFrameSeriesAnimator.SUPPORTED_VERSION) {
40313
+ const found = published && published.Version != null ? published.Version : "none";
40314
+ const at = published && published.Identity ? published.Identity.Generated : undefined;
40315
+ // In theory should be no files.
40316
+ console.warn("bruce-cesium: value map version " + found + " is not supported (this build reads "
40317
+ + TextureFrameSeriesAnimator.SUPPORTED_VERSION + "), so nothing will be drawn for it."
40318
+ + (at ? " The archive was generated at " + at + " and needs regenerating." : ""));
40319
+ return null;
40320
+ }
40321
+ const geometry = published.Geometry || {};
40322
+ const tiles = published.Tiles || [];
40323
+ if (geometry.Layout !== TextureFrameSeriesAnimator.LAYOUT_TILES || tiles.length === 0) {
40324
+ const at = published.Identity ? published.Identity.Generated : undefined;
40325
+ console.warn("bruce-cesium: value map declares no tiles, so nothing will be drawn for it."
40326
+ + (at ? " The archive was generated at " + at + " and needs regenerating." : ""));
40327
+ return null;
40328
+ }
40329
+ const value = published.Value || {};
40330
+ if (value.Encoding && value.Encoding !== TextureFrameSeriesAnimator.ENCODING_ABSOLUTE) {
40331
+ const at = published.Identity ? published.Identity.Generated : undefined;
40332
+ console.warn("bruce-cesium: value map encoding " + value.Encoding + " is not supported"
40333
+ + " (this build reads " + TextureFrameSeriesAnimator.ENCODING_ABSOLUTE + "), so nothing will be drawn for it."
40334
+ + (at ? " The archive was generated at " + at + " and needs regenerating." : ""));
40335
+ return null;
40336
+ }
40337
+ const series = published.Series || {};
40338
+ const layerRange = declaredRange(published, TextureFrameSeriesAnimator.LAYER_VALUE_MIN);
40339
+ const flat = {
40340
+ Frames: [],
40341
+ ValueMin: value.Min,
40342
+ ValueMax: value.Max,
40343
+ Format: value.Format || TextureFrameSeriesAnimator.FORMAT_U8,
40344
+ Encoding: value.Encoding || TextureFrameSeriesAnimator.ENCODING_ABSOLUTE,
40345
+ Units: value.Units,
40346
+ West: geometry.West,
40347
+ East: geometry.East,
40348
+ South: geometry.South,
40349
+ North: geometry.North,
40350
+ ResolutionX: geometry.ResolutionX,
40351
+ ResolutionY: geometry.ResolutionY,
40352
+ PixelPlacement: geometry.PixelPlacement,
40353
+ Times: series.Times,
40354
+ ExtremesValueMin: layerRange ? layerRange.min : undefined,
40355
+ ExtremesValueMax: layerRange ? layerRange.max : undefined,
40356
+ // The delivered values are what gets drawn. What the source called them, and any shift
40357
+ // applied on the way, travel for labels and analysis rather than for placement.
40358
+ SourceVerticalDatum: published.ValueSource ? published.ValueSource.VerticalDatum : undefined,
40359
+ Transform: published.ValueSource ? published.ValueSource.Transform : undefined,
40360
+ SourceUnits: published.ValueSource ? published.ValueSource.Units : undefined,
40361
+ SourceValueMin: published.ValueSource ? published.ValueSource.Min : undefined,
40362
+ SourceValueMax: published.ValueSource ? published.ValueSource.Max : undefined
40363
+ };
40364
+ flat.Tiles = tiles.map((tile) => adaptTile(tile, series.Times));
40365
+ // The coarsest tile covers the whole extent, so it is what a caller wanting one time list reads.
40366
+ flat.Frames = flat.Tiles[0].Frames;
40367
+ warnIfUnshifted(published, flat);
40368
+ return flat;
40369
+ }
40370
+ TextureFrameSeriesAnimator.Adapt = Adapt;
40371
+ /*
40372
+ * Says so when an archive's heights are not on the ellipsoid they will be drawn against.
40373
+ */
40374
+ function warnIfUnshifted(published, flat) {
40375
+ const datum = flat.SourceVerticalDatum;
40376
+ if (!datum || datum === TextureFrameSeriesAnimator.DATUM_ELLIPSOIDAL || flat.Transform) {
40377
+ return;
40378
+ }
40379
+ const at = published.Identity ? published.Identity.Generated : undefined;
40380
+ console.warn("bruce-cesium: value map heights are in " + datum + " and report no transform, so"
40381
+ + " they are not ellipsoidal and will draw at the wrong height when placed absolutely."
40382
+ + (at ? " The archive was generated at " + at + " and needs regenerating with the shift"
40383
+ + " applied." : ""));
40384
+ }
40385
+ /*
40386
+ * The Min and Max a named layer declares, since a statistics layer is encoded over the true data
40387
+ * range rather than over the frames' own.
40388
+ */
40389
+ function declaredRange(published, name) {
40390
+ const layers = published.Layers || [];
40391
+ for (const layer of layers) {
40392
+ if (layer.Name === name && typeof layer.Min === "number" && typeof layer.Max === "number") {
40393
+ return { min: layer.Min, max: layer.Max };
40394
+ }
40395
+ }
40396
+ return null;
40397
+ }
40398
+ /*
40399
+ * One tile, with its per-time value rasters lined up against the shared time list.
40400
+ */
40401
+ function adaptTile(tile, times) {
40402
+ const layers = tile.Layers || {};
40403
+ const perTime = layers[TextureFrameSeriesAnimator.LAYER_VALUE] || [];
40404
+ const frames = perTime.map((entry, index) => ({
40405
+ Timestamp: times && times[index] ? times[index] : "",
40406
+ ByteOffset: entry.Bytes.Offset,
40407
+ ByteLength: entry.Bytes.Length
40408
+ }));
40409
+ return {
40410
+ Level: tile.Level,
40411
+ X: tile.X,
40412
+ Y: tile.Y,
40413
+ West: tile.West,
40414
+ East: tile.East,
40415
+ South: tile.South,
40416
+ North: tile.North,
40417
+ TexelMetres: tile.TexelMetres,
40418
+ ResolutionX: tile.ResolutionX,
40419
+ ResolutionY: tile.ResolutionY,
40420
+ Frames: frames,
40421
+ Floor: single(layers[TextureFrameSeriesAnimator.LAYER_VALUE_MIN]),
40422
+ Ceiling: single(layers[TextureFrameSeriesAnimator.LAYER_VALUE_MAX]),
40423
+ BaselineMask: single(layers[TextureFrameSeriesAnimator.LAYER_BASELINE])
40424
+ };
40425
+ }
40426
+ function single(layer) {
40427
+ if (!layer || !layer.Bytes) {
40428
+ return undefined;
40429
+ }
40430
+ return { ByteOffset: layer.Bytes.Offset, ByteLength: layer.Bytes.Length };
40431
+ }
40432
+ /**
40433
+ * How to turn a delivered value into the number the source published, or null when the archive
40434
+ * does not say enough to do it.
40435
+ * @param metadata the archive's Data.generation
40436
+ */
40437
+ function SourceMap(metadata) {
40438
+ if (!metadata) {
40439
+ return null;
40440
+ }
40441
+ const { ValueMin, ValueMax, SourceValueMin, SourceValueMax } = metadata;
40442
+ if (typeof ValueMin !== "number" || typeof ValueMax !== "number"
40443
+ || typeof SourceValueMin !== "number" || typeof SourceValueMax !== "number") {
40444
+ return null;
40445
+ }
40446
+ const delivered = ValueMax - ValueMin;
40447
+ // A single delivered value describes no line, so all that can be recovered is where it sits.
40448
+ const scale = delivered !== 0 ? (SourceValueMax - SourceValueMin) / delivered : 1;
40449
+ return { scale, offset: SourceValueMin - ValueMin * scale };
40450
+ }
40451
+ TextureFrameSeriesAnimator.SourceMap = SourceMap;
40452
+ /**
40453
+ * A delivered value in the source's own terms, or unchanged when it cannot be turned back.
40454
+ * @param map from SourceMap(), null when the archive does not say enough
40455
+ */
40456
+ function ToSource(map, value) {
40457
+ return map ? value * map.scale + map.offset : value;
40458
+ }
40459
+ TextureFrameSeriesAnimator.ToSource = ToSource;
40460
+ /**
40461
+ * Whether the archive's frames carry a 16 bit value packed across R and G.
40298
40462
  *
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.
40463
+ * Every consumer that reads a texel has to ask: taking R alone from a packed frame yields a
40464
+ * plausible surface quantised to 255 steps of the full range rather than an obvious failure.
40301
40465
  * @param metadata the archive's Data.generation
40302
40466
  */
40303
- function GeneratorVersion(metadata) {
40304
- if (!metadata || typeof metadata.GeneratorVersion !== "number") {
40305
- return 1;
40467
+ function IsPackedValue(metadata) {
40468
+ return Boolean(metadata && metadata.Format === TextureFrameSeriesAnimator.FORMAT_RG16);
40469
+ }
40470
+ TextureFrameSeriesAnimator.IsPackedValue = IsPackedValue;
40471
+ /**
40472
+ * The normalised 0 to 1 value at a texel, from whichever format the archive packed it in.
40473
+ * @param metadata the archive's Data.generation
40474
+ * @param pixels untinted RGBA value pixels
40475
+ * @param at index of the texel's red channel
40476
+ */
40477
+ function NormalisedAt(metadata, pixels, at) {
40478
+ if (IsPackedValue(metadata)) {
40479
+ return (pixels[at] * 256 + pixels[at + 1]) / 65535;
40306
40480
  }
40307
- return metadata.GeneratorVersion;
40481
+ return pixels[at] / 255;
40308
40482
  }
40309
- TextureFrameSeriesAnimator.GeneratorVersion = GeneratorVersion;
40483
+ TextureFrameSeriesAnimator.NormalisedAt = NormalisedAt;
40484
+ // Values already sit on the ellipsoid Cesium measures against, so nothing is added to place them.
40485
+ TextureFrameSeriesAnimator.DATUM_ELLIPSOIDAL = "WGS84";
40310
40486
  /**
40311
- * Whether an archive is an adaptive pyramid rather than one raster.
40487
+ * Whether the values are heights against a vertical datum rather than a thickness.
40488
+ *
40489
+ * A thickness rises from wherever the polygon sits and a height already knows where it belongs, so
40490
+ * this is what decides whether the polygon's own altitude may be added underneath it.
40312
40491
  * @param metadata the archive's Data.generation
40313
40492
  */
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";
40493
+ function IsDatumHeight(metadata) {
40494
+ return Boolean(metadata && metadata.SourceVerticalDatum);
40495
+ }
40496
+ TextureFrameSeriesAnimator.IsDatumHeight = IsDatumHeight;
40497
+ /**
40498
+ * The archive's tiles, which Adapt() guarantees are present.
40499
+ * @param metadata the archive's Data.generation
40500
+ */
40501
+ function TilesOf(metadata) {
40502
+ return (metadata && metadata.Tiles) || [];
40503
+ }
40504
+ TextureFrameSeriesAnimator.TilesOf = TilesOf;
40323
40505
  /**
40324
40506
  * Turns a value in the attribute's own units into the 0 to 1 position the ramp is indexed by.
40325
40507
  *
@@ -40338,44 +40520,18 @@
40338
40520
  return Math.min(1, Math.max(0, (value - lo) / span));
40339
40521
  }
40340
40522
  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;
40372
40523
  /**
40373
40524
  * Detects whether a ClientFile's `Data.generation` metadata describes a frame archive rather than a single static image,
40374
40525
  * so a caller can decide whether to construct an Animator or fall back to the existing static-texture path.
40375
- * @param data ClientFile.Data (the `IFile.Data` field), as returned by ClientFile.Get().
40526
+ * @param data ClientFile.Data
40376
40527
  */
40377
40528
  function IsFrameArchiveMetadata(data) {
40378
- return Boolean(data && data.generation && Array.isArray(data.generation.Frames) && data.generation.Frames.length > 0);
40529
+ const generation = data && data.generation;
40530
+ if (!generation) {
40531
+ return false;
40532
+ }
40533
+ return (typeof generation.Version === "number" ||
40534
+ (Array.isArray(generation.Frames) && generation.Frames.length > 0));
40379
40535
  }
40380
40536
  TextureFrameSeriesAnimator.IsFrameArchiveMetadata = IsFrameArchiveMetadata;
40381
40537
  // First resolvable static number out of a calculator field list, since that is all a border needs.
@@ -40421,10 +40577,27 @@
40421
40577
  TextureFrameSeriesAnimator.AppearanceSignature = AppearanceSignature;
40422
40578
  const MAX_COMPOSITE_TEXELS = 2048;
40423
40579
  const VALUE_DILATE_PASSES = 2;
40580
+ /*
40581
+ * Rewrites a packed 16 bit value as the 8 bit grey the colour ramps index on.
40582
+ *
40583
+ * All three channels, since a ramp is free to read any of them and a leftover low byte in G would
40584
+ * show up as a green cast on the one that reads them all.
40585
+ */
40586
+ function flattenPacked(pixels, packed) {
40587
+ if (!packed) {
40588
+ return;
40589
+ }
40590
+ for (let i = 0; i < pixels.length; i += 4) {
40591
+ const grey = Math.round((pixels[i] * 256 + pixels[i + 1]) / 65535 * 255);
40592
+ pixels[i] = grey;
40593
+ pixels[i + 1] = grey;
40594
+ pixels[i + 2] = grey;
40595
+ }
40596
+ }
40424
40597
  /*
40425
40598
  * Bleeds covered values outward into uncovered texels, leaving alpha untouched.
40426
40599
  */
40427
- function dilateValues(value, width, height, passes) {
40600
+ function dilateValues(value, width, height, passes, packed) {
40428
40601
  const texels = width * height;
40429
40602
  const filled = new Uint8Array(texels);
40430
40603
  for (let p = 0; p < texels; p++) {
@@ -40448,15 +40621,25 @@
40448
40621
  if (nx < 0 || ny < 0 || nx >= width || ny >= height || !wasFilled[ny * width + nx]) {
40449
40622
  continue;
40450
40623
  }
40451
- sum += source[(ny * width + nx) * 4];
40624
+ const at = (ny * width + nx) * 4;
40625
+ // Averaged as a value rather than per byte: averaging a packed low byte
40626
+ // on its own wraps at every 256th step and speckles the bled edge.
40627
+ sum += packed ? source[at] * 256 + source[at + 1] : source[at];
40452
40628
  hits++;
40453
40629
  }
40454
40630
  }
40455
40631
  if (hits > 0) {
40456
40632
  const v = Math.round(sum / hits);
40457
- value[p * 4] = v;
40458
- value[p * 4 + 1] = v;
40459
- value[p * 4 + 2] = v;
40633
+ if (packed) {
40634
+ value[p * 4] = (v >> 8) & 255;
40635
+ value[p * 4 + 1] = v & 255;
40636
+ value[p * 4 + 2] = 0;
40637
+ }
40638
+ else {
40639
+ value[p * 4] = v;
40640
+ value[p * 4 + 1] = v;
40641
+ value[p * 4 + 2] = v;
40642
+ }
40460
40643
  filled[p] = 1;
40461
40644
  }
40462
40645
  }
@@ -40494,7 +40677,6 @@
40494
40677
  // forcing Cesium to re-upload the texture only when something changed.
40495
40678
  this.pool = [document.createElement("canvas"), document.createElement("canvas")];
40496
40679
  this.poolIdx = 0;
40497
- this.scratch = null;
40498
40680
  this.valueDims = null;
40499
40681
  this.presentVersion = 0;
40500
40682
  this.extremesLoad = null;
@@ -40517,7 +40699,7 @@
40517
40699
  this.highColor = (mask && BModels.Color.ColorFromStr(mask.maxColor)) || DEFAULT_HIGH_COLOR;
40518
40700
  // Positions are authored in the attribute's units and normalised once here, so the
40519
40701
  // 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;
40702
+ const points = mask && mask.points;
40521
40703
  this.rampStops = (points && points.length > 0)
40522
40704
  ? points.map((p) => ({
40523
40705
  position: NormalisePosition(options.metadata, p.position),
@@ -40543,9 +40725,8 @@
40543
40725
  this.produceValueCanvas = Boolean(options.produceValueCanvas);
40544
40726
  this.metadata = options.metadata;
40545
40727
  this.drapeExtent = options.drapeExtent;
40546
- this.tiles = IsTiledLayout(options.metadata) ? options.metadata.Tiles : undefined;
40547
- this.baselineMaskEntry = options.baselineMask || null;
40548
- this.maskBaseline = (_c = options.maskBaseline) !== null && _c !== void 0 ? _c : Boolean(options.baselineMask);
40728
+ this.tiles = TilesOf(options.metadata);
40729
+ this.maskBaseline = (_c = options.maskBaseline) !== null && _c !== void 0 ? _c : this.tiles.some((tile) => Boolean(tile.BaselineMask));
40549
40730
  this.valueCanvas = this.produceValueCanvas ? document.createElement("canvas") : null;
40550
40731
  this.driveMaterial = options.driveMaterial !== false;
40551
40732
  this.originalMaterial = options.entity.polygon.material;
@@ -40721,32 +40902,20 @@
40721
40902
  }
40722
40903
  async fetchAndTint(idx) {
40723
40904
  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);
40905
+ // Every tile's frame for one timestep sits contiguously, because the generator writes
40906
+ // frame major. So a tiled frame is still ONE range request, not one per tile.
40907
+ let lo = Number.MAX_SAFE_INTEGER;
40908
+ let hi = 0;
40909
+ for (const tile of tiles) {
40910
+ const at = tile.Frames[idx];
40911
+ if (!at) {
40912
+ continue;
40738
40913
  }
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();
40914
+ lo = Math.min(lo, at.ByteOffset);
40915
+ hi = Math.max(hi, at.ByteOffset + at.ByteLength);
40749
40916
  }
40917
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
40918
+ const buffer = await response.arrayBuffer();
40750
40919
  if (this.disposed) {
40751
40920
  return;
40752
40921
  }
@@ -40756,9 +40925,7 @@
40756
40925
  if (this.disposed) {
40757
40926
  return;
40758
40927
  }
40759
- const decoded = tiles
40760
- ? await this.composeTiles(tiles, idx, buffer, span)
40761
- : await this.decodeAndTint(buffer);
40928
+ const decoded = await this.composeTiles(tiles, idx, buffer, lo);
40762
40929
  if (this.disposed) {
40763
40930
  return;
40764
40931
  }
@@ -40769,26 +40936,6 @@
40769
40936
  this.beginCrossfadeTo(idx);
40770
40937
  }
40771
40938
  }
40772
- /**
40773
- * Decodes one frame's raw PNG bytes (as returned by a Range GET) and applies the grayscale color mask.
40774
- */
40775
- async decodeAndTint(buffer) {
40776
- const blob = new Blob([buffer], { type: "image/png" });
40777
- const objectUrl = URL.createObjectURL(blob);
40778
- let image;
40779
- try {
40780
- image = await loadImage(objectUrl);
40781
- }
40782
- finally {
40783
- URL.revokeObjectURL(objectUrl);
40784
- }
40785
- const canvas = document.createElement("canvas");
40786
- canvas.width = image.width;
40787
- canvas.height = image.height;
40788
- const ctx = canvas.getContext("2d");
40789
- ctx.drawImage(image, 0, 0);
40790
- return this.tint(ctx.getImageData(0, 0, canvas.width, canvas.height));
40791
- }
40792
40939
  /*
40793
40940
  * Draws every tile of one frame into a single raster covering the drape extent.
40794
40941
  */
@@ -40861,6 +41008,10 @@
40861
41008
  // Copied before tinting: the ramp is a lerp between two colours, so the value cannot be
40862
41009
  // recovered from the tinted pixels afterwards.
40863
41010
  const valuePixels = this.produceValueCanvas ? new Uint8ClampedArray(imageData.data) : undefined;
41011
+ // The ramp indexes on R as an 8 bit grey, so a packed frame is flattened to one here
41012
+ // rather than teaching every ramp function a second format.
41013
+ // The copy above keeps the packed pair, which is what displacement and labels read.
41014
+ flattenPacked(imageData.data, IsPackedValue(this.metadata));
40864
41015
  // Stops win when supplied: they can express a band and a hidden floor, which a two
40865
41016
  // colour ramp cannot. The pair stays the fallback so an older style still draws.
40866
41017
  if (this.rampStops && this.rampStops.length > 0) {
@@ -40874,72 +41025,20 @@
40874
41025
  }
40875
41026
  this.applyBaselineMask(imageData.data, valuePixels, canvas.width, canvas.height);
40876
41027
  if (valuePixels) {
40877
- dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES);
41028
+ dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES, IsPackedValue(this.metadata));
40878
41029
  }
40879
41030
  return { pixels: imageData.data, width: canvas.width, height: canvas.height, valuePixels };
40880
41031
  }
40881
41032
  /*
40882
- * Paints one frame's pixels onto a canvas, resampled into the drape extent when there is one.
41033
+ * Paints one frame's composited pixels onto a canvas.
40883
41034
  */
40884
41035
  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
41036
  canvas.width = dims.width;
40905
41037
  canvas.height = dims.height;
40906
41038
  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 };
41039
+ const image = ctx.createImageData(dims.width, dims.height);
41040
+ image.data.set(pixels);
41041
+ ctx.putImageData(image, 0, 0);
40943
41042
  }
40944
41043
  /**
40945
41044
  * The archive's per-cell floor and ceiling rasters, once EnsureExtremes has resolved.
@@ -40951,45 +41050,16 @@
40951
41050
  * Fetches and decodes the floor and ceiling rasters, at most once.
40952
41051
  */
40953
41052
  EnsureExtremes() {
40954
- if (this.extremesLoad) {
40955
- return this.extremesLoad;
40956
- }
40957
- if (this.tiles) {
41053
+ if (!this.extremesLoad) {
40958
41054
  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
- });
41055
+ }
40980
41056
  return this.extremesLoad;
40981
41057
  }
40982
41058
  /*
40983
41059
  * 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
41060
  */
40988
41061
  async loadTiledBaselineMask() {
40989
41062
  const tiles = this.tiles;
40990
- if (!tiles) {
40991
- return;
40992
- }
40993
41063
  const entries = tiles.map((tile) => tile.BaselineMask);
40994
41064
  let lo = Number.MAX_SAFE_INTEGER;
40995
41065
  let hi = 0;
@@ -41043,14 +41113,11 @@
41043
41113
  /*
41044
41114
  * Composites each tile's floor and ceiling into rasters matching the composited frames.
41045
41115
  *
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.
41116
+ * A label indexes them by the same texel as the value canvas, so they have to be laid out the
41117
+ * same way rather than fetched as whole-extent rasters.
41048
41118
  */
41049
41119
  async loadTiledExtremes() {
41050
41120
  const tiles = this.tiles;
41051
- if (!tiles) {
41052
- return;
41053
- }
41054
41121
  const target = this.compositeExtent();
41055
41122
  const size = this.compositeSize(tiles, target);
41056
41123
  // One request for the whole extremes block. They sit contiguously after the frames, so
@@ -41145,70 +41212,6 @@
41145
41212
  }
41146
41213
  }));
41147
41214
  }
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
- }
41212
41215
  /**
41213
41216
  * Fetches and decodes the archive's static baseline mask, at most once.
41214
41217
  */
@@ -41351,7 +41354,7 @@
41351
41354
  function Now() {
41352
41355
  return typeof performance !== "undefined" ? performance.now() : Date.now();
41353
41356
  }
41354
- })(TextureFrameSeriesAnimator || (TextureFrameSeriesAnimator = {}));
41357
+ })(exports.TextureFrameSeriesAnimator || (exports.TextureFrameSeriesAnimator = {}));
41355
41358
 
41356
41359
  (function (TextureValueLabels) {
41357
41360
  // Off by default: a caller can still set a floor, but thinning is the mechanism that keeps a
@@ -41399,6 +41402,49 @@
41399
41402
  return out;
41400
41403
  }
41401
41404
  TextureValueLabels.ClipOutline = ClipOutline;
41405
+ /**
41406
+ * One cell's numbers as a label should print them, or null where the texture has no data there.
41407
+ * @param params at is the index of the cell's red channel. sourceMap is derived from the metadata
41408
+ * when omitted, which a caller reading many cells should hoist rather than repeat.
41409
+ */
41410
+ function ReadingAt(params) {
41411
+ const { metadata, source, floor, ceiling, at } = params;
41412
+ if (source.data[at + 3] < 128) {
41413
+ return null;
41414
+ }
41415
+ const lo = metadata && typeof metadata.ValueMin === "number" ? metadata.ValueMin : 0;
41416
+ const hi = metadata && typeof metadata.ValueMax === "number" ? metadata.ValueMax : 1;
41417
+ const normalised = exports.TextureFrameSeriesAnimator.NormalisedAt(metadata, source.data, at);
41418
+ const toSource = params.sourceMap !== undefined
41419
+ ? params.sourceMap
41420
+ : exports.TextureFrameSeriesAnimator.SourceMap(metadata);
41421
+ const inSource = (value) => exports.TextureFrameSeriesAnimator.ToSource(toSource, value);
41422
+ const reading = { value: inSource(lo + normalised * (hi - lo)) };
41423
+ const eLo = metadata && metadata.ExtremesValueMin;
41424
+ const eHi = metadata && metadata.ExtremesValueMax;
41425
+ if (typeof eLo === "number" && typeof eHi === "number") {
41426
+ if (floor) {
41427
+ reading.floor = inSource(eLo + decode16(floor.data, at) * (eHi - eLo));
41428
+ }
41429
+ if (ceiling) {
41430
+ reading.ceiling = inSource(eLo + decode16(ceiling.data, at) * (eHi - eLo));
41431
+ }
41432
+ }
41433
+ return reading;
41434
+ }
41435
+ TextureValueLabels.ReadingAt = ReadingAt;
41436
+ /**
41437
+ * The unit a label's value line should name, or undefined when the archive states none.
41438
+ * @param metadata the archive's Data.generation
41439
+ */
41440
+ function LabelUnits(metadata) {
41441
+ if (!metadata) {
41442
+ return undefined;
41443
+ }
41444
+ const recovered = exports.TextureFrameSeriesAnimator.SourceMap(metadata);
41445
+ return (recovered && metadata.SourceUnits) || metadata.Units;
41446
+ }
41447
+ TextureValueLabels.LabelUnits = LabelUnits;
41402
41448
  /**
41403
41449
  * How many cells apart labels should sit, as a power of two.
41404
41450
  * @param options cellPixels is one cell's size on screen, held is the stride already in use
@@ -41425,6 +41471,7 @@
41425
41471
  TextureValueLabels.ChooseLabelStride = ChooseLabelStride;
41426
41472
  class Labels {
41427
41473
  constructor(options) {
41474
+ this.sourceMap = null;
41428
41475
  this.source = null;
41429
41476
  this.painted = null;
41430
41477
  this.floor = null;
@@ -41436,6 +41483,9 @@
41436
41483
  this.extent = options.extent;
41437
41484
  this.settings = options.settings || {};
41438
41485
  this.metadata = options.metadata;
41486
+ // Once per overlay rather than once per cell: a label pass probes thousands of texels
41487
+ // looking for one worth labelling, and the map cannot change while the metadata does not.
41488
+ this.sourceMap = exports.TextureFrameSeriesAnimator.SourceMap(options.metadata);
41439
41489
  this.tiles = options.tiles;
41440
41490
  this.clipRing = options.clipRing;
41441
41491
  this.canvas = document.createElement("canvas");
@@ -41851,7 +41901,7 @@
41851
41901
  widestLabel(parts) {
41852
41902
  const sample = [];
41853
41903
  if (parts.indexOf("value") >= 0) {
41854
- sample.push("-000.00");
41904
+ sample.push(valueLine("-000.00", LabelUnits(this.metadata)));
41855
41905
  }
41856
41906
  const range = [];
41857
41907
  if (parts.indexOf("floor") >= 0) {
@@ -41891,7 +41941,7 @@
41891
41941
  const dp = (_a = this.settings.decimals) !== null && _a !== void 0 ? _a : (Math.abs(reading.value) < 10 ? 2 : 1);
41892
41942
  const lines = [];
41893
41943
  if (parts.indexOf("value") >= 0) {
41894
- lines.push(reading.value.toFixed(dp));
41944
+ lines.push(valueLine(reading.value.toFixed(dp), LabelUnits(this.metadata)));
41895
41945
  }
41896
41946
  // Named rather than a bare pair: two numbers under a third say nothing about which is
41897
41947
  // the series low and which is its high.
@@ -41965,29 +42015,17 @@
41965
42015
  }
41966
42016
  // Decodes one cell, skipping anything the texture marks as having no data.
41967
42017
  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) {
42018
+ if (!this.source) {
41971
42019
  return null;
41972
42020
  }
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;
42021
+ return ReadingAt({
42022
+ metadata: this.metadata,
42023
+ source: this.source,
42024
+ floor: this.floor,
42025
+ ceiling: this.ceiling,
42026
+ at: (row * cols + col) * 4,
42027
+ sourceMap: this.sourceMap
42028
+ });
41991
42029
  }
41992
42030
  lonAt(col, cols) {
41993
42031
  return this.extent.West + ((col + 0.5) / cols) * (this.extent.East - this.extent.West);
@@ -42026,6 +42064,13 @@
42026
42064
  }
42027
42065
  }
42028
42066
  TextureValueLabels.Labels = Labels;
42067
+ /*
42068
+ * The value line. Units go on it alone: repeating them on the range underneath doubles the widest
42069
+ * label to say nothing, and the two lines are plainly the same quantity.
42070
+ */
42071
+ function valueLine(value, units) {
42072
+ return units ? `${value} (${units})` : value;
42073
+ }
42029
42074
  function decode16(data, i) {
42030
42075
  return (data[i] * 256 + data[i + 1]) / 65535;
42031
42076
  }
@@ -42046,11 +42091,6 @@
42046
42091
  const SKIRT_FRACTION = 0.015;
42047
42092
  // Alpha below which a texel counts as no-data and is not drawn at all.
42048
42093
  const COVERAGE_CUTOFF = 0.35;
42049
- // With no exaggeration stated, the full value range is drawn as this fraction of the extent's shorter side.
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;
42054
42094
  // Terrain is sampled on this grid and interpolated between samples.
42055
42095
  // Per-texel sampling would be hundreds of thousands of lookups for a surface whose own data is far coarser than that.
42056
42096
  const GROUND_SAMPLES_PER_SIDE = 64;
@@ -42071,6 +42111,7 @@ in float skirt;
42071
42111
 
42072
42112
  uniform sampler2D u_valueTexture;
42073
42113
  uniform sampler2D u_groundTexture;
42114
+ uniform float u_valuePacked;
42074
42115
  uniform float u_valueMin;
42075
42116
  uniform float u_valueRange;
42076
42117
  uniform float u_exaggeration;
@@ -42108,18 +42149,37 @@ float sampleGround(vec2 uv) {
42108
42149
  return mix(mix(h00, h10, f.x), mix(h01, h11, f.x), f.y);
42109
42150
  }
42110
42151
 
42152
+ /*
42153
+ * The texel's normalised value, whichever way the archive packed it.
42154
+ *
42155
+ * A packed frame keeps the high byte in R, so reading R alone would quantise the whole range to 255
42156
+ * steps and draw a plausible wrong surface rather than an obviously broken one.
42157
+ *
42158
+ * Each channel is scaled and then added, rather than reassembled to a 0..65535 integer and divided.
42159
+ * That integer is outside the range mediump guarantees, and a fragment shader only gets highp where
42160
+ * the device offers it, so the obvious form loses the low byte on exactly the older mobile hardware
42161
+ * that Cesium falls back to mediump for.
42162
+ */
42163
+ float decodeValue(vec4 texel) {
42164
+ float packed16 = texel.r * (255.0 * 256.0 / 65535.0) + texel.g * (255.0 / 65535.0);
42165
+ return mix(texel.r, packed16, u_valuePacked);
42166
+ }
42167
+
42111
42168
  void main() {
42112
42169
  vec4 texel = texture(u_valueTexture, st);
42113
42170
  v_st = st;
42114
- v_value = texel.r;
42171
+ v_value = decodeValue(texel);
42115
42172
  v_coverage = texel.a;
42116
42173
 
42117
- float metres = u_valueMin + texel.r * u_valueRange;
42174
+ // Exaggeration stretches the span above the archive's minimum rather than the height itself. On an
42175
+ // elevation the height is measured from the ellipsoid, tens of metres from any of the data, so
42176
+ // scaling that instead translates the whole sheet underground rather than spreading it out.
42177
+ float metres = u_valueMin + v_value * u_valueRange * u_exaggeration;
42118
42178
 
42119
42179
  // Value zero sits at the polygon's own altitude, plus the terrain underneath when the polygon
42120
42180
  // is following the ground rather than absolutely positioned.
42121
42181
  float ground = sampleGround(st) * u_groundWeight;
42122
- float displacement = u_baseHeight + ground + metres * u_exaggeration * texel.a - skirt * u_skirtDepth;
42182
+ float displacement = u_baseHeight + ground + metres * texel.a - skirt * u_skirtDepth;
42123
42183
 
42124
42184
  // Added to the LOW half of the encoded position: a metre-scale offset added to the high half
42125
42185
  // would be lost to float32 rounding at an earth radius.
@@ -42132,6 +42192,7 @@ uniform sampler2D u_valueTexture;
42132
42192
  uniform sampler2D u_rampTexture;
42133
42193
  uniform float u_coverageCutoff;
42134
42194
  uniform vec2 u_texelSize;
42195
+ uniform float u_valuePacked;
42135
42196
  uniform float u_valueRange;
42136
42197
  uniform float u_exaggeration;
42137
42198
  uniform float u_metresPerTexel;
@@ -42140,6 +42201,16 @@ in vec2 v_st;
42140
42201
  in float v_value;
42141
42202
  in float v_coverage;
42142
42203
 
42204
+ /*
42205
+ * Scaled per channel for the same reason the vertex shader's decode is: reassembling the pair reaches
42206
+ * 65535, which mediump does not have to represent, and this is the shader that may not get highp.
42207
+ */
42208
+ float valueAt(vec2 uv) {
42209
+ vec4 texel = texture(u_valueTexture, uv);
42210
+ float packed16 = texel.r * (255.0 * 256.0 / 65535.0) + texel.g * (255.0 / 65535.0);
42211
+ return mix(texel.r, packed16, u_valuePacked);
42212
+ }
42213
+
42143
42214
  void main() {
42144
42215
  if (v_coverage < u_coverageCutoff) {
42145
42216
  discard;
@@ -42154,10 +42225,10 @@ void main() {
42154
42225
 
42155
42226
  // Relief shading from the value gradient, so the displacement reads at a distance without
42156
42227
  // recomputing vertex normals every time the texture changes.
42157
- float left = texture(u_valueTexture, v_st - vec2(u_texelSize.x, 0.0)).r;
42158
- float right = texture(u_valueTexture, v_st + vec2(u_texelSize.x, 0.0)).r;
42159
- float down = texture(u_valueTexture, v_st - vec2(0.0, u_texelSize.y)).r;
42160
- float up = texture(u_valueTexture, v_st + vec2(0.0, u_texelSize.y)).r;
42228
+ float left = valueAt(v_st - vec2(u_texelSize.x, 0.0));
42229
+ float right = valueAt(v_st + vec2(u_texelSize.x, 0.0));
42230
+ float down = valueAt(v_st - vec2(0.0, u_texelSize.y));
42231
+ float up = valueAt(v_st + vec2(0.0, u_texelSize.y));
42161
42232
  float scale = u_valueRange * u_exaggeration;
42162
42233
  vec3 n = normalize(vec3((left - right) * scale, (down - up) * scale, 2.0 * u_metresPerTexel));
42163
42234
  float lambert = clamp(dot(n, normalize(vec3(-0.5, -0.6, 0.62))), 0.0, 1.0);
@@ -42367,8 +42438,9 @@ void main() {
42367
42438
  this.source = options.source || null;
42368
42439
  this.valueMin = (_a = options.valueMin) !== null && _a !== void 0 ? _a : 0;
42369
42440
  this.valueMax = (_b = options.valueMax) !== null && _b !== void 0 ? _b : 1;
42370
- this.exaggeration = (_c = options.exaggeration) !== null && _c !== void 0 ? _c : autoExaggeration(this.extent, this.valueMin, this.valueMax);
42441
+ this.exaggeration = (_c = options.exaggeration) !== null && _c !== void 0 ? _c : 1;
42371
42442
  this.baseHeight = (_d = options.baseHeight) !== null && _d !== void 0 ? _d : 0;
42443
+ this.packedValue = Boolean(options.packedValue);
42372
42444
  this.pixelPlacement = options.pixelPlacement;
42373
42445
  this.tileSkirts = Boolean(options.tileSkirts);
42374
42446
  this.rampPixels = RampLookup(options.rampStops || null, options.lowColor || DEFAULT_LOW_COLOR, options.highColor || DEFAULT_HIGH_COLOR);
@@ -42592,6 +42664,14 @@ void main() {
42592
42664
  if (this.texture) {
42593
42665
  this.texture.destroy();
42594
42666
  }
42667
+ // Nearest for a packed value, because filtering would blend the two bytes of the
42668
+ // packing rather than the values they encode, the same trap sampleGround avoids.
42669
+ const minification = this.packedValue
42670
+ ? Cesium.TextureMinificationFilter.NEAREST
42671
+ : Cesium.TextureMinificationFilter.LINEAR;
42672
+ const magnification = this.packedValue
42673
+ ? Cesium.TextureMagnificationFilter.NEAREST
42674
+ : Cesium.TextureMagnificationFilter.LINEAR;
42595
42675
  this.texture = new Cesium.Texture({
42596
42676
  context,
42597
42677
  source: this.source,
@@ -42601,8 +42681,8 @@ void main() {
42601
42681
  sampler: new Cesium.Sampler({
42602
42682
  wrapS: Cesium.TextureWrap.CLAMP_TO_EDGE,
42603
42683
  wrapT: Cesium.TextureWrap.CLAMP_TO_EDGE,
42604
- minificationFilter: Cesium.TextureMinificationFilter.LINEAR,
42605
- magnificationFilter: Cesium.TextureMagnificationFilter.LINEAR
42684
+ minificationFilter: minification,
42685
+ magnificationFilter: magnification
42606
42686
  })
42607
42687
  });
42608
42688
  this.textureDirty = false;
@@ -42703,6 +42783,7 @@ void main() {
42703
42783
  uniformMap: {
42704
42784
  u_valueTexture: () => self.texture,
42705
42785
  u_groundTexture: () => self.groundTexture,
42786
+ u_valuePacked: () => (self.packedValue ? 1 : 0),
42706
42787
  u_groundWeight: () => (self.followGround && self.groundPixels ? 1 : 0),
42707
42788
  u_groundRange: () => new Cesium.Cartesian2(GROUND_MIN_METRES, GROUND_MAX_METRES),
42708
42789
  u_groundSize: () => new Cesium.Cartesian2(self.groundPixels ? GROUND_SAMPLES_PER_SIDE : 1, self.groundPixels ? GROUND_SAMPLES_PER_SIDE : 1),
@@ -42755,31 +42836,6 @@ void main() {
42755
42836
  }
42756
42837
  return tiles;
42757
42838
  }
42758
- /**
42759
- * Exaggeration that makes the full value range stand AUTO_RELIEF_FRACTION of the extent tall.
42760
- *
42761
- * Derived from the whole extent rather than per tile: scaling each tile to its own size would
42762
- * make neighbours disagree along their shared edge and tear the surface apart.
42763
- * @param extent
42764
- * @param valueMin
42765
- * @param valueMax
42766
- */
42767
- function autoExaggeration(extent, valueMin, valueMax) {
42768
- const range = Math.abs(valueMax - valueMin);
42769
- if (!(range > 0)) {
42770
- return 1;
42771
- }
42772
- const southWest = Cesium.Cartesian3.fromDegrees(extent.West, extent.South, 0);
42773
- const southEast = Cesium.Cartesian3.fromDegrees(extent.East, extent.South, 0);
42774
- const northWest = Cesium.Cartesian3.fromDegrees(extent.West, extent.North, 0);
42775
- const shorterSide = Math.min(Cesium.Cartesian3.distance(southWest, southEast), Cesium.Cartesian3.distance(southWest, northWest));
42776
- if (!(shorterSide > 0)) {
42777
- return 1;
42778
- }
42779
- const relief = Math.min(shorterSide * AUTO_RELIEF_FRACTION, AUTO_MAX_RELIEF_METRES);
42780
- return relief / range;
42781
- }
42782
- DisplacedSurfacePrimitive.autoExaggeration = autoExaggeration;
42783
42839
  function now() {
42784
42840
  return typeof performance !== "undefined" ? performance.now() : Date.now();
42785
42841
  }
@@ -43517,12 +43573,12 @@ void main() {
43517
43573
  && existingAnimator.GetArchiveUrl() === extrusionArchive.url
43518
43574
  && existingSurface
43519
43575
  && !existingSurface.isDestroyed();
43520
- const placement = surfacePlacement(entity, heightRef);
43576
+ const placement = surfacePlacement(entity, heightRef, extrusionArchive && exports.TextureFrameSeriesAnimator.IsDatumHeight(extrusionArchive.metadata));
43521
43577
  // Whether the surface follows terrain is baked into its ground sampling, so a change there has
43522
43578
  // to rebuild rather than update in place.
43523
43579
  if (reusable && existingSurface.GetFollowsGround() === placement.followGround) {
43524
43580
  existingSurface.SetBaseHeight(placement.baseHeight);
43525
- existingSurface.SetExaggeration(exaggerationFor(style, extrusionArchive.metadata, existingSurface.GetExtent()));
43581
+ existingSurface.SetExaggeration(exaggerationFor(style));
43526
43582
  return;
43527
43583
  }
43528
43584
  disposeDisplacedSurface(cEntity, viewer);
@@ -43536,7 +43592,7 @@ void main() {
43536
43592
  }
43537
43593
  // Drives the value canvas only: the surface paints the colour, so the polygon's own material is
43538
43594
  // left transparent rather than animated underneath it.
43539
- const animator = new TextureFrameSeriesAnimator.Animator({
43595
+ const animator = new exports.TextureFrameSeriesAnimator.Animator({
43540
43596
  viewer,
43541
43597
  entity: cEntity,
43542
43598
  archiveUrl: extrusionArchive.url,
@@ -43546,8 +43602,6 @@ void main() {
43546
43602
  produceValueCanvas: true,
43547
43603
  metadata: extrusionArchive.metadata,
43548
43604
  cellTexels: 1,
43549
- baselineMask: extrusionArchive.metadata.BaselineMask,
43550
- // Off-type for the same reason as the fill path: the pinned bruce-models predates the field.
43551
43605
  maskBaseline: style.maskTextureBaseline
43552
43606
  });
43553
43607
  cEntity[EXTRUSION_ANIMATOR_KEY] = animator;
@@ -43562,13 +43616,14 @@ void main() {
43562
43616
  terrainProvider: viewer && viewer.terrainProvider,
43563
43617
  valueMin: extrusionArchive.metadata.ValueMin,
43564
43618
  valueMax: extrusionArchive.metadata.ValueMax,
43565
- exaggeration: exaggerationFor(style, extrusionArchive.metadata, extent),
43619
+ packedValue: exports.TextureFrameSeriesAnimator.IsPackedValue(extrusionArchive.metadata),
43620
+ exaggeration: exaggerationFor(style),
43566
43621
  lowColor: mask ? BModels.Color.ColorFromStr(mask.minColor) : null,
43567
43622
  highColor: mask ? BModels.Color.ColorFromStr(mask.maxColor) : null,
43568
43623
  // Normalised the same way the animator normalises them, so the surface and the flat drape
43569
43624
  // agree about which values the style hides.
43570
- rampStops: (mask && mask.points || []).map((stop) => ({
43571
- position: TextureFrameSeriesAnimator.NormalisePosition(extrusionArchive.metadata, stop.position),
43625
+ rampStops: ((mask && mask.points) || []).map((stop) => ({
43626
+ position: exports.TextureFrameSeriesAnimator.NormalisePosition(extrusionArchive.metadata, stop.position),
43572
43627
  color: BModels.Color.ColorFromStr(stop.color)
43573
43628
  })).filter((stop) => Boolean(stop.color))
43574
43629
  });
@@ -43583,7 +43638,7 @@ void main() {
43583
43638
  * @param entity
43584
43639
  * @param heightRef
43585
43640
  */
43586
- function surfacePlacement(entity, heightRef) {
43641
+ function surfacePlacement(entity, heightRef, datumHeight) {
43587
43642
  const rawAltitude = BModels.Entity.GetValue({
43588
43643
  entity,
43589
43644
  path: ["Bruce", "Location", "altitude"]
@@ -43595,32 +43650,18 @@ void main() {
43595
43650
  if (heightRef === Cesium.HeightReference.RELATIVE_TO_GROUND) {
43596
43651
  return { baseHeight: altitude, followGround: true };
43597
43652
  }
43598
- return { baseHeight: altitude, followGround: false };
43653
+ // Values that ARE heights against a datum already say where they belong, so adding the entity's
43654
+ // altitude on top would move a measured surface off the datum it was measured against.
43655
+ return { baseHeight: datumHeight ? 0 : altitude, followGround: false };
43599
43656
  }
43600
- /**
43601
- * The archive's own extent when it recorded one, otherwise the polygon's own bounds so archives
43602
- * generated before the extent was written still render somewhere sensible.
43603
- * @param metadata
43604
- * @param posses
43605
- */
43606
43657
  /*
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.
43658
+ * The factor a displaced surface should use, which is the style's or nothing.
43612
43659
  */
43613
- function exaggerationFor(style, metadata, extent) {
43660
+ function exaggerationFor(style) {
43614
43661
  if (style.extrusionExaggeration != null) {
43615
43662
  return style.extrusionExaggeration;
43616
43663
  }
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);
43664
+ return 1;
43624
43665
  }
43625
43666
  /*
43626
43667
  * The lon/lat bounding rectangle of a ring, which is the frame Cesium drapes an image material in.
@@ -43696,7 +43737,7 @@ void main() {
43696
43737
  // Appearance is part of the reuse test, not just the archive URL. Recolouring a texture-driven
43697
43738
  // polygon keeps the same archive, so matching on the URL alone kept the old Animator and the
43698
43739
  // style edit appeared to do nothing at all.
43699
- const appearance = TextureFrameSeriesAnimator.AppearanceSignature({
43740
+ const appearance = exports.TextureFrameSeriesAnimator.AppearanceSignature({
43700
43741
  textureColorMask, cellBorder, cellTexels, maskBaseline: maskTextureBaseline,
43701
43742
  valueCanvas: produceValueCanvas, drape: drapeExtent
43702
43743
  });
@@ -43712,7 +43753,7 @@ void main() {
43712
43753
  if (!frameArchive) {
43713
43754
  return null;
43714
43755
  }
43715
- const animator = new TextureFrameSeriesAnimator.Animator({
43756
+ const animator = new exports.TextureFrameSeriesAnimator.Animator({
43716
43757
  viewer,
43717
43758
  entity: cEntity,
43718
43759
  archiveUrl: frameArchive.url,
@@ -43721,7 +43762,6 @@ void main() {
43721
43762
  metadata: frameArchive.metadata,
43722
43763
  cellBorder,
43723
43764
  cellTexels,
43724
- baselineMask: frameArchive.metadata.BaselineMask,
43725
43765
  maskBaseline: maskTextureBaseline,
43726
43766
  produceValueCanvas,
43727
43767
  drapeExtent
@@ -43782,12 +43822,10 @@ void main() {
43782
43822
  }
43783
43823
  const labels = new exports.TextureValueLabels.Labels({
43784
43824
  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,
43825
+ tiles: exports.TextureFrameSeriesAnimator.TilesOf(metadata).map((tile) => ({
43826
+ West: tile.West, East: tile.East, South: tile.South, North: tile.North,
43827
+ ResolutionX: tile.ResolutionX, ResolutionY: tile.ResolutionY
43828
+ })),
43791
43829
  clipRing: params.clipRing
43792
43830
  });
43793
43831
  let lastVersion = -1;
@@ -43920,11 +43958,15 @@ void main() {
43920
43958
  const { clientFile } = await BModels.ClientFile.Get({ api, fileId: clientFileId });
43921
43959
  url = clientFile.URL;
43922
43960
  const clientFileData = clientFile.Data;
43923
- if (TextureFrameSeriesAnimator.IsFrameArchiveMetadata(clientFileData)) {
43961
+ if (exports.TextureFrameSeriesAnimator.IsFrameArchiveMetadata(clientFileData)) {
43962
+ const adapted = exports.TextureFrameSeriesAnimator.Adapt(clientFileData.generation);
43963
+ if (!adapted) {
43964
+ return { dataUri: null, effective: textureTrace.effective };
43965
+ }
43924
43966
  return {
43925
43967
  dataUri: null,
43926
43968
  effective: textureTrace.effective,
43927
- frameArchive: { url, metadata: clientFileData.generation }
43969
+ frameArchive: { url, metadata: adapted }
43928
43970
  };
43929
43971
  }
43930
43972
  }
@@ -43952,8 +43994,12 @@ void main() {
43952
43994
  }
43953
43995
  const url = clientFile.URL;
43954
43996
  const clientFileData = clientFile.Data;
43955
- if (TextureFrameSeriesAnimator.IsFrameArchiveMetadata(clientFileData)) {
43956
- return { dataUri: null, effective: null, frameArchive: { url, metadata: clientFileData.generation } };
43997
+ if (exports.TextureFrameSeriesAnimator.IsFrameArchiveMetadata(clientFileData)) {
43998
+ const adapted = exports.TextureFrameSeriesAnimator.Adapt(clientFileData.generation);
43999
+ if (adapted) {
44000
+ return { dataUri: null, effective: null, frameArchive: { url, metadata: adapted } };
44001
+ }
44002
+ return { dataUri: null, effective: null };
43957
44003
  }
43958
44004
  const cacheKey = "texture-" + url + "-none";
43959
44005
  let prom = _textureCache.Get(cacheKey);
@@ -45422,7 +45468,7 @@ void main() {
45422
45468
  StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
45423
45469
  })(exports.StyleUtils || (exports.StyleUtils = {}));
45424
45470
 
45425
- const VERSION = "7.2.2";
45471
+ const VERSION = "7.2.3";
45426
45472
  /**
45427
45473
  * Updates the environment instance used by bruce-cesium to one specified.
45428
45474
  * This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.