bruce-cesium 7.2.1 → 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,26 +6,317 @@ const Cesium = require("cesium");
6
6
  const image_utils_1 = require("../internal/image-utils");
7
7
  var TextureFrameSeriesAnimator;
8
8
  (function (TextureFrameSeriesAnimator) {
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";
26
+ /**
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.
181
+ *
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.
184
+ * @param metadata the archive's Data.generation
185
+ */
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;
199
+ }
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);
214
+ }
215
+ TextureFrameSeriesAnimator.IsDatumHeight = IsDatumHeight;
216
+ /**
217
+ * The archive's tiles, which Adapt() guarantees are present.
218
+ * @param metadata the archive's Data.generation
219
+ */
220
+ function TilesOf(metadata) {
221
+ return (metadata && metadata.Tiles) || [];
222
+ }
223
+ TextureFrameSeriesAnimator.TilesOf = TilesOf;
224
+ /**
225
+ * Turns a value in the attribute's own units into the 0 to 1 position the ramp is indexed by.
226
+ *
227
+ * Stops are authored in real units, since "hide anything under 0.1 m" is the sentence a user
228
+ * actually has, and the archive's own published range is what makes that expressible.
229
+ * @param metadata the archive's Data.generation
230
+ * @param value in the attribute's units
231
+ */
232
+ function NormalisePosition(metadata, value) {
233
+ const lo = metadata && typeof metadata.ValueMin === "number" ? metadata.ValueMin : 0;
234
+ const hi = metadata && typeof metadata.ValueMax === "number" ? metadata.ValueMax : 1;
235
+ const span = hi - lo;
236
+ if (!(span > 0)) {
237
+ return 0;
238
+ }
239
+ return Math.min(1, Math.max(0, (value - lo) / span));
240
+ }
241
+ TextureFrameSeriesAnimator.NormalisePosition = NormalisePosition;
9
242
  /**
10
243
  * Detects whether a ClientFile's `Data.generation` metadata describes a frame archive rather than a single static image,
11
244
  * so a caller can decide whether to construct an Animator or fall back to the existing static-texture path.
12
- * @param data ClientFile.Data (the `IFile.Data` field), as returned by ClientFile.Get().
245
+ * @param data ClientFile.Data
13
246
  */
14
247
  function IsFrameArchiveMetadata(data) {
15
- 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));
16
254
  }
17
255
  TextureFrameSeriesAnimator.IsFrameArchiveMetadata = IsFrameArchiveMetadata;
18
- // How far covered values bleed outward into uncovered texels before the GPU samples them.
256
+ // First resolvable static number out of a calculator field list, since that is all a border needs.
257
+ function resolveNumber(fields) {
258
+ if (!fields) {
259
+ return 0;
260
+ }
261
+ for (const field of fields) {
262
+ const value = Number(field && field.value);
263
+ if (Number.isFinite(value)) {
264
+ return value;
265
+ }
266
+ }
267
+ return 0;
268
+ }
269
+ // First resolvable static colour out of a calculator field list.
270
+ function resolveColor(fields) {
271
+ if (!fields) {
272
+ return null;
273
+ }
274
+ for (const field of fields) {
275
+ const parsed = typeof (field === null || field === void 0 ? void 0 : field.value) === "string"
276
+ ? bruce_models_1.Color.ColorFromStr(field.value)
277
+ : null;
278
+ if (parsed) {
279
+ return parsed;
280
+ }
281
+ }
282
+ return null;
283
+ }
284
+ /*
285
+ * Everything about an Animator that a style can change, as a comparable string.
286
+ */
287
+ function AppearanceSignature(options) {
288
+ var _a, _b, _c;
289
+ return JSON.stringify([
290
+ (_a = options.textureColorMask) !== null && _a !== void 0 ? _a : null,
291
+ (_b = options.cellBorder) !== null && _b !== void 0 ? _b : null,
292
+ (_c = options.cellTexels) !== null && _c !== void 0 ? _c : null,
293
+ Boolean(options.maskBaseline)
294
+ ]);
295
+ }
296
+ TextureFrameSeriesAnimator.AppearanceSignature = AppearanceSignature;
297
+ const MAX_COMPOSITE_TEXELS = 2048;
19
298
  const VALUE_DILATE_PASSES = 2;
