bruce-cesium 7.2.2 → 7.2.4

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,229 @@ 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
+ * Whether anything was applied to the values on the way out of the source.
173
+ * @param metadata the archive's Data.generation
174
+ */
175
+ function IsTransformed(metadata) {
176
+ return Boolean(metadata && metadata.Transform);
177
+ }
178
+ TextureFrameSeriesAnimator.IsTransformed = IsTransformed;
179
+ /**
180
+ * A delivered value in the source's own terms, or unchanged when it cannot be turned back.
181
+ * @param map from SourceMap(), null when the archive does not say enough
182
+ */
183
+ function ToSource(map, value) {
184
+ return map ? value * map.scale + map.offset : value;
185
+ }
186
+ TextureFrameSeriesAnimator.ToSource = ToSource;
187
+ /**
188
+ * Whether the archive's frames carry a 16 bit value packed across R and G.
16
189
  *
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.
190
+ * Every consumer that reads a texel has to ask: taking R alone from a packed frame yields a
191
+ * plausible surface quantised to 255 steps of the full range rather than an obvious failure.
192
+ * @param metadata the archive's Data.generation
193
+ */
194
+ function IsPackedValue(metadata) {
195
+ return Boolean(metadata && metadata.Format === TextureFrameSeriesAnimator.FORMAT_RG16);
196
+ }
197
+ TextureFrameSeriesAnimator.IsPackedValue = IsPackedValue;
198
+ /**
199
+ * The normalised 0 to 1 value at a texel, from whichever format the archive packed it in.
19
200
  * @param metadata the archive's Data.generation
201
+ * @param pixels untinted RGBA value pixels
202
+ * @param at index of the texel's red channel
20
203
  */
