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