20
299
  /*
21
- * Bleeds covered values outward into uncovered texels, leaving alpha untouched.
300
+ * Rewrites a packed 16 bit value as the 8 bit grey the colour ramps index on.
22
301
  *
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.
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
+ }
316
+ /*
317
+ * Bleeds covered values outward into uncovered texels, leaving alpha untouched.
27
318
  */
28
- function dilateValues(value, width, height, passes) {
319
+ function dilateValues(value, width, height, passes, packed) {
29
320
  const texels = width * height;
30
321
  const filled = new Uint8Array(texels);
31
322
  for (let p = 0; p < texels; p++) {
@@ -49,15 +340,25 @@ var TextureFrameSeriesAnimator;
49
340
  if (nx < 0 || ny < 0 || nx >= width || ny >= height || !wasFilled[ny * width + nx]) {
50
341
  continue;
51
342
  }
52
- 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];
53
347
  hits++;
54
348
  }
55
349
  }
56
350
  if (hits > 0) {
57
351
  const v = Math.round(sum / hits);
58
- value[p * 4] = v;
59
- value[p * 4 + 1] = v;
60
- 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
+ }
61
362
  filled[p] = 1;
62
363
  }
63
364
  }
@@ -69,7 +370,7 @@ var TextureFrameSeriesAnimator;
69
370
  const DEFAULT_HIGH_COLOR = { red: 21, green: 96, blue: 196, alpha: 0.92 };
