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.
@@ -6,38 +6,221 @@ 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
9
  TextureFrameSeriesAnimator.LAYOUT_TILES = "tiles";
10
+ // The only generation this renderer draws. Anything else is refused rather than half read: the
11
+ // shapes differ enough that a partial reading produces a plausible wrong surface, which is worse
12
+ // than an empty one.
13
+ TextureFrameSeriesAnimator.SUPPORTED_VERSION = 3;
14
+ // Value in R at 8 bits, or high in R and low in G at 16.
15
+ TextureFrameSeriesAnimator.FORMAT_U8 = "u8";
16
+ TextureFrameSeriesAnimator.FORMAT_RG16 = "rg16";
17
+ // Declared as a field with one value, so a second mode can be added later without an absent
18
+ // Encoding having to mean anything.
19
+ TextureFrameSeriesAnimator.ENCODING_ABSOLUTE = "absolute";
20
+ TextureFrameSeriesAnimator.LAYER_VALUE = "Value";
21
+ TextureFrameSeriesAnimator.LAYER_VALUE_MIN = "ValueMin";
22
+ TextureFrameSeriesAnimator.LAYER_VALUE_MAX = "ValueMax";
23
+ TextureFrameSeriesAnimator.LAYER_BASELINE = "Baseline";
24
+ // The per-texel correction that was applied, published only on an archive that was transformed.
25
+ TextureFrameSeriesAnimator.LAYER_VALUE_SHIFT = "ValueShift";
14
26
  /**
15
- * The generator that produced an archive, for telling a stale one from a current one.
27
+ * Turns a published value map into the flat shape this renderer works in, or null when it cannot be drawn.
28
+ * @param published the archive's Data.generation
29
+ */
30
+ function Adapt(published) {
31
+ if (!published || published.Version !== TextureFrameSeriesAnimator.SUPPORTED_VERSION) {
32
+ const found = published && published.Version != null ? published.Version : "none";
33
+ const at = published && published.Identity ? published.Identity.Generated : undefined;
34
+ // In theory should be no files.
35
+ console.warn("bruce-cesium: value map version " + found + " is not supported (this build reads "
36
+ + TextureFrameSeriesAnimator.SUPPORTED_VERSION + "), so nothing will be drawn for it."
37
+ + (at ? " The archive was generated at " + at + " and needs regenerating." : ""));
38
+ return null;
39
+ }
40
+ const geometry = published.Geometry || {};
41
+ const tiles = published.Tiles || [];
42
+ if (geometry.Layout !== TextureFrameSeriesAnimator.LAYOUT_TILES || tiles.length === 0) {
43
+ const at = published.Identity ? published.Identity.Generated : undefined;
44
+ console.warn("bruce-cesium: value map declares no tiles, so nothing will be drawn for it."
45
+ + (at ? " The archive was generated at " + at + " and needs regenerating." : ""));
46
+ return null;
47
+ }
48
+ const value = published.Value || {};
49
+ if (value.Encoding && value.Encoding !== TextureFrameSeriesAnimator.ENCODING_ABSOLUTE) {
50
+ const at = published.Identity ? published.Identity.Generated : undefined;
51
+ console.warn("bruce-cesium: value map encoding " + value.Encoding + " is not supported"
52
+ + " (this build reads " + TextureFrameSeriesAnimator.ENCODING_ABSOLUTE + "), so nothing will be drawn for it."
53
+ + (at ? " The archive was generated at " + at + " and needs regenerating." : ""));
54
+ return null;
55
+ }
56
+ const series = published.Series || {};
57
+ const layerRange = declaredRange(published, TextureFrameSeriesAnimator.LAYER_VALUE_MIN);
58
+ const flat = {
59
+ Frames: [],
60
+ ValueMin: value.Min,
61
+ ValueMax: value.Max,
62
+ Format: value.Format || TextureFrameSeriesAnimator.FORMAT_U8,
63
+ Encoding: value.Encoding || TextureFrameSeriesAnimator.ENCODING_ABSOLUTE,
64
+ Units: value.Units,
65
+ West: geometry.West,
66
+ East: geometry.East,
67
+ South: geometry.South,
68
+ North: geometry.North,
69
+ ResolutionX: geometry.ResolutionX,
70
+ ResolutionY: geometry.ResolutionY,
71
+ PixelPlacement: geometry.PixelPlacement,
72
+ Times: series.Times,
73
+ ExtremesValueMin: layerRange ? layerRange.min : undefined,
74
+ ExtremesValueMax: layerRange ? layerRange.max : undefined,
75
+ // The delivered values are what gets drawn. What the source called them, and any shift
76
+ // applied on the way, travel for labels and analysis rather than for placement.
77
+ SourceVerticalDatum: published.ValueSource ? published.ValueSource.VerticalDatum : undefined,
78
+ Transform: published.ValueSource ? published.ValueSource.Transform : undefined,
79
+ SourceUnits: published.ValueSource ? published.ValueSource.Units : undefined,
80
+ SourceValueMin: published.ValueSource ? published.ValueSource.Min : undefined,
81
+ SourceValueMax: published.ValueSource ? published.ValueSource.Max : undefined
82
+ };
83
+ flat.Tiles = tiles.map((tile) => adaptTile(tile, series.Times));
84
+ // The coarsest tile covers the whole extent, so it is what a caller wanting one time list reads.
85
+ flat.Frames = flat.Tiles[0].Frames;
86
+ warnIfUnshifted(published, flat);
87
+ return flat;
88
+ }
89
+ TextureFrameSeriesAnimator.Adapt = Adapt;
90
+ /*
91
+ * Says so when an archive's heights are not on the ellipsoid they will be drawn against.
92
+ */
93
+ function warnIfUnshifted(published, flat) {
94
+ const datum = flat.SourceVerticalDatum;
95
+ if (!datum || datum === TextureFrameSeriesAnimator.DATUM_ELLIPSOIDAL || flat.Transform) {
96
+ return;
97
+ }
98
+ const at = published.Identity ? published.Identity.Generated : undefined;
99
+ console.warn("bruce-cesium: value map heights are in " + datum + " and report no transform, so"
100
+ + " they are not ellipsoidal and will draw at the wrong height when placed absolutely."
101
+ + (at ? " The archive was generated at " + at + " and needs regenerating with the shift"
102
+ + " applied." : ""));
103
+ }
104
+ /*
105
+ * The Min and Max a named layer declares, since a statistics layer is encoded over the true data
106
+ * range rather than over the frames' own.
107
+ */
108
+ function declaredRange(published, name) {
109
+ const layers = published.Layers || [];
110
+ for (const layer of layers) {
111
+ if (layer.Name === name && typeof layer.Min === "number" && typeof layer.Max === "number") {
112
+ return { min: layer.Min, max: layer.Max };
113
+ }
114
+ }
115
+ return null;
116
+ }
117
+ /*
118
+ * One tile, with its per-time value rasters lined up against the shared time list.
119
+ */
120
+ function adaptTile(tile, times) {
121
+ const layers = tile.Layers || {};
122
+ const perTime = layers[TextureFrameSeriesAnimator.LAYER_VALUE] || [];
123
+ const frames = perTime.map((entry, index) => ({
124
+ Timestamp: times && times[index] ? times[index] : "",
125
+ ByteOffset: entry.Bytes.Offset,
126
+ ByteLength: entry.Bytes.Length
127
+ }));
128
+ return {
129
+ Level: tile.Level,
130
+ X: tile.X,
131
+ Y: tile.Y,
132
+ West: tile.West,
133
+ East: tile.East,
134
+ South: tile.South,
135
+ North: tile.North,
136
+ TexelMetres: tile.TexelMetres,
137
+ ResolutionX: tile.ResolutionX,
138
+ ResolutionY: tile.ResolutionY,
139
+ Frames: frames,
140
+ Floor: single(layers[TextureFrameSeriesAnimator.LAYER_VALUE_MIN]),
141
+ Ceiling: single(layers[TextureFrameSeriesAnimator.LAYER_VALUE_MAX]),
142
+ BaselineMask: single(layers[TextureFrameSeriesAnimator.LAYER_BASELINE])
143
+ };
144
+ }
145
+ function single(layer) {
146
+ if (!layer || !layer.Bytes) {
147
+ return undefined;
148
+ }
149
+ return { ByteOffset: layer.Bytes.Offset, ByteLength: layer.Bytes.Length };
150
+ }
151
+ /**
152
+ * How to turn a delivered value into the number the source published, or null when the archive
153
+ * does not say enough to do it.
154
+ * @param metadata the archive's Data.generation
155
+ */
156
+ function SourceMap(metadata) {
157
+ if (!metadata) {
158
+ return null;
159
+ }
160
+ const { ValueMin, ValueMax, SourceValueMin, SourceValueMax } = metadata;
161
+ if (typeof ValueMin !== "number" || typeof ValueMax !== "number"
162
+ || typeof SourceValueMin !== "number" || typeof SourceValueMax !== "number") {
163
+ return null;
164
+ }
165
+ const delivered = ValueMax - ValueMin;
166
+ // A single delivered value describes no line, so all that can be recovered is where it sits.
167
+ const scale = delivered !== 0 ? (SourceValueMax - SourceValueMin) / delivered : 1;
168
+ return { scale, offset: SourceValueMin - ValueMin * scale };
169
+ }
170
+ TextureFrameSeriesAnimator.SourceMap = SourceMap;
171
+ /**
172
+ * A delivered value in the source's own terms, or unchanged when it cannot be turned back.
173
+ * @param map from SourceMap(), null when the archive does not say enough
174
+ */
175
+ function ToSource(map, value) {
176
+ return map ? value * map.scale + map.offset : value;
177
+ }
178
+ TextureFrameSeriesAnimator.ToSource = ToSource;
179
+ /**
180
+ * Whether the archive's frames carry a 16 bit value packed across R and G.
16
181
  *
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.
182
+ * Every consumer that reads a texel has to ask: taking R alone from a packed frame yields a
183
+ * plausible surface quantised to 255 steps of the full range rather than an obvious failure.
19
184
  * @param metadata the archive's Data.generation
20
185
  */