21
- function GeneratorVersion(metadata) {
22
- if (!metadata || typeof metadata.GeneratorVersion !== "number") {
23
- return 1;
204
+ function NormalisedAt(metadata, pixels, at) {
205
+ if (IsPackedValue(metadata)) {
206
+ return (pixels[at] * 256 + pixels[at + 1]) / 65535;
24
207
  }
25
- return metadata.GeneratorVersion;
208
+ return pixels[at] / 255;
26
209
  }
27
- TextureFrameSeriesAnimator.GeneratorVersion = GeneratorVersion;
210
+ TextureFrameSeriesAnimator.NormalisedAt = NormalisedAt;
211
+ // Values already sit on the ellipsoid Cesium measures against, so nothing is added to place them.
212
+ TextureFrameSeriesAnimator.DATUM_ELLIPSOIDAL = "WGS84";
28
213
  /**
29
- * Whether an archive is an adaptive pyramid rather than one raster.
214
+ * Whether the values are heights against a vertical datum rather than a thickness.
215
+ *
216
+ * A thickness rises from wherever the polygon sits and a height already knows where it belongs, so
217
+ * this is what decides whether the polygon's own altitude may be added underneath it.
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 IsDatumHeight(metadata) {
221
+ return Boolean(metadata && metadata.SourceVerticalDatum);
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.IsDatumHeight = IsDatumHeight;
224
+ /**
225
+ * The archive's tiles, which Adapt() guarantees are present.
226
+ * @param metadata the archive's Data.generation
227
+ */
228
+ function TilesOf(metadata) {
229
+ return (metadata && metadata.Tiles) || [];
230
+ }
231
+ TextureFrameSeriesAnimator.TilesOf = TilesOf;
41
232
  /**
42
233
  * Turns a value in the attribute's own units into the 0 to 1 position the ramp is indexed by.
43
234
  *
@@ -56,44 +247,18 @@ var TextureFrameSeriesAnimator;
56
247
  return Math.min(1, Math.max(0, (value - lo) / span));
57
248
  }
58
249
  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
250
  /**
91
251
  * Detects whether a ClientFile's `Data.generation` metadata describes a frame archive rather than a single static image,
92
252
  * 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().
253
+ * @param data ClientFile.Data
94
254
  */
95
255
  function IsFrameArchiveMetadata(data) {
96
- return Boolean(data && data.generation && Array.isArray(data.generation.Frames) && data.generation.Frames.length > 0);
256
+ const generation = data && data.generation;
257
+ if (!generation) {
258
+ return false;
259
+ }
260
+ return (typeof generation.Version === "number" ||
261
+ (Array.isArray(generation.Frames) && generation.Frames.length > 0));
97
262
  }
98
263
  TextureFrameSeriesAnimator.IsFrameArchiveMetadata = IsFrameArchiveMetadata;
99
264
  // First resolvable static number out of a calculator field list, since that is all a border needs.
@@ -139,10 +304,27 @@ var TextureFrameSeriesAnimator;
139
304
  TextureFrameSeriesAnimator.AppearanceSignature = AppearanceSignature;
140
305
  const MAX_COMPOSITE_TEXELS = 2048;
141
306
  const VALUE_DILATE_PASSES = 2;
307
+ /*
308
+ * Rewrites a packed 16 bit value as the 8 bit grey the colour ramps index on.
309
+ *
310
+ * All three channels, since a ramp is free to read any of them and a leftover low byte in G would
311
+ * show up as a green cast on the one that reads them all.
312
+ */
313
+ function flattenPacked(pixels, packed) {
314
+ if (!packed) {
315
+ return;
316
+ }
317
+ for (let i = 0; i < pixels.length; i += 4) {
318
+ const grey = Math.round((pixels[i] * 256 + pixels[i + 1]) / 65535 * 255);
319
+ pixels[i] = grey;
320
+ pixels[i + 1] = grey;
321
+ pixels[i + 2] = grey;
322
+ }
323
+ }
142
324
  /*
143
325
  * Bleeds covered values outward into uncovered texels, leaving alpha untouched.
144
326
  */
145
- function dilateValues(value, width, height, passes) {
327
+ function dilateValues(value, width, height, passes, packed) {
146
328
  const texels = width * height;
147
329
  const filled = new Uint8Array(texels);
148
330
  for (let p = 0; p < texels; p++) {
@@ -166,15 +348,25 @@ var TextureFrameSeriesAnimator;
166
348
  if (nx < 0 || ny < 0 || nx >= width || ny >= height || !wasFilled[ny * width + nx]) {
167
349
  continue;
168
350
  }
169
- sum += source[(ny * width + nx) * 4];
351
+ const at = (ny * width + nx) * 4;
352
+ // Averaged as a value rather than per byte: averaging a packed low byte
353
+ // on its own wraps at every 256th step and speckles the bled edge.
354
+ sum += packed ? source[at] * 256 + source[at + 1] : source[at];
170
355
  hits++;
171
356
  }
172
357
  }
173
358
  if (hits > 0) {
174
359
  const v = Math.round(sum / hits);
175
- value[p * 4] = v;
176
- value[p * 4 + 1] = v;
177
- value[p * 4 + 2] = v;
360
+ if (packed) {
361
+ value[p * 4] = (v >> 8) & 255;
362
+ value[p * 4 + 1] = v & 255;
363
+ value[p * 4 + 2] = 0;
364
+ }
365
+ else {
366
+ value[p * 4] = v;
367
+ value[p * 4 + 1] = v;
368
+ value[p * 4 + 2] = v;
369
+ }
178
370
  filled[p] = 1;
179
371
  }
180
372
  }
@@ -212,7 +404,6 @@ var TextureFrameSeriesAnimator;
212
404
  // forcing Cesium to re-upload the texture only when something changed.
213
405
  this.pool = [document.createElement("canvas"), document.createElement("canvas")];
214
406
  this.poolIdx = 0;
215
- this.scratch = null;
216
407
  this.valueDims = null;
217
408
  this.presentVersion = 0;
218
409
  this.extremesLoad = null;
@@ -235,7 +426,7 @@ var TextureFrameSeriesAnimator;
235
426
  this.highColor = (mask && bruce_models_1.Color.ColorFromStr(mask.maxColor)) || DEFAULT_HIGH_COLOR;
236
427
  // Positions are authored in the attribute's units and normalised once here, so the
237
428
  // 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;
429
+ const points = mask && mask.points;
239
430
  this.rampStops = (points && points.length > 0)
240
431
  ? points.map((p) => ({
241
432
  position: NormalisePosition(options.metadata, p.position),
@@ -261,9 +452,8 @@ var TextureFrameSeriesAnimator;
261
452
  this.produceValueCanvas = Boolean(options.produceValueCanvas);
262
453
  this.metadata = options.metadata;
263
454
  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);
455
+ this.tiles = TilesOf(options.metadata);
456
+ this.maskBaseline = (_c = options.maskBaseline) !== null && _c !== void 0 ? _c : this.tiles.some((tile) => Boolean(tile.BaselineMask));
267
457
  this.valueCanvas = this.produceValueCanvas ? document.createElement("canvas") : null;
268
458
  this.driveMaterial = options.driveMaterial !== false;
269
459
  this.originalMaterial = options.entity.polygon.material;
@@ -439,32 +629,20 @@ var TextureFrameSeriesAnimator;
439
629
  }
440
630
  async fetchAndTint(idx) {
441
631
  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);
632
+ // Every tile's frame for one timestep sits contiguously, because the generator writes
633
+ // frame major. So a tiled frame is still ONE range request, not one per tile.
634
+ let lo = Number.MAX_SAFE_INTEGER;
635
+ let hi = 0;
636
+ for (const tile of tiles) {
637
+ const at = tile.Frames[idx];
638
+ if (!at) {
639
+ continue;
456
640
  }
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();
641
+ lo = Math.min(lo, at.ByteOffset);
642
+ hi = Math.max(hi, at.ByteOffset + at.ByteLength);
467
643
  }
644
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
645
+ const buffer = await response.arrayBuffer();
468
646
  if (this.disposed) {
469
647
  return;
470
648
  }
@@ -474,9 +652,7 @@ var TextureFrameSeriesAnimator;
474
652
  if (this.disposed) {
475
653
  return;
476
654
  }
477
- const decoded = tiles
478
- ? await this.composeTiles(tiles, idx, buffer, span)
479
- : await this.decodeAndTint(buffer);
655
+ const decoded = await this.composeTiles(tiles, idx, buffer, lo);
480
656
  if (this.disposed) {
481
657
  return;
482
658
  }
@@ -487,26 +663,6 @@ var TextureFrameSeriesAnimator;
487
663
  this.beginCrossfadeTo(idx);
488
664
  }
489
665
  }
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
666
  /*
511
667
  * Draws every tile of one frame into a single raster covering the drape extent.
512
668
  */
@@ -579,6 +735,10 @@ var TextureFrameSeriesAnimator;
579
735
  // Copied before tinting: the ramp is a lerp between two colours, so the value cannot be
580
736
  // recovered from the tinted pixels afterwards.
581
737
  const valuePixels = this.produceValueCanvas ? new Uint8ClampedArray(imageData.data) : undefined;
738
+ // The ramp indexes on R as an 8 bit grey, so a packed frame is flattened to one here
739
+ // rather than teaching every ramp function a second format.
740
+ // The copy above keeps the packed pair, which is what displacement and labels read.
741
+ flattenPacked(imageData.data, IsPackedValue(this.metadata));
582
742
  // Stops win when supplied: they can express a band and a hidden floor, which a two
583
743
  // colour ramp cannot. The pair stays the fallback so an older style still draws.
584
744
  if (this.rampStops && this.rampStops.length > 0) {
@@ -592,72 +752,20 @@ var TextureFrameSeriesAnimator;
592
752
  }
593
753
  this.applyBaselineMask(imageData.data, valuePixels, canvas.width, canvas.height);
594
754
  if (valuePixels) {
595
- dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES);
755
+ dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES, IsPackedValue(this.metadata));
596
756
  }