70
371
  class Animator {
71
372
  constructor(options) {
72
- var _a, _b;
373
+ var _a, _b, _c;
73
374
  this.removeOnTick = null;
74
375
  this.removeCrossfadeTick = null;
75
376
  this.disposed = false;
@@ -97,6 +398,9 @@ var TextureFrameSeriesAnimator;
97
398
  this.poolIdx = 0;
98
399
  this.valueDims = null;
99
400
  this.presentVersion = 0;
401
+ this.extremesLoad = null;
402
+ this.floorPixels = null;
403
+ this.ceilingPixels = null;
100
404
  if (!options.entity.polygon) {
101
405
  throw new Error("TextureFrameSeriesAnimator requires an entity with polygon graphics.");
102
406
  }
@@ -108,16 +412,40 @@ var TextureFrameSeriesAnimator;
108
412
  this.archiveUrl = options.archiveUrl;
109
413
  this.frames = options.frames;
110
414
  this.crossfadeMs = (_a = options.crossfadeMs) !== null && _a !== void 0 ? _a : DEFAULT_CROSSFADE_MS;
415
+ this.appearance = AppearanceSignature(options);
111
416
  const mask = options.textureColorMask;
112
417
  this.lowColor = (mask && bruce_models_1.Color.ColorFromStr(mask.minColor)) || DEFAULT_LOW_COLOR;
113
418
  this.highColor = (mask && bruce_models_1.Color.ColorFromStr(mask.maxColor)) || DEFAULT_HIGH_COLOR;
419
+ // Positions are authored in the attribute's units and normalised once here, so the
420
+ // per-pixel loop stays a comparison against 0 to 1 like the two-colour path.
421
+ const points = mask && mask.points;
422
+ this.rampStops = (points && points.length > 0)
423
+ ? points.map((p) => ({
424
+ position: NormalisePosition(options.metadata, p.position),
425
+ color: bruce_models_1.Color.ColorFromStr(p.color) || DEFAULT_LOW_COLOR
426
+ }))
427
+ : null;
428
+ const border = options.cellBorder;
429
+ const borderWidth = border ? resolveNumber(border.width) : 0;
430
+ const borderColor = border ? resolveColor(border.color) : null;
431
+ // Zero width or a fully transparent colour means no grid, matching how the polygon's own
432
+ // outline behaves, so an existing style that wants no border keeps getting none.
433
+ this.cellBorder = (border && borderWidth > 0 && borderColor && borderColor.alpha > 0)
434
+ ? {
435
+ cellTexels: Math.max(1, Math.round((_b = options.cellTexels) !== null && _b !== void 0 ? _b : 1)),
436
+ widthTexels: borderWidth,
437
+ color: borderColor
438
+ }
439
+ : null;
114
440
  this.frameDates = this.frames.map((f) => Cesium.JulianDate.fromIso8601(f.Timestamp));
115
441
  this.frameCache = new Array(this.frames.length).fill(null);
116
442
  this.frameDims = new Array(this.frames.length).fill(null);
117
443
  this.valueCache = new Array(this.frames.length).fill(null);
118
444
  this.produceValueCanvas = Boolean(options.produceValueCanvas);
119
- this.baselineMaskEntry = options.baselineMask || null;
120
- this.maskBaseline = (_b = options.maskBaseline) !== null && _b !== void 0 ? _b : Boolean(options.baselineMask);
445
+ this.metadata = options.metadata;
446
+ this.drapeExtent = options.drapeExtent;
447
+ this.tiles = TilesOf(options.metadata);
448
+ this.maskBaseline = (_c = options.maskBaseline) !== null && _c !== void 0 ? _c : this.tiles.some((tile) => Boolean(tile.BaselineMask));
121
449
  this.valueCanvas = this.produceValueCanvas ? document.createElement("canvas") : null;
122
450
  this.driveMaterial = options.driveMaterial !== false;
123
451
  this.originalMaterial = options.entity.polygon.material;
@@ -153,6 +481,10 @@ var TextureFrameSeriesAnimator;
153
481
  IsDisposed() {
154
482
  return this.disposed;
155
483
  }
484
+ // What this Animator was built to look like, for deciding whether it can be reused.
485
+ GetAppearanceSignature() {
486
+ return this.appearance;
487
+ }
156
488
  /**
157
489
  * The archive URL this instance was constructed with,
158
490
  * lets a caller re-rendering the same entity tell whether an existing instance is already correct, without reaching into private state.
@@ -167,6 +499,15 @@ var TextureFrameSeriesAnimator;
167
499
  GetImageProperty() {
168
500
  return this.imageProperty;
169
501
  }
502
+ /**
503
+ * The presented frame's TINTED pixels, where alpha is what the ramp actually painted.
504
+ *
505
+ * Distinct from the value canvas, whose alpha only says a texel has data. A cell can hold a
506
+ * reading and still be painted nothing, which is exactly what a hidden floor band does.
507
+ */
508
+ GetDisplayedPixels() {
509
+ return this.displayedPixels;
510
+ }
170
511
  /**
171
512
  * The untinted canvas carrying the presented frame's value in RGB and its coverage in alpha,
172
513
  * or null unless the instance was constructed with produceValueCanvas.
@@ -279,10 +620,20 @@ var TextureFrameSeriesAnimator;
279
620
  });
280
621
  }
281
622
  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}` } });
623
+ const tiles = this.tiles;
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;
632
+ }
633
+ lo = Math.min(lo, at.ByteOffset);
634
+ hi = Math.max(hi, at.ByteOffset + at.ByteLength);
635
+ }
636
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
286
637
  const buffer = await response.arrayBuffer();
287
638
  if (this.disposed) {
288
639
  return;
@@ -293,7 +644,7 @@ var TextureFrameSeriesAnimator;
293
644
  if (this.disposed) {
294
645
  return;
295
646
  }
296
- const decoded = await this.decodeAndTint(buffer);
647
+ const decoded = await this.composeTiles(tiles, idx, buffer, lo);
297
648
  if (this.disposed) {
298
649
  return;
299
650
  }
@@ -304,39 +655,292 @@ var TextureFrameSeriesAnimator;
304
655
  this.beginCrossfadeTo(idx);
305
656
  }
306
657
  }
307
- /**
308
- * Decodes one frame's raw PNG bytes (as returned by a Range GET) and applies the grayscale color mask.
658
+ /*
659
+ * Draws every tile of one frame into a single raster covering the drape extent.
309
660
  */
310
- async decodeAndTint(buffer) {
311
- const blob = new Blob([buffer], { type: "image/png" });
312
- const objectUrl = URL.createObjectURL(blob);
313
- let image;
314
- try {
315
- image = await (0, image_utils_1.loadImage)(objectUrl);
316
- }
317
- finally {
318
- URL.revokeObjectURL(objectUrl);
319
- }
661
+ async composeTiles(tiles, idx, buffer, spanStart) {
662
+ const target = this.compositeExtent();
663
+ const size = this.compositeSize(tiles, target);
320
664
  const canvas = document.createElement("canvas");
321
- canvas.width = image.width;
322
- canvas.height = image.height;
665
+ canvas.width = size.width;
666
+ canvas.height = size.height;
323
667
  const ctx = canvas.getContext("2d");
324
- ctx.drawImage(image, 0, 0);
325
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
668
+ ctx.imageSmoothingEnabled = false;
669
+ ctx.clearRect(0, 0, size.width, size.height);
670
+ const images = await this.decodeSlices(buffer, spanStart, tiles.map((tile) => tile.Frames[idx]));
671
+ for (let i = 0; i < tiles.length; i++) {
672
+ const image = images[i];
673
+ if (!image || this.disposed) {
674
+ continue;
675
+ }
676
+ const at = this.tileRect(tiles[i], target, size);
677
+ ctx.drawImage(image, at.x, at.y, at.w, at.h);
678
+ }
679
+ return this.tint(ctx.getImageData(0, 0, size.width, size.height));
680
+ }
681
+ /*
682
+ * Where a composited frame is draped, which is the polygon's rectangle when there is one.
683
+ */
684
+ compositeExtent() {
685
+ if (this.drapeExtent) {
686
+ return this.drapeExtent;
687
+ }
688
+ const m = this.metadata;
689
+ return {
690
+ West: m && m.West != null ? m.West : -180,
691
+ East: m && m.East != null ? m.East : 180,
692
+ South: m && m.South != null ? m.South : -90,
693
+ North: m && m.North != null ? m.North : 90
694
+ };
695
+ }
696
+ /*
697
+ * Composite resolution, capped by what can be uploaded as a texture every frame.
698
+ */
699
+ compositeSize(tiles, target) {
700
+ let finest = Number.MAX_VALUE;
701
+ for (const tile of tiles) {
702
+ if (tile.TexelMetres > 0) {
703
+ finest = Math.min(finest, tile.TexelMetres);
704
+ }
705
+ }
706
+ if (!(finest > 0) || finest === Number.MAX_VALUE) {
707
+ finest = 1;
708
+ }
709
+ const mid = (target.South + target.North) / 2 * Math.PI / 180;
710
+ const widthM = Math.max((target.East - target.West) * 111320 * Math.cos(mid), 1);
711
+ const heightM = Math.max((target.North - target.South) * 110574, 1);
712
+ let width = Math.ceil(widthM / finest);
713
+ let height = Math.ceil(heightM / finest);
714
+ const longest = Math.max(width, height);
715
+ if (longest > MAX_COMPOSITE_TEXELS) {
716
+ const shrink = MAX_COMPOSITE_TEXELS / longest;
717
+ width = Math.max(1, Math.round(width * shrink));
718
+ height = Math.max(1, Math.round(height * shrink));
719
+ }
720
+ return { width: Math.max(1, width), height: Math.max(1, height) };
721
+ }
722
+ /*
723
+ * Applies the ramp, borders and baseline mask to raw value pixels, whatever produced them.
724
+ */
725
+ tint(imageData) {
726
+ const canvas = { width: imageData.width, height: imageData.height };
326
727
  // Copied before tinting: the ramp is a lerp between two colours, so the value cannot be
327
728
  // recovered from the tinted pixels afterwards.
328
729
  const valuePixels = this.produceValueCanvas ? new Uint8ClampedArray(imageData.data) : undefined;
329
- (0, image_utils_1.ApplyGrayscaleColorMask)(imageData, this.lowColor, this.highColor);
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));
734
+ // Stops win when supplied: they can express a band and a hidden floor, which a two
735
+ // colour ramp cannot. The pair stays the fallback so an older style still draws.
736
+ if (this.rampStops && this.rampStops.length > 0) {
737
+ (0, image_utils_1.ApplyGradientStops)(imageData, this.rampStops);
738
+ }
739
+ else {
740
+ (0, image_utils_1.ApplyGrayscaleColorMask)(imageData, this.lowColor, this.highColor);
741
+ }
742
+ if (this.cellBorder) {
743
+ (0, image_utils_1.ApplyCellBorders)(imageData, this.cellBorder.cellTexels, this.cellBorder.widthTexels, this.cellBorder.color);
744
+ }
330
745
  this.applyBaselineMask(imageData.data, valuePixels, canvas.width, canvas.height);
331
746
  if (valuePixels) {
332
- dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES);
747
+ dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES, IsPackedValue(this.metadata));
333
748
  }
