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.
@@ -6,6 +6,87 @@ const Cesium = require("cesium");
6
6
  const image_utils_1 = require("../internal/image-utils");
7
7
  var TextureFrameSeriesAnimator;
8
8
  (function (TextureFrameSeriesAnimator) {
9
+ // Values are measured up from the ground beneath them.
10
+ TextureFrameSeriesAnimator.QUANTITY_THICKNESS = "thickness";
11
+ // Values are heights against a vertical datum, so they need that datum to be placed.
12
+ TextureFrameSeriesAnimator.QUANTITY_ELEVATION = "elevation";
13
+ TextureFrameSeriesAnimator.LAYOUT_TILES = "tiles";
14
+ /**
15
+ * The generator that produced an archive, for telling a stale one from a current one.
16
+ *
17
+ * Read the version rather than sniffing for fields: an archive can legitimately omit Tiles or
18
+ * GeoidSeparation and still be current, so their absence says nothing about its age.
19
+ * @param metadata the archive's Data.generation
20
+ */
21
+ function GeneratorVersion(metadata) {
22
+ if (!metadata || typeof metadata.GeneratorVersion !== "number") {
23
+ return 1;
24
+ }
25
+ return metadata.GeneratorVersion;
26
+ }
27
+ TextureFrameSeriesAnimator.GeneratorVersion = GeneratorVersion;
28
+ /**
29
+ * Whether an archive is an adaptive pyramid rather than one raster.
30
+ * @param metadata the archive's Data.generation
31
+ */
32
+ function IsTiledLayout(metadata) {
33
+ return (Boolean(metadata && metadata.Layout === TextureFrameSeriesAnimator.LAYOUT_TILES &&
34
+ metadata.Tiles && metadata.Tiles.length > 0));
35
+ }
36
+ TextureFrameSeriesAnimator.IsTiledLayout = IsTiledLayout;
37
+ // Frames carry the value as the source reported it.
38
+ TextureFrameSeriesAnimator.MEASURE_ABSOLUTE = "absolute";
39
+ // Frames carry the value minus each cell's own minimum over the series.
40
+ TextureFrameSeriesAnimator.MEASURE_ANOMALY = "anomaly";
41
+ /**
42
+ * Turns a value in the attribute's own units into the 0 to 1 position the ramp is indexed by.
43
+ *
44
+ * Stops are authored in real units, since "hide anything under 0.1 m" is the sentence a user
45
+ * actually has, and the archive's own published range is what makes that expressible.
46
+ * @param metadata the archive's Data.generation
47
+ * @param value in the attribute's units
48
+ */
49
+ function NormalisePosition(metadata, value) {
50
+ const lo = metadata && typeof metadata.ValueMin === "number" ? metadata.ValueMin : 0;
51
+ const hi = metadata && typeof metadata.ValueMax === "number" ? metadata.ValueMax : 1;
52
+ const span = hi - lo;
53
+ if (!(span > 0)) {
54
+ return 0;
55
+ }
56
+ return Math.min(1, Math.max(0, (value - lo) / span));
57
+ }
58
+ TextureFrameSeriesAnimator.NormalisePosition = NormalisePosition;
59
+ /**
60
+ * Whether the frames are a departure from each cell's own normal rather than a raw reading.
61
+ *
62
+ * Worth asking before labelling anything: the two measures need different words for the same
63
+ * number, and calling an anomaly "water depth" is a worse error than the coarse ramp it replaced.
64
+ * @param metadata the archive's Data.generation
65
+ */
66
+ function IsAnomalyMeasure(metadata) {
67
+ return Boolean(metadata && metadata.Measure === TextureFrameSeriesAnimator.MEASURE_ANOMALY);
68
+ }
69
+ TextureFrameSeriesAnimator.IsAnomalyMeasure = IsAnomalyMeasure;
70
+ /**
71
+ * How much the value moves anywhere in the series, in source units.
72
+ *
73
+ * Falls back to the published range, which is the right answer for an anomaly archive (its range
74
+ * IS the movement) and the only available one for an archive predating AnomalyMax.
75
+ * @param metadata the archive's Data.generation
76
+ */
77
+ function MovingRange(metadata) {
78
+ if (!metadata) {
79
+ return undefined;
80
+ }
81
+ if (typeof metadata.AnomalyMax === "number" && metadata.AnomalyMax > 0) {
82
+ return metadata.AnomalyMax;
83
+ }
84
+ if (typeof metadata.ValueMin === "number" && typeof metadata.ValueMax === "number") {
85
+ return Math.abs(metadata.ValueMax - metadata.ValueMin);
86
+ }
87
+ return undefined;
88
+ }
89
+ TextureFrameSeriesAnimator.MovingRange = MovingRange;
9
90
  /**
10
91
  * Detects whether a ClientFile's `Data.generation` metadata describes a frame archive rather than a single static image,
11
92
  * so a caller can decide whether to construct an Animator or fall back to the existing static-texture path.
@@ -15,15 +96,51 @@ var TextureFrameSeriesAnimator;
15
96
  return Boolean(data && data.generation && Array.isArray(data.generation.Frames) && data.generation.Frames.length > 0);
16
97
  }
17
98
  TextureFrameSeriesAnimator.IsFrameArchiveMetadata = IsFrameArchiveMetadata;
18
- // How far covered values bleed outward into uncovered texels before the GPU samples them.
99
+ // First resolvable static number out of a calculator field list, since that is all a border needs.
100
+ function resolveNumber(fields) {
101
+ if (!fields) {
102
+ return 0;
103
+ }
104
+ for (const field of fields) {
105
+ const value = Number(field && field.value);
106
+ if (Number.isFinite(value)) {
107
+ return value;
108
+ }
109
+ }
110
+ return 0;
111
+ }
112
+ // First resolvable static colour out of a calculator field list.
113
+ function resolveColor(fields) {
114
+ if (!fields) {
115
+ return null;
116
+ }
117
+ for (const field of fields) {
118
+ const parsed = typeof (field === null || field === void 0 ? void 0 : field.value) === "string"
119
+ ? bruce_models_1.Color.ColorFromStr(field.value)
120
+ : null;
121
+ if (parsed) {
122
+ return parsed;
123
+ }
124
+ }
125
+ return null;
126
+ }
127
+ /*
128
+ * Everything about an Animator that a style can change, as a comparable string.
129
+ */
130
+ function AppearanceSignature(options) {
131
+ var _a, _b, _c;
132
+ return JSON.stringify([
133
+ (_a = options.textureColorMask) !== null && _a !== void 0 ? _a : null,
134
+ (_b = options.cellBorder) !== null && _b !== void 0 ? _b : null,
135
+ (_c = options.cellTexels) !== null && _c !== void 0 ? _c : null,
136
+ Boolean(options.maskBaseline)
137
+ ]);
138
+ }
139
+ TextureFrameSeriesAnimator.AppearanceSignature = AppearanceSignature;
140
+ const MAX_COMPOSITE_TEXELS = 2048;
19
141
  const VALUE_DILATE_PASSES = 2;
20
142
  /*
21
143
  * Bleeds covered values outward into uncovered texels, leaving alpha untouched.
22
- *
23
- * The GPU samples the value texture with linear filtering, so an uncovered texel's RGB still gets
24
- * averaged into the vertices next to it even though its alpha is zero. Land sits at one end of the
25
- * ramp, so without this the coastline grows a row of spikes exactly where the data stops. Masking the
26
- * baseline creates more of these edges, which is what makes this necessary rather than cosmetic.
27
144
  */
28
145
  function dilateValues(value, width, height, passes) {
29
146
  const texels = width * height;
@@ -69,7 +186,7 @@ var TextureFrameSeriesAnimator;
69
186
  const DEFAULT_HIGH_COLOR = { red: 21, green: 96, blue: 196, alpha: 0.92 };
70
187
  class Animator {
71
188
  constructor(options) {
72
- var _a, _b;
189
+ var _a, _b, _c;
73
190
  this.removeOnTick = null;
74
191
  this.removeCrossfadeTick = null;
75
192
  this.disposed = false;
@@ -95,8 +212,12 @@ var TextureFrameSeriesAnimator;
95
212
  // forcing Cesium to re-upload the texture only when something changed.
96
213
  this.pool = [document.createElement("canvas"), document.createElement("canvas")];
97
214
  this.poolIdx = 0;
215
+ this.scratch = null;
98
216
  this.valueDims = null;
99
217
  this.presentVersion = 0;
218
+ this.extremesLoad = null;
219
+ this.floorPixels = null;
220
+ this.ceilingPixels = null;
100
221
  if (!options.entity.polygon) {
101
222
  throw new Error("TextureFrameSeriesAnimator requires an entity with polygon graphics.");
102
223
  }
@@ -108,16 +229,41 @@ var TextureFrameSeriesAnimator;
108
229
  this.archiveUrl = options.archiveUrl;
109
230
  this.frames = options.frames;
110
231
  this.crossfadeMs = (_a = options.crossfadeMs) !== null && _a !== void 0 ? _a : DEFAULT_CROSSFADE_MS;
232
+ this.appearance = AppearanceSignature(options);
111
233
  const mask = options.textureColorMask;
112
234
  this.lowColor = (mask && bruce_models_1.Color.ColorFromStr(mask.minColor)) || DEFAULT_LOW_COLOR;
113
235
  this.highColor = (mask && bruce_models_1.Color.ColorFromStr(mask.maxColor)) || DEFAULT_HIGH_COLOR;
236
+ // Positions are authored in the attribute's units and normalised once here, so the
237
+ // per-pixel loop stays a comparison against 0 to 1 like the two-colour path.
238
+ const points = mask === null || mask === void 0 ? void 0 : mask.points;
239
+ this.rampStops = (points && points.length > 0)
240
+ ? points.map((p) => ({
241
+ position: NormalisePosition(options.metadata, p.position),
242
+ color: bruce_models_1.Color.ColorFromStr(p.color) || DEFAULT_LOW_COLOR
243
+ }))
244
+ : null;
245
+ const border = options.cellBorder;
246
+ const borderWidth = border ? resolveNumber(border.width) : 0;
247
+ const borderColor = border ? resolveColor(border.color) : null;
248
+ // Zero width or a fully transparent colour means no grid, matching how the polygon's own
249
+ // outline behaves, so an existing style that wants no border keeps getting none.
250
+ this.cellBorder = (border && borderWidth > 0 && borderColor && borderColor.alpha > 0)
251
+ ? {
252
+ cellTexels: Math.max(1, Math.round((_b = options.cellTexels) !== null && _b !== void 0 ? _b : 1)),
253
+ widthTexels: borderWidth,
254
+ color: borderColor
255
+ }
256
+ : null;
114
257
  this.frameDates = this.frames.map((f) => Cesium.JulianDate.fromIso8601(f.Timestamp));
115
258
  this.frameCache = new Array(this.frames.length).fill(null);
116
259
  this.frameDims = new Array(this.frames.length).fill(null);
117
260
  this.valueCache = new Array(this.frames.length).fill(null);
118
261
  this.produceValueCanvas = Boolean(options.produceValueCanvas);
262
+ this.metadata = options.metadata;
263
+ this.drapeExtent = options.drapeExtent;
264
+ this.tiles = IsTiledLayout(options.metadata) ? options.metadata.Tiles : undefined;
119
265
  this.baselineMaskEntry = options.baselineMask || null;
120
- this.maskBaseline = (_b = options.maskBaseline) !== null && _b !== void 0 ? _b : Boolean(options.baselineMask);
266
+ this.maskBaseline = (_c = options.maskBaseline) !== null && _c !== void 0 ? _c : Boolean(options.baselineMask);
121
267
  this.valueCanvas = this.produceValueCanvas ? document.createElement("canvas") : null;
122
268
  this.driveMaterial = options.driveMaterial !== false;
123
269
  this.originalMaterial = options.entity.polygon.material;
@@ -153,6 +299,10 @@ var TextureFrameSeriesAnimator;
153
299
  IsDisposed() {
154
300
  return this.disposed;
155
301
  }
302
+ // What this Animator was built to look like, for deciding whether it can be reused.
303
+ GetAppearanceSignature() {
304
+ return this.appearance;
305
+ }
156
306
  /**
157
307
  * The archive URL this instance was constructed with,
158
308
  * lets a caller re-rendering the same entity tell whether an existing instance is already correct, without reaching into private state.
@@ -167,6 +317,15 @@ var TextureFrameSeriesAnimator;
167
317
  GetImageProperty() {
168
318
  return this.imageProperty;
169
319
  }
320
+ /**
321
+ * The presented frame's TINTED pixels, where alpha is what the ramp actually painted.
322
+ *
323
+ * Distinct from the value canvas, whose alpha only says a texel has data. A cell can hold a
324
+ * reading and still be painted nothing, which is exactly what a hidden floor band does.
325
+ */
326
+ GetDisplayedPixels() {
327
+ return this.displayedPixels;
328
+ }
170
329
  /**
171
330
  * The untinted canvas carrying the presented frame's value in RGB and its coverage in alpha,
172
331
  * or null unless the instance was constructed with produceValueCanvas.
@@ -279,11 +438,33 @@ var TextureFrameSeriesAnimator;
279
438
  });
280
439
  }
281
440
  async fetchAndTint(idx) {
282
- const entry = this.frames[idx];
283
- const start = entry.ByteOffset;
284
- const end = entry.ByteOffset + entry.ByteLength - 1;
285
- const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${start}-${end}` } });
286
- const buffer = await response.arrayBuffer();
441
+ const tiles = this.tiles;
442
+ let buffer;
443
+ let span = 0;
444
+ if (tiles) {
445
+ // Every tile's frame for one timestep sits contiguously, because the generator writes
446
+ // frame major. So a tiled frame is still ONE range request, not one per tile.
447
+ let lo = Number.MAX_SAFE_INTEGER;
448
+ let hi = 0;
449
+ for (const tile of tiles) {
450
+ const at = tile.Frames[idx];
451
+ if (!at) {
452
+ continue;
453
+ }
454
+ lo = Math.min(lo, at.ByteOffset);
455
+ hi = Math.max(hi, at.ByteOffset + at.ByteLength);
456
+ }
457
+ span = lo;
458
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
459
+ buffer = await response.arrayBuffer();
460
+ }
461
+ else {
462
+ const entry = this.frames[idx];
463
+ const start = entry.ByteOffset;
464
+ const end = entry.ByteOffset + entry.ByteLength - 1;
465
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${start}-${end}` } });
466
+ buffer = await response.arrayBuffer();
467
+ }
287
468
  if (this.disposed) {
288
469
  return;
289
470
  }
@@ -293,7 +474,9 @@ var TextureFrameSeriesAnimator;
293
474
  if (this.disposed) {
294
475
  return;
295
476
  }
296
- const decoded = await this.decodeAndTint(buffer);
477
+ const decoded = tiles
478
+ ? await this.composeTiles(tiles, idx, buffer, span)
479
+ : await this.decodeAndTint(buffer);
297
480
  if (this.disposed) {
298
481
  return;
299
482
  }
@@ -322,21 +505,438 @@ var TextureFrameSeriesAnimator;
322
505
  canvas.height = image.height;
323
506
  const ctx = canvas.getContext("2d");
324
507
  ctx.drawImage(image, 0, 0);
325
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
508
+ return this.tint(ctx.getImageData(0, 0, canvas.width, canvas.height));
509
+ }
510
+ /*
511
+ * Draws every tile of one frame into a single raster covering the drape extent.
512
+ */
513
+ async composeTiles(tiles, idx, buffer, spanStart) {
514
+ const target = this.compositeExtent();
515
+ const size = this.compositeSize(tiles, target);
516
+ const canvas = document.createElement("canvas");
517
+ canvas.width = size.width;
518
+ canvas.height = size.height;
519
+ const ctx = canvas.getContext("2d");
520
+ ctx.imageSmoothingEnabled = false;
521
+ ctx.clearRect(0, 0, size.width, size.height);
522
+ const images = await this.decodeSlices(buffer, spanStart, tiles.map((tile) => tile.Frames[idx]));
523
+ for (let i = 0; i < tiles.length; i++) {
524
+ const image = images[i];
525
+ if (!image || this.disposed) {
526
+ continue;
527
+ }
528
+ const at = this.tileRect(tiles[i], target, size);
529
+ ctx.drawImage(image, at.x, at.y, at.w, at.h);
530
+ }
531
+ return this.tint(ctx.getImageData(0, 0, size.width, size.height));
532
+ }
533
+ /*
534
+ * Where a composited frame is draped, which is the polygon's rectangle when there is one.
535
+ */
536
+ compositeExtent() {
537
+ if (this.drapeExtent) {
538
+ return this.drapeExtent;
539
+ }
540
+ const m = this.metadata;
541
+ return {
542
+ West: m && m.West != null ? m.West : -180,
543
+ East: m && m.East != null ? m.East : 180,
544
+ South: m && m.South != null ? m.South : -90,
545
+ North: m && m.North != null ? m.North : 90
546
+ };
547
+ }
548
+ /*
549
+ * Composite resolution, capped by what can be uploaded as a texture every frame.
550
+ */
551
+ compositeSize(tiles, target) {
552
+ let finest = Number.MAX_VALUE;
553
+ for (const tile of tiles) {
554
+ if (tile.TexelMetres > 0) {
555
+ finest = Math.min(finest, tile.TexelMetres);
556
+ }
557
+ }
558
+ if (!(finest > 0) || finest === Number.MAX_VALUE) {
559
+ finest = 1;
560
+ }
561
+ const mid = (target.South + target.North) / 2 * Math.PI / 180;
562
+ const widthM = Math.max((target.East - target.West) * 111320 * Math.cos(mid), 1);
563
+ const heightM = Math.max((target.North - target.South) * 110574, 1);
564
+ let width = Math.ceil(widthM / finest);
565
+ let height = Math.ceil(heightM / finest);
566
+ const longest = Math.max(width, height);
567
+ if (longest > MAX_COMPOSITE_TEXELS) {
568
+ const shrink = MAX_COMPOSITE_TEXELS / longest;
569
+ width = Math.max(1, Math.round(width * shrink));
570
+ height = Math.max(1, Math.round(height * shrink));
571
+ }
572
+ return { width: Math.max(1, width), height: Math.max(1, height) };
573
+ }
574
+ /*
575
+ * Applies the ramp, borders and baseline mask to raw value pixels, whatever produced them.
576
+ */
577
+ tint(imageData) {
578
+ const canvas = { width: imageData.width, height: imageData.height };
326
579
  // Copied before tinting: the ramp is a lerp between two colours, so the value cannot be
327
580
  // recovered from the tinted pixels afterwards.
328
581
  const valuePixels = this.produceValueCanvas ? new Uint8ClampedArray(imageData.data) : undefined;
329
- (0, image_utils_1.ApplyGrayscaleColorMask)(imageData, this.lowColor, this.highColor);
582
+ // Stops win when supplied: they can express a band and a hidden floor, which a two
583
+ // colour ramp cannot. The pair stays the fallback so an older style still draws.
584
+ if (this.rampStops && this.rampStops.length > 0) {
585
+ (0, image_utils_1.ApplyGradientStops)(imageData, this.rampStops);
586
+ }
587
+ else {
588
+ (0, image_utils_1.ApplyGrayscaleColorMask)(imageData, this.lowColor, this.highColor);
589
+ }
590
+ if (this.cellBorder) {
591
+ (0, image_utils_1.ApplyCellBorders)(imageData, this.cellBorder.cellTexels, this.cellBorder.widthTexels, this.cellBorder.color);
592
+ }
330
593
  this.applyBaselineMask(imageData.data, valuePixels, canvas.width, canvas.height);
331
594
  if (valuePixels) {
332
595
  dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES);
333
596
  }
334
597
  return { pixels: imageData.data, width: canvas.width, height: canvas.height, valuePixels };
335
598
  }
599
+ /*
600
+ * Paints one frame's pixels onto a canvas, resampled into the drape extent when there is one.
601
+ */
602
+ writeInto(canvas, pixels, dims) {
603
+ const rect = this.sourceRect(dims);
604
+ if (!rect) {
605
+ canvas.width = dims.width;
606
+ canvas.height = dims.height;
607
+ const direct = canvas.getContext("2d");
608
+ const image = direct.createImageData(dims.width, dims.height);
609
+ image.data.set(pixels);
610
+ direct.putImageData(image, 0, 0);
611
+ return;
612
+ }
613
+ if (!this.scratch) {
614
+ this.scratch = document.createElement("canvas");
615
+ }
616
+ this.scratch.width = dims.width;
617
+ this.scratch.height = dims.height;
618
+ const scratchCtx = this.scratch.getContext("2d");
619
+ const image = scratchCtx.createImageData(dims.width, dims.height);
620
+ image.data.set(pixels);
621
+ scratchCtx.putImageData(image, 0, 0);
622
+ canvas.width = dims.width;
623
+ canvas.height = dims.height;
624
+ const ctx = canvas.getContext("2d");
625
+ // Nearest neighbour: a value texel has to survive the resample exactly, or a label reads
626
+ // a blend of two cells rather than the cell it points at.
627
+ ctx.imageSmoothingEnabled = false;
628
+ ctx.clearRect(0, 0, dims.width, dims.height);
629
+ ctx.drawImage(this.scratch, rect.x, rect.y, rect.width, rect.height, 0, 0, dims.width, dims.height);
630
+ }
631
+ /*
632
+ * The part of the archive raster that the drape extent covers, in source pixels.
633
+ */
634
+ sourceRect(dims) {
635
+ const drape = this.drapeExtent;
636
+ const m = this.metadata;
637
+ // A composited frame was drawn into the drape extent already, so resampling it again
638
+ // would apply the same correction twice.
639
+ if (this.tiles) {
640
+ return null;
641
+ }
642
+ if (!drape || !m || m.West == null || m.East == null || m.South == null || m.North == null) {
643
+ return null;
644
+ }
645
+ const spanLon = m.East - m.West;
646
+ const spanLat = m.North - m.South;
647
+ if (!(spanLon > 0) || !(spanLat > 0)) {
648
+ return null;
649
+ }
650
+ const x = (drape.West - m.West) / spanLon * dims.width;
651
+ const width = (drape.East - drape.West) / spanLon * dims.width;
652
+ const y = (m.North - drape.North) / spanLat * dims.height;
653
+ const height = (drape.North - drape.South) / spanLat * dims.height;
654
+ // A drape that already matches the archive is the common case and must not pay for a
655
+ // resample, nor lose a half pixel to rounding.
656
+ if (Math.abs(x) < 0.01 && Math.abs(y) < 0.01
657
+ && Math.abs(width - dims.width) < 0.01 && Math.abs(height - dims.height) < 0.01) {
658
+ return null;
659
+ }
660
+ return { x, y, width, height };
661
+ }
662
+ /**
663
+ * The archive's per-cell floor and ceiling rasters, once EnsureExtremes has resolved.
664
+ */
665
+ GetExtremes() {
666
+ return { floor: this.floorPixels, ceiling: this.ceilingPixels };
667
+ }
668
+ /**
669
+ * Fetches and decodes the floor and ceiling rasters, at most once.
670
+ */
671
+ EnsureExtremes() {
672
+ if (this.extremesLoad) {
673
+ return this.extremesLoad;
674
+ }
675
+ if (this.tiles) {
676
+ this.extremesLoad = this.loadTiledExtremes();
677
+ return this.extremesLoad;
678
+ }
679
+ const floor = this.metadata && this.metadata.Floor;
680
+ const ceiling = this.metadata && this.metadata.Ceiling;
681
+ if (!floor && !ceiling) {
682
+ this.extremesLoad = Promise.resolve();
683
+ return this.extremesLoad;
684
+ }
685
+ this.extremesLoad = Promise.all([
686
+ floor ? this.loadImageDataAt(floor) : Promise.resolve(null),
687
+ ceiling ? this.loadImageDataAt(ceiling) : Promise.resolve(null)
688
+ ]).then(([f, c]) => {
689
+ if (this.disposed) {
690
+ return;
691
+ }
692
+ this.floorPixels = f;
693
+ this.ceilingPixels = c;
694
+ }).catch((e) => {
695
+ // Missing extremes cost a label its range, and must not take the animation with them.
696
+ console.warn("TextureFrameSeriesAnimator: could not load the extremes rasters.", e);
697
+ });
698
+ return this.extremesLoad;
699
+ }
700
+ /*
701
+ * Composites each tile's baseline mask into one covering the composited frames.
702
+ *
703
+ * A tiled archive publishes the mask per tile, so reading the archive level BaselineMask
704
+ * finds nothing and hiding dry cells silently does nothing at all.
705
+ */
706
+ async loadTiledBaselineMask() {
707
+ const tiles = this.tiles;
708
+ if (!tiles) {
709
+ return;
710
+ }
711
+ const entries = tiles.map((tile) => tile.BaselineMask);
712
+ let lo = Number.MAX_SAFE_INTEGER;
713
+ let hi = 0;
714
+ for (const at of entries) {
715
+ if (!at) {
716
+ continue;
717
+ }
718
+ lo = Math.min(lo, at.ByteOffset);
719
+ hi = Math.max(hi, at.ByteOffset + at.ByteLength);
720
+ }
721
+ if (!(hi > lo)) {
722
+ return;
723
+ }
724
+ try {
725
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
726
+ const block = await response.arrayBuffer();
727
+ if (this.disposed) {
728
+ return;
729
+ }
730
+ const images = await this.decodeSlices(block, lo, entries);
731
+ if (this.disposed) {
732
+ return;
733
+ }
734
+ const target = this.compositeExtent();
735
+ const size = this.compositeSize(tiles, target);
736
+ const canvas = document.createElement("canvas");
737
+ canvas.width = size.width;
738
+ canvas.height = size.height;
739
+ const ctx = canvas.getContext("2d");
740
+ ctx.imageSmoothingEnabled = false;
741
+ ctx.clearRect(0, 0, size.width, size.height);
742
+ for (let i = 0; i < tiles.length; i++) {
743
+ if (!images[i]) {
744
+ continue;
745
+ }
746
+ const at = this.tileRect(tiles[i], target, size);
747
+ ctx.drawImage(images[i], at.x, at.y, at.w, at.h);
748
+ }
749
+ const pixels = ctx.getImageData(0, 0, size.width, size.height).data;
750
+ const flags = new Uint8Array(size.width * size.height);
751
+ for (let p = 0; p < flags.length; p++) {
752
+ flags[p] = pixels[p * 4 + 3] >= 128 ? 1 : 0;
753
+ }
754
+ this.baselineFlags = flags;
755
+ this.baselineDims = { width: size.width, height: size.height };
756
+ }
757
+ catch (e) {
758
+ console.warn("TextureFrameSeriesAnimator: could not load the tiled baseline mask.", e);
759
+ }
760
+ }
761
+ /*
762
+ * Composites each tile's floor and ceiling into rasters matching the composited frames.
763
+ *
764
+ * A tiled archive has no whole-extent extremes to fetch, only per-tile ones, and a label
765
+ * indexes them by the same texel as the value canvas, so they have to be laid out the same way.
766
+ */
767
+ async loadTiledExtremes() {
768
+ const tiles = this.tiles;
769
+ if (!tiles) {
770
+ return;
771
+ }
772
+ const target = this.compositeExtent();
773
+ const size = this.compositeSize(tiles, target);
774
+ // One request for the whole extremes block. They sit contiguously after the frames, so
775
+ // fetching per tile would be 164 round trips for an 82 tile pyramid.
776
+ let lo = Number.MAX_SAFE_INTEGER;
777
+ let hi = 0;
778
+ for (const tile of tiles) {
779
+ for (const at of [tile.Floor, tile.Ceiling]) {
780
+ if (!at) {
781
+ continue;
782
+ }
783
+ lo = Math.min(lo, at.ByteOffset);
784
+ hi = Math.max(hi, at.ByteOffset + at.ByteLength);
785
+ }
786
+ }
787
+ if (!(hi > lo)) {
788
+ return;
789
+ }
790
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
791
+ const block = await response.arrayBuffer();
792
+ if (this.disposed) {
793
+ return;
794
+ }
795
+ const draw = async (pick) => {
796
+ const images = await this.decodeSlices(block, lo, tiles.map(pick));
797
+ if (this.disposed) {
798
+ return null;
799
+ }
800
+ const canvas = document.createElement("canvas");
801
+ canvas.width = size.width;
802
+ canvas.height = size.height;
803
+ const ctx = canvas.getContext("2d");
804
+ ctx.imageSmoothingEnabled = false;
805
+ ctx.clearRect(0, 0, size.width, size.height);
806
+ let drew = false;
807
+ for (let i = 0; i < tiles.length; i++) {
808
+ const image = images[i];
809
+ if (!image) {
810
+ continue;
811
+ }
812
+ const at = this.tileRect(tiles[i], target, size);
813
+ ctx.drawImage(image, at.x, at.y, at.w, at.h);
814
+ drew = true;
815
+ }
816
+ return drew ? ctx.getImageData(0, 0, size.width, size.height) : null;
817
+ };
818
+ try {
819
+ this.floorPixels = await draw((tile) => tile.Floor);
820
+ this.ceilingPixels = await draw((tile) => tile.Ceiling);
821
+ }
822
+ catch (e) {
823
+ console.warn("TextureFrameSeriesAnimator: could not load the tiled extremes.", e);
824
+ }
825
+ }
826
+ /*
827
+ * Where one tile lands on the composite, snapped to whole pixels.
828
+ *
829
+ * Rounding each edge rather than the origin and size is what makes neighbours meet exactly:
830
+ * a tile's right edge and the next tile's left edge round to the same pixel, so no seam of
831
+ * background shows between them.
832
+ */
833
+ tileRect(tile, target, size) {
834
+ const spanLon = target.East - target.West;
835
+ const spanLat = target.North - target.South;
836
+ const x0 = Math.round((tile.West - target.West) / spanLon * size.width);
837
+ const x1 = Math.round((tile.East - target.West) / spanLon * size.width);
838
+ const y0 = Math.round((target.North - tile.North) / spanLat * size.height);
839
+ const y1 = Math.round((target.North - tile.South) / spanLat * size.height);
840
+ return { x: x0, y: y0, w: Math.max(1, x1 - x0), h: Math.max(1, y1 - y0) };
841
+ }
842
+ /*
843
+ * Decodes many tile images at once.
844
+ *
845
+ * Serially awaiting each decode makes a frame cost as many round trips through the image
846
+ * decoder as there are tiles, which for an 82 tile pyramid is seconds rather than one.
847
+ */
848
+ decodeSlices(block, base, entries) {
849
+ return Promise.all(entries.map(async (at) => {
850
+ if (!at) {
851
+ return null;
852
+ }
853
+ const slice = block.slice(at.ByteOffset - base, at.ByteOffset - base + at.ByteLength);
854
+ const objectUrl = URL.createObjectURL(new Blob([slice], { type: "image/png" }));
855
+ try {
856
+ return await (0, image_utils_1.loadImage)(objectUrl);
857
+ }
858
+ catch {
859
+ return null;
860
+ }
861
+ finally {
862
+ URL.revokeObjectURL(objectUrl);
863
+ }
864
+ }));
865
+ }
866
+ /*
867
+ * Range GETs one entry and decodes it to an image.
868
+ */
869
+ async loadImageAt(entry) {
870
+ const start = entry.ByteOffset;
871
+ const end = entry.ByteOffset + entry.ByteLength - 1;
872
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${start}-${end}` } });
873
+ const buffer = await response.arrayBuffer();
874
+ const objectUrl = URL.createObjectURL(new Blob([buffer], { type: "image/png" }));
875
+ try {
876
+ return await (0, image_utils_1.loadImage)(objectUrl);
877
+ }
878
+ finally {
879
+ URL.revokeObjectURL(objectUrl);
880
+ }
881
+ }
882
+ /*
883
+ * Puts a static raster through the same resample the frames get.
884
+ */
885
+ remap(image) {
886
+ if (!image) {
887
+ return null;
888
+ }
889
+ const rect = this.sourceRect({ width: image.width, height: image.height });
890
+ if (!rect) {
891
+ return image;
892
+ }
893
+ const from = document.createElement("canvas");
894
+ from.width = image.width;
895
+ from.height = image.height;
896
+ from.getContext("2d").putImageData(image, 0, 0);
897
+ const to = document.createElement("canvas");
898
+ to.width = image.width;
899
+ to.height = image.height;
900
+ const ctx = to.getContext("2d");
901
+ ctx.imageSmoothingEnabled = false;
902
+ ctx.clearRect(0, 0, image.width, image.height);
903
+ ctx.drawImage(from, rect.x, rect.y, rect.width, rect.height, 0, 0, image.width, image.height);
904
+ return ctx.getImageData(0, 0, image.width, image.height);
905
+ }
906
+ /*
907
+ * Range GETs one entry out of the blob and decodes it to pixels.
908
+ */
909
+ async loadImageDataAt(entry) {
910
+ const start = entry.ByteOffset;
911
+ const end = entry.ByteOffset + entry.ByteLength - 1;
912
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${start}-${end}` } });
913
+ const buffer = await response.arrayBuffer();
914
+ const blob = new Blob([buffer], { type: "image/png" });
915
+ const objectUrl = URL.createObjectURL(blob);
916
+ let image;
917
+ try {
918
+ image = await (0, image_utils_1.loadImage)(objectUrl);
919
+ }
920
+ finally {
921
+ URL.revokeObjectURL(objectUrl);
922
+ }
923
+ const canvas = document.createElement("canvas");
924
+ canvas.width = image.width;
925
+ canvas.height = image.height;
926
+ const ctx = canvas.getContext("2d");
927
+ ctx.drawImage(image, 0, 0);
928
+ return ctx.getImageData(0, 0, canvas.width, canvas.height);
929
+ }
336
930
  /**
337
931
  * Fetches and decodes the archive's static baseline mask, at most once.
338
932
  */
339
933
  ensureBaselineMask() {
934
+ if (this.maskBaseline && this.tiles) {
935
+ if (!this.baselineLoad) {
936
+ this.baselineLoad = this.loadTiledBaselineMask();
937
+ }
938
+ return this.baselineLoad;
939
+ }
340
940
  if (!this.maskBaseline || !this.baselineMaskEntry) {
341
941
  return Promise.resolve();
342
942
  }
@@ -449,24 +1049,17 @@ var TextureFrameSeriesAnimator;
449
1049
  presentPixels(pixels, dims, valuePixels) {
450
1050
  this.poolIdx = 1 - this.poolIdx;
451
1051
  const target = this.pool[this.poolIdx];
452
- target.width = dims.width;
453
- target.height = dims.height;
454
- const ctx = target.getContext("2d");
455
- const imgData = ctx.createImageData(dims.width, dims.height);
456
- imgData.data.set(pixels);
457
- ctx.putImageData(imgData, 0, 0);
1052
+ this.writeInto(target, pixels, dims);
458
1053
  this.displayedPixels = pixels;
459
1054
  if (this.valueCanvas && valuePixels) {
460
- this.valueCanvas.width = dims.width;
461
- this.valueCanvas.height = dims.height;
462
- const valueCtx = this.valueCanvas.getContext("2d");
463
- const valueImgData = valueCtx.createImageData(dims.width, dims.height);
464
- valueImgData.data.set(valuePixels);
465
- valueCtx.putImageData(valueImgData, 0, 0);
1055
+ this.writeInto(this.valueCanvas, valuePixels, dims);
466
1056
  this.displayedValuePixels = valuePixels;
467
1057
  this.valueDims = dims;
468
1058
  }
469
1059
  this.presentVersion++;
1060
+ if (this.viewer && this.viewer.scene && this.viewer.scene.requestRenderMode) {
1061
+ this.viewer.scene.requestRender();
1062
+ }
470
1063
  }
471
1064
  }
472
1065
  TextureFrameSeriesAnimator.Animator = Animator;