21
- function GeneratorVersion(metadata) {
22
- if (!metadata || typeof metadata.GeneratorVersion !== "number") {
23
- return 1;
186
+ function IsPackedValue(metadata) {
187
+ return Boolean(metadata && metadata.Format === TextureFrameSeriesAnimator.FORMAT_RG16);
188
+ }
189
+ TextureFrameSeriesAnimator.IsPackedValue = IsPackedValue;
190
+ /**
191
+ * The normalised 0 to 1 value at a texel, from whichever format the archive packed it in.
192
+ * @param metadata the archive's Data.generation
193
+ * @param pixels untinted RGBA value pixels
194
+ * @param at index of the texel's red channel
195
+ */
196
+ function NormalisedAt(metadata, pixels, at) {
197
+ if (IsPackedValue(metadata)) {
198
+ return (pixels[at] * 256 + pixels[at + 1]) / 65535;
24
199
  }
25
- return metadata.GeneratorVersion;
200
+ return pixels[at] / 255;
201
+ }
202
+ TextureFrameSeriesAnimator.NormalisedAt = NormalisedAt;
203
+ // Values already sit on the ellipsoid Cesium measures against, so nothing is added to place them.
204
+ TextureFrameSeriesAnimator.DATUM_ELLIPSOIDAL = "WGS84";
205
+ /**
206
+ * Whether the values are heights against a vertical datum rather than a thickness.
207
+ *
208
+ * A thickness rises from wherever the polygon sits and a height already knows where it belongs, so
209
+ * this is what decides whether the polygon's own altitude may be added underneath it.
210
+ * @param metadata the archive's Data.generation
211
+ */
212
+ function IsDatumHeight(metadata) {
213
+ return Boolean(metadata && metadata.SourceVerticalDatum);
26
214
  }
27
- TextureFrameSeriesAnimator.GeneratorVersion = GeneratorVersion;
215
+ TextureFrameSeriesAnimator.IsDatumHeight = IsDatumHeight;
28
216
  /**
29
- * Whether an archive is an adaptive pyramid rather than one raster.
217
+ * The archive's tiles, which Adapt() guarantees are present.
30
218
  * @param metadata the archive's Data.generation
31
219
  */
32
- function IsTiledLayout(metadata) {
33
- return (Boolean(metadata && metadata.Layout === TextureFrameSeriesAnimator.LAYOUT_TILES &&
34
- metadata.Tiles && metadata.Tiles.length > 0));
220
+ function TilesOf(metadata) {
221
+ return (metadata && metadata.Tiles) || [];
35
222
  }
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";
223
+ TextureFrameSeriesAnimator.TilesOf = TilesOf;
41
224
  /**
42
225
  * Turns a value in the attribute's own units into the 0 to 1 position the ramp is indexed by.
43
226
  *
@@ -56,44 +239,18 @@ var TextureFrameSeriesAnimator;
56
239
  return Math.min(1, Math.max(0, (value - lo) / span));
57
240
  }
58
241
  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;
90
242
  /**
91
243
  * Detects whether a ClientFile's `Data.generation` metadata describes a frame archive rather than a single static image,
92
244
  * so a caller can decide whether to construct an Animator or fall back to the existing static-texture path.
93
- * @param data ClientFile.Data (the `IFile.Data` field), as returned by ClientFile.Get().
245
+ * @param data ClientFile.Data
94
246
  */
95
247
  function IsFrameArchiveMetadata(data) {
96
- return Boolean(data && data.generation && Array.isArray(data.generation.Frames) && data.generation.Frames.length > 0);
248
+ const generation = data && data.generation;
249
+ if (!generation) {
250
+ return false;
251
+ }
252
+ return (typeof generation.Version === "number" ||
253
+ (Array.isArray(generation.Frames) && generation.Frames.length > 0));
97
254
  }
98
255
  TextureFrameSeriesAnimator.IsFrameArchiveMetadata = IsFrameArchiveMetadata;
99
256
  // First resolvable static number out of a calculator field list, since that is all a border needs.
@@ -139,10 +296,27 @@ var TextureFrameSeriesAnimator;
139
296
  TextureFrameSeriesAnimator.AppearanceSignature = AppearanceSignature;
140
297
  const MAX_COMPOSITE_TEXELS = 2048;
141
298
  const VALUE_DILATE_PASSES = 2;
299
+ /*
300
+ * Rewrites a packed 16 bit value as the 8 bit grey the colour ramps index on.
301
+ *
302
+ * All three channels, since a ramp is free to read any of them and a leftover low byte in G would
303
+ * show up as a green cast on the one that reads them all.
304
+ */
305
+ function flattenPacked(pixels, packed) {
306
+ if (!packed) {
307
+ return;
308
+ }
309
+ for (let i = 0; i < pixels.length; i += 4) {
310
+ const grey = Math.round((pixels[i] * 256 + pixels[i + 1]) / 65535 * 255);
311
+ pixels[i] = grey;
312
+ pixels[i + 1] = grey;
313
+ pixels[i + 2] = grey;
314
+ }
315
+ }
142
316
  /*
143
317
  * Bleeds covered values outward into uncovered texels, leaving alpha untouched.
144
318
  */
145
- function dilateValues(value, width, height, passes) {
319
+ function dilateValues(value, width, height, passes, packed) {
146
320
  const texels = width * height;
147
321
  const filled = new Uint8Array(texels);
148
322
  for (let p = 0; p < texels; p++) {
@@ -166,15 +340,25 @@ var TextureFrameSeriesAnimator;
166
340
  if (nx < 0 || ny < 0 || nx >= width || ny >= height || !wasFilled[ny * width + nx]) {
167
341
  continue;
168
342
  }
169
- sum += source[(ny * width + nx) * 4];
343
+ const at = (ny * width + nx) * 4;
344
+ // Averaged as a value rather than per byte: averaging a packed low byte
345
+ // on its own wraps at every 256th step and speckles the bled edge.
346
+ sum += packed ? source[at] * 256 + source[at + 1] : source[at];
170
347
  hits++;
171
348
  }
172
349
  }
173
350
  if (hits > 0) {
174
351
  const v = Math.round(sum / hits);
175
- value[p * 4] = v;
176
- value[p * 4 + 1] = v;
177
- value[p * 4 + 2] = v;
352
+ if (packed) {
353
+ value[p * 4] = (v >> 8) & 255;
354
+ value[p * 4 + 1] = v & 255;
355
+ value[p * 4 + 2] = 0;
356
+ }
357
+ else {
358
+ value[p * 4] = v;
359
+ value[p * 4 + 1] = v;
360
+ value[p * 4 + 2] = v;
361
+ }
178
362
  filled[p] = 1;
179
363
  }
180
364
  }
@@ -212,7 +396,6 @@ var TextureFrameSeriesAnimator;
212
396
  // forcing Cesium to re-upload the texture only when something changed.
213
397
  this.pool = [document.createElement("canvas"), document.createElement("canvas")];
214
398
  this.poolIdx = 0;
215
- this.scratch = null;
216
399
  this.valueDims = null;
217
400
  this.presentVersion = 0;
218
401
  this.extremesLoad = null;
@@ -235,7 +418,7 @@ var TextureFrameSeriesAnimator;
235
418
  this.highColor = (mask && bruce_models_1.Color.ColorFromStr(mask.maxColor)) || DEFAULT_HIGH_COLOR;
236
419
  // Positions are authored in the attribute's units and normalised once here, so the
237
420
  // 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;
421
+ const points = mask && mask.points;
239
422
  this.rampStops = (points && points.length > 0)
240
423
  ? points.map((p) => ({
241
424
  position: NormalisePosition(options.metadata, p.position),
@@ -261,9 +444,8 @@ var TextureFrameSeriesAnimator;
261
444
  this.produceValueCanvas = Boolean(options.produceValueCanvas);
262
445
  this.metadata = options.metadata;
263
446
  this.drapeExtent = options.drapeExtent;
264
- this.tiles = IsTiledLayout(options.metadata) ? options.metadata.Tiles : undefined;
265
- this.baselineMaskEntry = options.baselineMask || null;
266
- this.maskBaseline = (_c = options.maskBaseline) !== null && _c !== void 0 ? _c : Boolean(options.baselineMask);
447
+ this.tiles = TilesOf(options.metadata);
448
+ this.maskBaseline = (_c = options.maskBaseline) !== null && _c !== void 0 ? _c : this.tiles.some((tile) => Boolean(tile.BaselineMask));
267
449
  this.valueCanvas = this.produceValueCanvas ? document.createElement("canvas") : null;
268
450
  this.driveMaterial = options.driveMaterial !== false;
269
451
  this.originalMaterial = options.entity.polygon.material;
@@ -439,32 +621,20 @@ var TextureFrameSeriesAnimator;
439
621
  }
440
622
  async fetchAndTint(idx) {
441
623
  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);
624
+ // Every tile's frame for one timestep sits contiguously, because the generator writes
625
+ // frame major. So a tiled frame is still ONE range request, not one per tile.
626
+ let lo = Number.MAX_SAFE_INTEGER;
627
+ let hi = 0;
628
+ for (const tile of tiles) {
629
+ const at = tile.Frames[idx];
630
+ if (!at) {
631
+ continue;
456
632
  }
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();
633
+ lo = Math.min(lo, at.ByteOffset);
634
+ hi = Math.max(hi, at.ByteOffset + at.ByteLength);
467
635
  }
636
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
637
+ const buffer = await response.arrayBuffer();
468
638
  if (this.disposed) {
469
639
  return;
470
640
  }
@@ -474,9 +644,7 @@ var TextureFrameSeriesAnimator;
474
644
  if (this.disposed) {
475
645
  return;
476
646
  }
477
- const decoded = tiles
478
- ? await this.composeTiles(tiles, idx, buffer, span)
479
- : await this.decodeAndTint(buffer);
647
+ const decoded = await this.composeTiles(tiles, idx, buffer, lo);
480
648
  if (this.disposed) {
481
649
  return;
482
650
  }
@@ -487,26 +655,6 @@ var TextureFrameSeriesAnimator;
487
655
  this.beginCrossfadeTo(idx);
488
656
  }
489
657
  }
490
- /**
491
- * Decodes one frame's raw PNG bytes (as returned by a Range GET) and applies the grayscale color mask.
492
- */
493
- async decodeAndTint(buffer) {
494
- const blob = new Blob([buffer], { type: "image/png" });
495
- const objectUrl = URL.createObjectURL(blob);
496
- let image;
497
- try {
498
- image = await (0, image_utils_1.loadImage)(objectUrl);
499
- }
500
- finally {
501
- URL.revokeObjectURL(objectUrl);
502
- }
503
- const canvas = document.createElement("canvas");
504
- canvas.width = image.width;
505
- canvas.height = image.height;
506
- const ctx = canvas.getContext("2d");
507
- ctx.drawImage(image, 0, 0);
508
- return this.tint(ctx.getImageData(0, 0, canvas.width, canvas.height));
509
- }
510
658
  /*
511
659
  * Draws every tile of one frame into a single raster covering the drape extent.
512
660
  */
@@ -579,6 +727,10 @@ var TextureFrameSeriesAnimator;
579
727
  // Copied before tinting: the ramp is a lerp between two colours, so the value cannot be
580
728
  // recovered from the tinted pixels afterwards.
581
729
  const valuePixels = this.produceValueCanvas ? new Uint8ClampedArray(imageData.data) : undefined;
730
+ // The ramp indexes on R as an 8 bit grey, so a packed frame is flattened to one here
731
+ // rather than teaching every ramp function a second format.
732
+ // The copy above keeps the packed pair, which is what displacement and labels read.
733
+ flattenPacked(imageData.data, IsPackedValue(this.metadata));
582
734
  // Stops win when supplied: they can express a band and a hidden floor, which a two
583
735
  // colour ramp cannot. The pair stays the fallback so an older style still draws.
584
736
  if (this.rampStops && this.rampStops.length > 0) {
@@ -592,72 +744,20 @@ var TextureFrameSeriesAnimator;
592
744
  }
593
745
  this.applyBaselineMask(imageData.data, valuePixels, canvas.width, canvas.height);
594
746
  if (valuePixels) {
595
- dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES);
747
+ dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES, IsPackedValue(this.metadata));
596
748
  }
597
749
  return { pixels: imageData.data, width: canvas.width, height: canvas.height, valuePixels };
598
750
  }