597
757
  return { pixels: imageData.data, width: canvas.width, height: canvas.height, valuePixels };
598
758
  }
599
759
  /*
600
- * Paints one frame's pixels onto a canvas, resampled into the drape extent when there is one.
760
+ * Paints one frame's composited pixels onto a canvas.
601
761
  */
602
762
  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
763
  canvas.width = dims.width;
623
764
  canvas.height = dims.height;
624
765
  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 };
766
+ const image = ctx.createImageData(dims.width, dims.height);
767
+ image.data.set(pixels);
768
+ ctx.putImageData(image, 0, 0);
661
769
  }
662
770
  /**
663
771
  * The archive's per-cell floor and ceiling rasters, once EnsureExtremes has resolved.
@@ -669,45 +777,16 @@ var TextureFrameSeriesAnimator;
669
777
  * Fetches and decodes the floor and ceiling rasters, at most once.
670
778
  */
671
779
  EnsureExtremes() {
672
- if (this.extremesLoad) {
673
- return this.extremesLoad;
674
- }
675
- if (this.tiles) {
780
+ if (!this.extremesLoad) {
676
781
  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
782
  }
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
783
  return this.extremesLoad;
699
784
  }
700
785
  /*
701
786
  * 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
787
  */
706
788
  async loadTiledBaselineMask() {
707
789
  const tiles = this.tiles;
708
- if (!tiles) {
709
- return;
710
- }
711
790
  const entries = tiles.map((tile) => tile.BaselineMask);
712
791
  let lo = Number.MAX_SAFE_INTEGER;
713
792
  let hi = 0;
@@ -761,14 +840,11 @@ var TextureFrameSeriesAnimator;
761
840
  /*
762
841
  * Composites each tile's floor and ceiling into rasters matching the composited frames.
763
842
  *
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.
843
+ * A label indexes them by the same texel as the value canvas, so they have to be laid out the
844
+ * same way rather than fetched as whole-extent rasters.
766
845
  */
767
846
  async loadTiledExtremes() {
768
847
  const tiles = this.tiles;
769
- if (!tiles) {
770
- return;
771
- }
772
848
  const target = this.compositeExtent();
773
849
  const size = this.compositeSize(tiles, target);
774
850
  // One request for the whole extremes block. They sit contiguously after the frames, so
@@ -863,70 +939,6 @@ var TextureFrameSeriesAnimator;
863
939
  }
864
940
  }));
865
941
  }
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
942
  /**
931
943
  * Fetches and decodes the archive's static baseline mask, at most once.
932
944
  */