334
749
  return { pixels: imageData.data, width: canvas.width, height: canvas.height, valuePixels };
335
750
  }
751
+ /*
752
+ * Paints one frame's composited pixels onto a canvas.
753
+ */
754
+ writeInto(canvas, pixels, dims) {
755
+ canvas.width = dims.width;
756
+ canvas.height = dims.height;
757
+ const ctx = canvas.getContext("2d");
758
+ const image = ctx.createImageData(dims.width, dims.height);
759
+ image.data.set(pixels);
760
+ ctx.putImageData(image, 0, 0);
761
+ }
762
+ /**
763
+ * The archive's per-cell floor and ceiling rasters, once EnsureExtremes has resolved.
764
+ */
765
+ GetExtremes() {
766
+ return { floor: this.floorPixels, ceiling: this.ceilingPixels };
767
+ }
768
+ /**
769
+ * Fetches and decodes the floor and ceiling rasters, at most once.
770
+ */
771
+ EnsureExtremes() {
772
+ if (!this.extremesLoad) {
773
+ this.extremesLoad = this.loadTiledExtremes();
774
+ }
775
+ return this.extremesLoad;
776
+ }
777
+ /*
778
+ * Composites each tile's baseline mask into one covering the composited frames.
779
+ */
780
+ async loadTiledBaselineMask() {
781
+ const tiles = this.tiles;
782
+ const entries = tiles.map((tile) => tile.BaselineMask);
783
+ let lo = Number.MAX_SAFE_INTEGER;
784
+ let hi = 0;
785
+ for (const at of entries) {
786
+ if (!at) {
787
+ continue;
788
+ }
789
+ lo = Math.min(lo, at.ByteOffset);
790
+ hi = Math.max(hi, at.ByteOffset + at.ByteLength);
791
+ }
792
+ if (!(hi > lo)) {
793
+ return;
794
+ }
795
+ try {
796
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
797
+ const block = await response.arrayBuffer();
798
+ if (this.disposed) {
799
+ return;
800
+ }
801
+ const images = await this.decodeSlices(block, lo, entries);
802
+ if (this.disposed) {
803
+ return;
804
+ }
805
+ const target = this.compositeExtent();
806
+ const size = this.compositeSize(tiles, target);
807
+ const canvas = document.createElement("canvas");
808
+ canvas.width = size.width;
809
+ canvas.height = size.height;
810
+ const ctx = canvas.getContext("2d");
811
+ ctx.imageSmoothingEnabled = false;
812
+ ctx.clearRect(0, 0, size.width, size.height);
813
+ for (let i = 0; i < tiles.length; i++) {
814
+ if (!images[i]) {
815
+ continue;
816
+ }
817
+ const at = this.tileRect(tiles[i], target, size);
818
+ ctx.drawImage(images[i], at.x, at.y, at.w, at.h);
819
+ }
820
+ const pixels = ctx.getImageData(0, 0, size.width, size.height).data;
821
+ const flags = new Uint8Array(size.width * size.height);
822
+ for (let p = 0; p < flags.length; p++) {
823
+ flags[p] = pixels[p * 4 + 3] >= 128 ? 1 : 0;
824
+ }
825
+ this.baselineFlags = flags;
826
+ this.baselineDims = { width: size.width, height: size.height };
827
+ }
828
+ catch (e) {
829
+ console.warn("TextureFrameSeriesAnimator: could not load the tiled baseline mask.", e);
830
+ }
831
+ }
832
+ /*
833
+ * Composites each tile's floor and ceiling into rasters matching the composited frames.
834
+ *
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.
837
+ */
838
+ async loadTiledExtremes() {
839
+ const tiles = this.tiles;
840
+ const target = this.compositeExtent();
841
+ const size = this.compositeSize(tiles, target);
842
+ // One request for the whole extremes block. They sit contiguously after the frames, so
843
+ // fetching per tile would be 164 round trips for an 82 tile pyramid.
844
+ let lo = Number.MAX_SAFE_INTEGER;
845
+ let hi = 0;
846
+ for (const tile of tiles) {
847
+ for (const at of [tile.Floor, tile.Ceiling]) {
848
+ if (!at) {
849
+ continue;
850
+ }
851
+ lo = Math.min(lo, at.ByteOffset);
852
+ hi = Math.max(hi, at.ByteOffset + at.ByteLength);
853
+ }
854
+ }
855
+ if (!(hi > lo)) {
856
+ return;
857
+ }
858
+ const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
859
+ const block = await response.arrayBuffer();
860
+ if (this.disposed) {
861
+ return;
862
+ }
863
+ const draw = async (pick) => {
864
+ const images = await this.decodeSlices(block, lo, tiles.map(pick));
865
+ if (this.disposed) {
866
+ return null;
867
+ }
868
+ const canvas = document.createElement("canvas");
869
+ canvas.width = size.width;
870
+ canvas.height = size.height;
871
+ const ctx = canvas.getContext("2d");
872
+ ctx.imageSmoothingEnabled = false;
873
+ ctx.clearRect(0, 0, size.width, size.height);
874
+ let drew = false;
875
+ for (let i = 0; i < tiles.length; i++) {
876
+ const image = images[i];
877
+ if (!image) {
878
+ continue;
879
+ }
880
+ const at = this.tileRect(tiles[i], target, size);
881
+ ctx.drawImage(image, at.x, at.y, at.w, at.h);
882
+ drew = true;
883
+ }
884
+ return drew ? ctx.getImageData(0, 0, size.width, size.height) : null;
885
+ };
886
+ try {
887
+ this.floorPixels = await draw((tile) => tile.Floor);
888
+ this.ceilingPixels = await draw((tile) => tile.Ceiling);
889
+ }
890
+ catch (e) {
891
+ console.warn("TextureFrameSeriesAnimator: could not load the tiled extremes.", e);
892
+ }
893
+ }
894
+ /*
895
+ * Where one tile lands on the composite, snapped to whole pixels.
896
+ *
897
+ * Rounding each edge rather than the origin and size is what makes neighbours meet exactly:
898
+ * a tile's right edge and the next tile's left edge round to the same pixel, so no seam of
899
+ * background shows between them.
900
+ */
901
+ tileRect(tile, target, size) {
902
+ const spanLon = target.East - target.West;
903
+ const spanLat = target.North - target.South;
904
+ const x0 = Math.round((tile.West - target.West) / spanLon * size.width);
905
+ const x1 = Math.round((tile.East - target.West) / spanLon * size.width);
906
+ const y0 = Math.round((target.North - tile.North) / spanLat * size.height);
907
+ const y1 = Math.round((target.North - tile.South) / spanLat * size.height);
908
+ return { x: x0, y: y0, w: Math.max(1, x1 - x0), h: Math.max(1, y1 - y0) };
909
+ }
910
+ /*
911
+ * Decodes many tile images at once.
912
+ *
913
+ * Serially awaiting each decode makes a frame cost as many round trips through the image
914
+ * decoder as there are tiles, which for an 82 tile pyramid is seconds rather than one.
915
+ */
916
+ decodeSlices(block, base, entries) {
917
+ return Promise.all(entries.map(async (at) => {
918
+ if (!at) {
919
+ return null;
920
+ }
921
+ const slice = block.slice(at.ByteOffset - base, at.ByteOffset - base + at.ByteLength);
922
+ const objectUrl = URL.createObjectURL(new Blob([slice], { type: "image/png" }));
923
+ try {
924
+ return await (0, image_utils_1.loadImage)(objectUrl);
925
+ }
926
+ catch {
927
+ return null;
928
+ }
929
+ finally {
930
+ URL.revokeObjectURL(objectUrl);
931
+ }
932
+ }));
933
+ }
336
934
  /**
337
935
  * Fetches and decodes the archive's static baseline mask, at most once.
338
936
  */