599
751
  /*
600
- * Paints one frame's pixels onto a canvas, resampled into the drape extent when there is one.
752
+ * Paints one frame's composited pixels onto a canvas.
601
753
  */
602
754
  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
755
  canvas.width = dims.width;
623
756
  canvas.height = dims.height;
624
757
  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 };
758
+ const image = ctx.createImageData(dims.width, dims.height);
759
+ image.data.set(pixels);
760
+ ctx.putImageData(image, 0, 0);
661
761
  }
662
762
  /**
663
763
  * The archive's per-cell floor and ceiling rasters, once EnsureExtremes has resolved.
@@ -669,45 +769,16 @@ var TextureFrameSeriesAnimator;
669
769
  * Fetches and decodes the floor and ceiling rasters, at most once.
670
770
  */
671
771
  EnsureExtremes() {
672
- if (this.extremesLoad) {
673
- return this.extremesLoad;
674
- }
675
- if (this.tiles) {
772
+ if (!this.extremesLoad) {
676
773
  this.extremesLoad = this.loadTiledExtremes();
677
- return this.extremesLoad;
678
774
  }
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
775
  return this.extremesLoad;
699
776
  }
700
777
  /*
701
778
  * 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
779
  */
706
780
  async loadTiledBaselineMask() {
707
781
  const tiles = this.tiles;
708
- if (!tiles) {
709
- return;
710
- }
711
782
  const entries = tiles.map((tile) => tile.BaselineMask);
712
783
  let lo = Number.MAX_SAFE_INTEGER;
713
784
  let hi = 0;
@@ -761,14 +832,11 @@ var TextureFrameSeriesAnimator;
761
832
  /*
762
833
  * Composites each tile's floor and ceiling into rasters matching the composited frames.
763
834
  *
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.
835
+ * A label indexes them by the same texel as the value canvas, so they have to be laid out the
836
+ * same way rather than fetched as whole-extent rasters.
766
837
  */
767
838
  async loadTiledExtremes() {
768
839
  const tiles = this.tiles;
769
- if (!tiles) {
770
- return;
771
- }
772
840
  const target = this.compositeExtent();
773
841
  const size = this.compositeSize(tiles, target);
774
842
  // One request for the whole extremes block. They sit contiguously after the frames, so
@@ -863,70 +931,6 @@ var TextureFrameSeriesAnimator;
863
931
  }
864
932
  }));
865
933
  }
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
- }
930
934
  /**
931
935
  * Fetches and decodes the archive's static baseline mask, at most once.
932
936
  */