339
937
  ensureBaselineMask() {
938
+ if (this.maskBaseline && this.tiles) {
939
+ if (!this.baselineLoad) {
940
+ this.baselineLoad = this.loadTiledBaselineMask();
941
+ }
942
+ return this.baselineLoad;
943
+ }
340
944
  if (!this.maskBaseline || !this.baselineMaskEntry) {
341
945
  return Promise.resolve();
342
946
  }
@@ -449,24 +1053,17 @@ var TextureFrameSeriesAnimator;
449
1053
  presentPixels(pixels, dims, valuePixels) {
450
1054
  this.poolIdx = 1 - this.poolIdx;
451
1055
  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);
1056
+ this.writeInto(target, pixels, dims);
458
1057
  this.displayedPixels = pixels;
459
1058
  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);
1059
+ this.writeInto(this.valueCanvas, valuePixels, dims);
466
1060
  this.displayedValuePixels = valuePixels;
467
1061
  this.valueDims = dims;
468
1062
  }
469
1063
  this.presentVersion++;
1064
+ if (this.viewer && this.viewer.scene && this.viewer.scene.requestRenderMode) {
1065
+ this.viewer.scene.requestRender();
1066
+ }
470
1067
  }
471
1068
  }
472
1069
  TextureFrameSeriesAnimator.Animator = Animator;