bruce-models 7.1.87 → 7.1.89
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-models.es5.js +2482 -176
- package/dist/bruce-models.es5.js.map +1 -1
- package/dist/bruce-models.umd.js +2475 -180
- package/dist/bruce-models.umd.js.map +1 -1
- package/dist/lib/bruce-models.js +3 -1
- package/dist/lib/bruce-models.js.map +1 -1
- package/dist/lib/client-file/client-file-value-map.js +197 -0
- package/dist/lib/client-file/client-file-value-map.js.map +1 -0
- package/dist/lib/client-file/client-file.js +0 -76
- package/dist/lib/client-file/client-file.js.map +1 -1
- package/dist/lib/client-file/value-map-grid.js +298 -0
- package/dist/lib/client-file/value-map-grid.js.map +1 -0
- package/dist/lib/client-file/value-map-metadata.js +362 -0
- package/dist/lib/client-file/value-map-metadata.js.map +1 -0
- package/dist/lib/client-file/value-map-reader.js +1547 -0
- package/dist/lib/client-file/value-map-reader.js.map +1 -0
- package/dist/lib/export/export-nsx.js +32 -0
- package/dist/lib/export/export-nsx.js.map +1 -0
- package/dist/types/bruce-models.d.ts +3 -1
- package/dist/types/client-file/client-file-value-map.d.ts +186 -0
- package/dist/types/client-file/client-file.d.ts +1 -14
- package/dist/types/client-file/value-map-grid.d.ts +108 -0
- package/dist/types/client-file/value-map-metadata.d.ts +176 -0
- package/dist/types/client-file/value-map-reader.d.ts +520 -0
- package/dist/types/export/export-nsx.d.ts +16 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1547 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.ValueMapReader = void 0;
|
|
13
|
+
const bounds_1 = require("../common/bounds");
|
|
14
|
+
const geometry_1 = require("../common/geometry");
|
|
15
|
+
const client_file_1 = require("./client-file");
|
|
16
|
+
const value_map_metadata_1 = require("./value-map-metadata");
|
|
17
|
+
const value_map_grid_1 = require("./value-map-grid");
|
|
18
|
+
/**
|
|
19
|
+
* Reads a generated value map: its frames, the values in them, and the shapes those values make.
|
|
20
|
+
*/
|
|
21
|
+
var ValueMapReader;
|
|
22
|
+
(function (ValueMapReader) {
|
|
23
|
+
/*
|
|
24
|
+
Texels an area needs before it counts as one.
|
|
25
|
+
A texel is a mesh sample rather than a measurement of ground, so a handful of them
|
|
26
|
+
touching is usually the edge of the model flickering over the threshold.
|
|
27
|
+
Pass 1 to keep every one of them.
|
|
28
|
+
*/
|
|
29
|
+
ValueMapReader.MIN_TEXELS = 8;
|
|
30
|
+
// How far a reduced outline may move from the traced one, in texels.
|
|
31
|
+
ValueMapReader.SIMPLIFY_TOLERANCE = 0.25;
|
|
32
|
+
// Fraction of the busiest frame that counts as the event being under way, used when picking
|
|
33
|
+
// which frames are worth looking at.
|
|
34
|
+
ValueMapReader.ONSET_FRACTION = 0.5;
|
|
35
|
+
// Candidate levels walked when a threshold has to be read off the series.
|
|
36
|
+
ValueMapReader.LEVELS = 256;
|
|
37
|
+
// Longest edge of the grid tiles are composited onto. A pyramid's finest tile can imply a
|
|
38
|
+
// raster far larger than the analysis needs, and every frame pays for it.
|
|
39
|
+
ValueMapReader.MAX_COMPOSITE_TEXELS = 2048;
|
|
40
|
+
// Decoded frames kept, since compositing a tiled frame costs one image decode per tile and the
|
|
41
|
+
// passes over a series each walk the whole thing again.
|
|
42
|
+
ValueMapReader.FRAME_CACHE = 16;
|
|
43
|
+
// Single tile rasters kept, which is what a point sample reads instead of a whole composite.
|
|
44
|
+
ValueMapReader.TILE_CACHE = 32;
|
|
45
|
+
// How far apart two slices can sit and still be read together, from the format.
|
|
46
|
+
ValueMapReader.MAX_READ_GAP = value_map_metadata_1.ValueMapMetadata.MAX_READ_GAP;
|
|
47
|
+
class Reader {
|
|
48
|
+
constructor(params) {
|
|
49
|
+
this.frames = new Map();
|
|
50
|
+
this.tileLayers = new Map();
|
|
51
|
+
this.statistics = new Map();
|
|
52
|
+
this.thresholds = new Map();
|
|
53
|
+
this.baseline = undefined;
|
|
54
|
+
this.unchanging = undefined;
|
|
55
|
+
this.File = params.file;
|
|
56
|
+
this.Metadata = value_map_metadata_1.ValueMapMetadata.Of(params.file);
|
|
57
|
+
const refused = value_map_metadata_1.ValueMapMetadata.Unreadable(this.Metadata);
|
|
58
|
+
if (refused) {
|
|
59
|
+
// An unknown shape decodes into plausible numbers rather than failing, which is why
|
|
60
|
+
// it is refused here instead of half read.
|
|
61
|
+
throw new Error(refused);
|
|
62
|
+
}
|
|
63
|
+
this.Tiles = value_map_metadata_1.ValueMapMetadata.TilesOf(this.Metadata);
|
|
64
|
+
this.bytes = params.bytes;
|
|
65
|
+
this.decoder = params.decoder || ValueMapReader.DefaultDecoder;
|
|
66
|
+
this.Extent = params.extent || value_map_metadata_1.ValueMapMetadata.ExtentOf(this.Metadata);
|
|
67
|
+
if (!this.Extent) {
|
|
68
|
+
throw new Error("The value map's Geometry block does not place its raster on the"
|
|
69
|
+
+ " world, so there is nowhere to read it onto.");
|
|
70
|
+
}
|
|
71
|
+
const size = value_map_grid_1.ValueMapGrid.CompositeSize(this.Tiles, this.Extent, params.maxCompositeTexels || ValueMapReader.MAX_COMPOSITE_TEXELS);
|
|
72
|
+
this.Grid = new value_map_grid_1.ValueMapGrid.Grid(this.Metadata.Geometry, size.Width, size.Height, this.Extent);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The attribute this value map was generated from.
|
|
76
|
+
*/
|
|
77
|
+
get Attribute() {
|
|
78
|
+
return this.identity().Attribute || null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* What the attribute is called for a reader, where the source names it.
|
|
82
|
+
*/
|
|
83
|
+
get Label() {
|
|
84
|
+
return this.identity().Label || null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The dataset the values came from, since one account holds several over the same mesh.
|
|
88
|
+
*/
|
|
89
|
+
get Dataset() {
|
|
90
|
+
return this.identity().Dataset || null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The Entity Type Source this was generated from.
|
|
94
|
+
*/
|
|
95
|
+
get EntityTypeSourceID() {
|
|
96
|
+
const at = this.identity().EntityTypeSourceID;
|
|
97
|
+
return at == null ? null : Number(at);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* When the archive was written, which is what tells two generations of it apart.
|
|
101
|
+
*/
|
|
102
|
+
get Generated() {
|
|
103
|
+
return this.identity().Generated || null;
|
|
104
|
+
}
|
|
105
|
+
get Revision() {
|
|
106
|
+
const at = this.identity().Revision;
|
|
107
|
+
return at == null ? null : Number(at);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The units a reading is in, where the source states them.
|
|
111
|
+
*/
|
|
112
|
+
get Units() {
|
|
113
|
+
return (this.Metadata.Value || {}).Units || null;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* How a frame's number is packed: one channel or two.
|
|
117
|
+
*/
|
|
118
|
+
get Format() {
|
|
119
|
+
return (this.Metadata.Value || {}).Format || value_map_metadata_1.ValueMapMetadata.FORMAT_U8;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The values a reading out of this value map falls between, in its own units.
|
|
123
|
+
*/
|
|
124
|
+
get ValueRange() {
|
|
125
|
+
return value_map_metadata_1.ValueMapMetadata.ValueRange(this.Metadata);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The values the source's own numbers fall between, or null where nothing was transformed.
|
|
129
|
+
*/
|
|
130
|
+
get SourceRange() {
|
|
131
|
+
return value_map_metadata_1.ValueMapMetadata.SourceRange(this.Metadata);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* What the source's own heights are measured from, when the archive says.
|
|
135
|
+
* A thickness declares none, and an untransformed elevation declares one with no transform.
|
|
136
|
+
*/
|
|
137
|
+
get VerticalDatum() {
|
|
138
|
+
return (this.Metadata.ValueSource || {}).VerticalDatum || null;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* What was applied to the source's numbers to get the delivered ones, empty for nothing.
|
|
142
|
+
*/
|
|
143
|
+
get Transform() {
|
|
144
|
+
return value_map_metadata_1.ValueMapMetadata.TransformOf(this.Metadata);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Whether a reading differs from what the source's own data said.
|
|
148
|
+
*/
|
|
149
|
+
get IsTransformed() {
|
|
150
|
+
return value_map_metadata_1.ValueMapMetadata.IsTransformed(this.Metadata);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* What the source suggests a picture be ramped over, which is not a bound on the data.
|
|
154
|
+
*/
|
|
155
|
+
get ColorBarRange() {
|
|
156
|
+
return value_map_metadata_1.ValueMapMetadata.ColorBarRange(this.Metadata);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The largest movement any one texel made over the series.
|
|
160
|
+
*/
|
|
161
|
+
get MovingRangeMax() {
|
|
162
|
+
const stats = this.Metadata.Stats || {};
|
|
163
|
+
return typeof stats.MovingRangeMax === "number" ? stats.MovingRangeMax : null;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* How many timesteps the generator set out to write.
|
|
167
|
+
*/
|
|
168
|
+
get RequestedFrames() {
|
|
169
|
+
return (this.Metadata.Series || {}).Requested || 0;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* How many it actually wrote.
|
|
173
|
+
*/
|
|
174
|
+
get WrittenFrames() {
|
|
175
|
+
const series = this.Metadata.Series || {};
|
|
176
|
+
return typeof series.Written === "number" ? series.Written : this.Times.length;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Whether the archive holds every timestep that was asked for.
|
|
180
|
+
*/
|
|
181
|
+
get IsComplete() {
|
|
182
|
+
return value_map_metadata_1.ValueMapMetadata.IsComplete(this.Metadata);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* When each frame was sampled. Authoritative, unlike the series increment.
|
|
186
|
+
*/
|
|
187
|
+
get Times() {
|
|
188
|
+
return value_map_metadata_1.ValueMapMetadata.TimesOf(this.Metadata);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* How many frames the archive holds.
|
|
192
|
+
*/
|
|
193
|
+
get Length() {
|
|
194
|
+
return this.Times.length;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Says so when the archive's heights are not on the ellipsoid, or null when they are.
|
|
198
|
+
*/
|
|
199
|
+
Warning() {
|
|
200
|
+
return value_map_metadata_1.ValueMapMetadata.UnshiftedWarning(this.Metadata);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* One layer's declaration, or undefined when the archive does not carry it.
|
|
204
|
+
*/
|
|
205
|
+
Layer(name) {
|
|
206
|
+
return value_map_metadata_1.ValueMapMetadata.LayerOf(this.Metadata, name);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* One frame's values and a mask of which texels were sampled.
|
|
210
|
+
*/
|
|
211
|
+
Frame(index = 0, source = false) {
|
|
212
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
213
|
+
const position = this.position(index);
|
|
214
|
+
const key = position + ":" + source;
|
|
215
|
+
const held = this.frames.get(key);
|
|
216
|
+
if (held) {
|
|
217
|
+
this.frames.delete(key);
|
|
218
|
+
this.frames.set(key, held);
|
|
219
|
+
return held;
|
|
220
|
+
}
|
|
221
|
+
let field = yield this.composite(value_map_metadata_1.ValueMapMetadata.LAYER_VALUE, position);
|
|
222
|
+
if (!field) {
|
|
223
|
+
throw new Error("Frame " + index + " has no image in any of this value map's "
|
|
224
|
+
+ this.Tiles.length + " tiles.");
|
|
225
|
+
}
|
|
226
|
+
if (source) {
|
|
227
|
+
field = yield this.toSource(field);
|
|
228
|
+
}
|
|
229
|
+
this.frames.set(key, field);
|
|
230
|
+
Trim(this.frames, ValueMapReader.FRAME_CACHE);
|
|
231
|
+
return field;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Each texel's lowest value over the series and where that is known, or null.
|
|
236
|
+
*/
|
|
237
|
+
ValueMin() {
|
|
238
|
+
return this.statistic(value_map_metadata_1.ValueMapMetadata.LAYER_VALUE_MIN);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Each texel's highest value over the series and where that is known, or null.
|
|
242
|
+
*/
|
|
243
|
+
ValueMax() {
|
|
244
|
+
return this.statistic(value_map_metadata_1.ValueMapMetadata.LAYER_VALUE_MAX);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* The per-texel correction applied to the source's own numbers, or null where none was.
|
|
248
|
+
*
|
|
249
|
+
* sourceValue = value - shift, which is what makes the transform reversible per texel rather
|
|
250
|
+
* than only describable in prose.
|
|
251
|
+
*/
|
|
252
|
+
ValueShift() {
|
|
253
|
+
return this.statistic(value_map_metadata_1.ValueMapMetadata.LAYER_VALUE_SHIFT);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* The texels flagged as permanently at their own minimum, or null when none were published.
|
|
257
|
+
*
|
|
258
|
+
* A caller measuring change subtracts these rather than reporting ground that never moved.
|
|
259
|
+
*/
|
|
260
|
+
Baseline() {
|
|
261
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
262
|
+
if (this.baseline !== undefined) {
|
|
263
|
+
return this.baseline;
|
|
264
|
+
}
|
|
265
|
+
this.baseline = null;
|
|
266
|
+
if (this.Layer(value_map_metadata_1.ValueMapMetadata.LAYER_BASELINE)) {
|
|
267
|
+
const field = yield this.composite(value_map_metadata_1.ValueMapMetadata.LAYER_BASELINE);
|
|
268
|
+
if (field) {
|
|
269
|
+
this.baseline = { Width: field.Width, Height: field.Height, Bits: field.Covered };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return this.baseline;
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* The texels holding one value for the whole series, or null for a single frame archive.
|
|
277
|
+
*/
|
|
278
|
+
Unchanging() {
|
|
279
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
280
|
+
if (this.unchanging !== undefined) {
|
|
281
|
+
return this.unchanging;
|
|
282
|
+
}
|
|
283
|
+
this.unchanging = null;
|
|
284
|
+
if (this.Length < 2) {
|
|
285
|
+
return this.unchanging;
|
|
286
|
+
}
|
|
287
|
+
let lowest = null;
|
|
288
|
+
let highest = null;
|
|
289
|
+
let always = null;
|
|
290
|
+
let width = 0;
|
|
291
|
+
let height = 0;
|
|
292
|
+
for (let index = 0; index < this.Length; index++) {
|
|
293
|
+
const frame = yield this.Frame(index);
|
|
294
|
+
width = frame.Width;
|
|
295
|
+
height = frame.Height;
|
|
296
|
+
if (!lowest) {
|
|
297
|
+
lowest = frame.Values.slice();
|
|
298
|
+
highest = frame.Values.slice();
|
|
299
|
+
always = frame.Covered.slice();
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
for (let at = 0; at < lowest.length; at++) {
|
|
303
|
+
lowest[at] = Math.min(lowest[at], frame.Values[at]);
|
|
304
|
+
highest[at] = Math.max(highest[at], frame.Values[at]);
|
|
305
|
+
// A texel sampled in only some frames is changing, whatever its values did.
|
|
306
|
+
always[at] = always[at] && frame.Covered[at] ? 1 : 0;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const bits = new Uint8Array(always.length);
|
|
310
|
+
for (let at = 0; at < bits.length; at++) {
|
|
311
|
+
bits[at] = always[at] && lowest[at] === highest[at] ? 1 : 0;
|
|
312
|
+
}
|
|
313
|
+
this.unchanging = { Width: width, Height: height, Bits: bits };
|
|
314
|
+
return this.unchanging;
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Every texel a change in the series cannot be read from, or null when there are none.
|
|
319
|
+
*
|
|
320
|
+
* The published baseline and the texels measured as unchanging, since neither describes
|
|
321
|
+
* anything the series does and a caller asking what moved wants both gone.
|
|
322
|
+
*/
|
|
323
|
+
Ignored() {
|
|
324
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
325
|
+
const baseline = yield this.Baseline();
|
|
326
|
+
const unchanging = yield this.Unchanging();
|
|
327
|
+
if (!baseline) {
|
|
328
|
+
return unchanging;
|
|
329
|
+
}
|
|
330
|
+
if (!unchanging) {
|
|
331
|
+
return baseline;
|
|
332
|
+
}
|
|
333
|
+
return Combine(baseline, unchanging, (left, right) => left || right);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* The level the series itself separates on, for a caller who cannot name one.
|
|
338
|
+
*/
|
|
339
|
+
NaturalThreshold(source = false) {
|
|
340
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
341
|
+
if (this.thresholds.has(source)) {
|
|
342
|
+
return this.thresholds.get(source);
|
|
343
|
+
}
|
|
344
|
+
const range = (source && this.SourceRange) || this.ValueRange;
|
|
345
|
+
this.thresholds.set(source, range.Min);
|
|
346
|
+
if (this.Length < 2) {
|
|
347
|
+
return range.Min;
|
|
348
|
+
}
|
|
349
|
+
const step = (range.Max - range.Min) / (ValueMapReader.LEVELS - 1);
|
|
350
|
+
if (step <= 0) {
|
|
351
|
+
return range.Min;
|
|
352
|
+
}
|
|
353
|
+
const ignored = yield this.Ignored();
|
|
354
|
+
const above = [];
|
|
355
|
+
for (let index = 0; index < this.Length; index++) {
|
|
356
|
+
const frame = yield this.Frame(index, source);
|
|
357
|
+
const counts = new Float64Array(ValueMapReader.LEVELS);
|
|
358
|
+
for (let at = 0; at < frame.Values.length; at++) {
|
|
359
|
+
if (!frame.Covered[at] || (ignored && ignored.Bits[at])) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const level = Math.max(0, Math.min(ValueMapReader.LEVELS - 1, Math.round((frame.Values[at] - range.Min) / step)));
|
|
363
|
+
counts[level]++;
|
|
364
|
+
}
|
|
365
|
+
// Texels strictly above each level, which is what a mask at that level would hold.
|
|
366
|
+
const holding = new Array(ValueMapReader.LEVELS);
|
|
367
|
+
let running = 0;
|
|
368
|
+
for (let level = ValueMapReader.LEVELS - 1; level >= 0; level--) {
|
|
369
|
+
holding[level] = running;
|
|
370
|
+
running += counts[level];
|
|
371
|
+
}
|
|
372
|
+
above.push(holding);
|
|
373
|
+
}
|
|
374
|
+
let best = 0;
|
|
375
|
+
let widest = 0;
|
|
376
|
+
for (let level = 0; level < ValueMapReader.LEVELS; level++) {
|
|
377
|
+
const spread = Deviation(above.map((counts) => counts[level]));
|
|
378
|
+
if (spread > widest) {
|
|
379
|
+
widest = spread;
|
|
380
|
+
best = level;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (widest <= 0) {
|
|
384
|
+
return range.Min;
|
|
385
|
+
}
|
|
386
|
+
this.thresholds.set(source, range.Min + best * step);
|
|
387
|
+
return this.thresholds.get(source);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* The level to measure against, read off the series when the caller names none.
|
|
392
|
+
*/
|
|
393
|
+
Resolve(threshold, source = false) {
|
|
394
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
395
|
+
return threshold == null ? this.NaturalThreshold(source) : threshold;
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* The texels in one frame whose value is over a threshold.
|
|
400
|
+
*
|
|
401
|
+
* Ask for source to compare against the customer's own numbers, so a level stated against
|
|
402
|
+
* their data means what they think it means.
|
|
403
|
+
*/
|
|
404
|
+
MaskAbove(params) {
|
|
405
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
406
|
+
const options = params || {};
|
|
407
|
+
const source = !!options.source;
|
|
408
|
+
const frame = yield this.Frame(options.frame || 0, source);
|
|
409
|
+
const level = yield this.Resolve(options.threshold, source);
|
|
410
|
+
const bits = new Uint8Array(frame.Values.length);
|
|
411
|
+
for (let at = 0; at < bits.length; at++) {
|
|
412
|
+
bits[at] = frame.Covered[at] && frame.Values[at] > level ? 1 : 0;
|
|
413
|
+
}
|
|
414
|
+
const mask = { Width: frame.Width, Height: frame.Height, Bits: bits };
|
|
415
|
+
if (options.excludeBaseline === false) {
|
|
416
|
+
return mask;
|
|
417
|
+
}
|
|
418
|
+
const ignored = yield this.Ignored();
|
|
419
|
+
return ignored ? Combine(mask, ignored, (left, right) => left && !right) : mask;
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* The texels over a threshold in every frame, or null for an archive with no frames.
|
|
424
|
+
*
|
|
425
|
+
* Measured rather than read off the first frame, so a tide that is out at the start does
|
|
426
|
+
* not get counted as ground the event flooded.
|
|
427
|
+
*/
|
|
428
|
+
AlwaysAbove(params) {
|
|
429
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
430
|
+
const options = params || {};
|
|
431
|
+
let always = null;
|
|
432
|
+
for (let index = 0; index < this.Length; index++) {
|
|
433
|
+
const mask = yield this.MaskAbove({
|
|
434
|
+
threshold: options.threshold,
|
|
435
|
+
frame: index,
|
|
436
|
+
excludeBaseline: options.excludeBaseline,
|
|
437
|
+
source: options.source
|
|
438
|
+
});
|
|
439
|
+
always = always ? Combine(always, mask, (left, right) => left && right) : mask;
|
|
440
|
+
}
|
|
441
|
+
return always;
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* One frame's mask with anything below the noise floor already removed.
|
|
446
|
+
*
|
|
447
|
+
* Built from the components so the floor is applied once, which is what lets two frames be
|
|
448
|
+
* compared as sets that already agree on what counts.
|
|
449
|
+
*/
|
|
450
|
+
CoveredMask(params) {
|
|
451
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
452
|
+
const options = params || {};
|
|
453
|
+
const mask = yield this.MaskAbove(options);
|
|
454
|
+
const floor = options.minTexels == null ? ValueMapReader.MIN_TEXELS : options.minTexels;
|
|
455
|
+
if (floor <= 1) {
|
|
456
|
+
return mask;
|
|
457
|
+
}
|
|
458
|
+
const keep = new Uint8Array(mask.Bits.length);
|
|
459
|
+
for (const members of Components(mask, floor)) {
|
|
460
|
+
for (const texel of members) {
|
|
461
|
+
keep[texel.Row * mask.Width + texel.Column] = 1;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return { Width: mask.Width, Height: mask.Height, Bits: keep };
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* How many texels are over the threshold in each frame.
|
|
469
|
+
*
|
|
470
|
+
* Decoding is local, so the whole series is read without a request and the frames worth
|
|
471
|
+
* querying can be found rather than guessed at.
|
|
472
|
+
*/
|
|
473
|
+
Profile(params) {
|
|
474
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
475
|
+
const options = params || {};
|
|
476
|
+
const floor = options.minTexels == null ? 1 : options.minTexels;
|
|
477
|
+
const counts = [];
|
|
478
|
+
for (let index = 0; index < this.Length; index++) {
|
|
479
|
+
const mask = floor <= 1
|
|
480
|
+
? yield this.MaskAbove({
|
|
481
|
+
threshold: options.threshold,
|
|
482
|
+
frame: index,
|
|
483
|
+
excludeBaseline: options.excludeBaseline,
|
|
484
|
+
source: options.source
|
|
485
|
+
})
|
|
486
|
+
: yield this.CoveredMask({
|
|
487
|
+
threshold: options.threshold,
|
|
488
|
+
frame: index,
|
|
489
|
+
minTexels: floor,
|
|
490
|
+
excludeBaseline: options.excludeBaseline,
|
|
491
|
+
source: options.source
|
|
492
|
+
});
|
|
493
|
+
counts.push(Count(mask));
|
|
494
|
+
}
|
|
495
|
+
return counts;
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* The frames that characterise an event: it arriving, its worst, and it passing.
|
|
500
|
+
*
|
|
501
|
+
* Read off the series rather than named by the caller, since a frame number that suits one
|
|
502
|
+
* archive is quietly wrong for the next and finding the worst is usually the question.
|
|
503
|
+
*/
|
|
504
|
+
FramesOfInterest(params) {
|
|
505
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
506
|
+
const options = params || {};
|
|
507
|
+
const counts = yield this.Profile(options);
|
|
508
|
+
if (!counts.length) {
|
|
509
|
+
return [];
|
|
510
|
+
}
|
|
511
|
+
let peak = 0;
|
|
512
|
+
for (let index = 1; index < counts.length; index++) {
|
|
513
|
+
if (counts[index] > counts[peak]) {
|
|
514
|
+
peak = index;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (!counts[peak]) {
|
|
518
|
+
return [];
|
|
519
|
+
}
|
|
520
|
+
const limit = counts[peak] * (options.fraction == null ? ValueMapReader.ONSET_FRACTION : options.fraction);
|
|
521
|
+
const over = [];
|
|
522
|
+
counts.forEach((count, index) => {
|
|
523
|
+
if (count >= limit) {
|
|
524
|
+
over.push(index);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
// De-duplicated, since a short event can put all three on one frame.
|
|
528
|
+
const picked = [over[0], peak, over[over.length - 1]];
|
|
529
|
+
return picked.filter((at, index) => picked.indexOf(at) === index).sort((a, b) => a - b);
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Each area of one frame over a threshold, as a Bruce geometry ready to query with.
|
|
534
|
+
* Holes are dropped unless asked for.
|
|
535
|
+
*/
|
|
536
|
+
ShapesAbove(params) {
|
|
537
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
538
|
+
const options = params || {};
|
|
539
|
+
const mask = yield this.MaskAbove({
|
|
540
|
+
threshold: options.threshold,
|
|
541
|
+
frame: options.frame,
|
|
542
|
+
excludeBaseline: options.excludeBaseline,
|
|
543
|
+
source: options.source
|
|
544
|
+
});
|
|
545
|
+
return this.ShapesOf(mask, {
|
|
546
|
+
minTexels: options.minTexels,
|
|
547
|
+
keepHoles: options.keepHoles,
|
|
548
|
+
tolerance: options.tolerance
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Each area of any mask as a Bruce geometry, for a caller who built the mask itself.
|
|
554
|
+
*/
|
|
555
|
+
ShapesOf(mask, params) {
|
|
556
|
+
const options = params || {};
|
|
557
|
+
const floor = options.minTexels == null ? ValueMapReader.MIN_TEXELS : options.minTexels;
|
|
558
|
+
const shapes = [];
|
|
559
|
+
for (const members of Components(mask, floor)) {
|
|
560
|
+
const rings = this.RingsOf(members, options.tolerance);
|
|
561
|
+
if (rings.Outer.length < ValueMapReader.MIN_RING_POINTS) {
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
shapes.push(Polygon(rings.Outer, options.keepHoles ? rings.Holes : []));
|
|
565
|
+
}
|
|
566
|
+
return shapes;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* One area's outer ring and its holes, as positions.
|
|
570
|
+
*
|
|
571
|
+
* The longest ring is the outer one, since a hole is enclosed by what surrounds it. Pass a
|
|
572
|
+
* tolerance of 0 to keep every traced vertex.
|
|
573
|
+
*/
|
|
574
|
+
RingsOf(members, tolerance) {
|
|
575
|
+
const traced = Trace(members);
|
|
576
|
+
if (!traced.length) {
|
|
577
|
+
return { Outer: [], Holes: [] };
|
|
578
|
+
}
|
|
579
|
+
traced.sort((left, right) => right.length - left.length);
|
|
580
|
+
const reduce = tolerance == null ? ValueMapReader.SIMPLIFY_TOLERANCE : tolerance;
|
|
581
|
+
const outer = reduce ? Simplify(traced[0], reduce) : traced[0];
|
|
582
|
+
const holes = traced.slice(1)
|
|
583
|
+
.map((hole) => (reduce ? Simplify(hole, reduce) : hole))
|
|
584
|
+
.filter((hole) => hole.length >= 4);
|
|
585
|
+
return {
|
|
586
|
+
Outer: this.Grid.RingToPoints(outer),
|
|
587
|
+
Holes: holes.map((hole) => this.Grid.RingToPoints(hole))
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Walks the series in order, reporting only the ground each frame adds.
|
|
592
|
+
* Most frames add nothing, so onProgress is called for every frame while only the ones that grew come back.
|
|
593
|
+
*/
|
|
594
|
+
NewAreas(params) {
|
|
595
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
596
|
+
const options = params || {};
|
|
597
|
+
const times = this.Times;
|
|
598
|
+
const added = [];
|
|
599
|
+
let seen = null;
|
|
600
|
+
for (let index = 0; index < this.Length; index++) {
|
|
601
|
+
const mask = yield this.CoveredMask({
|
|
602
|
+
threshold: options.threshold,
|
|
603
|
+
frame: index,
|
|
604
|
+
minTexels: options.minTexels,
|
|
605
|
+
excludeBaseline: options.excludeBaseline,
|
|
606
|
+
source: options.source
|
|
607
|
+
});
|
|
608
|
+
const fresh = seen ? Combine(mask, seen, (left, right) => left && !right) : mask;
|
|
609
|
+
seen = seen ? Combine(seen, mask, (left, right) => left || right) : mask;
|
|
610
|
+
// Each area here sits inside one that already cleared the floor, so applying the
|
|
611
|
+
// floor again would drop the thin edge that makes this frame new.
|
|
612
|
+
const shapes = Count(fresh)
|
|
613
|
+
? this.ShapesOf(fresh, { minTexels: 1, tolerance: options.tolerance })
|
|
614
|
+
: [];
|
|
615
|
+
if (options.onProgress) {
|
|
616
|
+
options.onProgress({
|
|
617
|
+
Frame: index,
|
|
618
|
+
Frames: this.Length,
|
|
619
|
+
Time: times[index] || null,
|
|
620
|
+
NewTexels: Count(fresh),
|
|
621
|
+
Added: !!shapes.length
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
if (shapes.length) {
|
|
625
|
+
added.push({
|
|
626
|
+
Frame: index,
|
|
627
|
+
Time: times[index] || null,
|
|
628
|
+
NewTexels: Count(fresh),
|
|
629
|
+
Shapes: shapes,
|
|
630
|
+
New: fresh,
|
|
631
|
+
Cumulative: { Width: seen.Width, Height: seen.Height, Bits: seen.Bits.slice() }
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return added;
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* The texels an Entity covers, from where its record says it is.
|
|
640
|
+
*
|
|
641
|
+
* The raster does not move, so this holds for the whole series and a caller reading every
|
|
642
|
+
* frame works it out once.
|
|
643
|
+
*
|
|
644
|
+
* Ask for geometry to use the Entity's own outline rather than the box around it.
|
|
645
|
+
*/
|
|
646
|
+
TexelsOf(entity, useGeometry = false) {
|
|
647
|
+
if (useGeometry) {
|
|
648
|
+
const rings = RingsOfEntity(entity);
|
|
649
|
+
if (rings.length) {
|
|
650
|
+
const hits = new Map();
|
|
651
|
+
for (const ring of rings) {
|
|
652
|
+
for (const texel of this.Grid.TexelsUnder(ring)) {
|
|
653
|
+
hits.set(texel.Row + ":" + texel.Column, texel);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const found = [];
|
|
657
|
+
hits.forEach((texel) => found.push(texel));
|
|
658
|
+
return found.sort((left, right) => left.Row - right.Row || left.Column - right.Column);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return this.Grid.TexelsIn(bounds_1.Bounds.FromEntity(entity));
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Every value a frame holds over a set of texels, skipping the texels it never sampled.
|
|
665
|
+
*
|
|
666
|
+
* An unsampled texel decodes to the bottom of the archive's range, which reads as a real
|
|
667
|
+
* measurement rather than as an absence, so the coverage mask decides what is reported.
|
|
668
|
+
*/
|
|
669
|
+
ReadingsAt(texels, frame) {
|
|
670
|
+
const readings = [];
|
|
671
|
+
for (const texel of texels || []) {
|
|
672
|
+
const at = texel.Row * frame.Width + texel.Column;
|
|
673
|
+
if (at < 0 || at >= frame.Covered.length || !frame.Covered[at]) {
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
readings.push(frame.Values[at]);
|
|
677
|
+
}
|
|
678
|
+
return readings;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* The highest value a frame holds over a set of texels, or null where it holds nothing.
|
|
682
|
+
*
|
|
683
|
+
* The highest rather than the middle one, because something spanning more than one texel is
|
|
684
|
+
* described by its worst part.
|
|
685
|
+
*/
|
|
686
|
+
PeakAt(texels, frame = 0, source = false) {
|
|
687
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
688
|
+
const readings = this.ReadingsAt(texels, yield this.Frame(frame, source));
|
|
689
|
+
return readings.length ? Math.max.apply(null, readings) : null;
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* The average value a frame holds over a set of texels, or null where it holds nothing.
|
|
694
|
+
* What something spanning several texels experienced overall, as against the worst part.
|
|
695
|
+
*/
|
|
696
|
+
MeanAt(texels, frame = 0, source = false) {
|
|
697
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
698
|
+
const readings = this.ReadingsAt(texels, yield this.Frame(frame, source));
|
|
699
|
+
return readings.length ? Sum(readings) / readings.length : null;
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* How the values over a mask are spread, for a caller reporting on an area.
|
|
704
|
+
*/
|
|
705
|
+
ValuesAt(mask, frame) {
|
|
706
|
+
const selected = [];
|
|
707
|
+
for (let at = 0; at < mask.Bits.length; at++) {
|
|
708
|
+
if (mask.Bits[at] && frame.Covered[at]) {
|
|
709
|
+
selected.push(frame.Values[at]);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (!selected.length) {
|
|
713
|
+
return { Texels: 0 };
|
|
714
|
+
}
|
|
715
|
+
selected.sort((left, right) => left - right);
|
|
716
|
+
const middle = Math.floor(selected.length / 2);
|
|
717
|
+
return {
|
|
718
|
+
Texels: selected.length,
|
|
719
|
+
Peak: selected[selected.length - 1],
|
|
720
|
+
Mean: Sum(selected) / selected.length,
|
|
721
|
+
Median: selected.length % 2
|
|
722
|
+
? selected[middle]
|
|
723
|
+
: (selected[middle - 1] + selected[middle]) / 2
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* The finest tile covering a position, or null when it falls outside every one of them.
|
|
728
|
+
*
|
|
729
|
+
* Tiles overlap, and the finer one is the better answer, so the last match wins over a list
|
|
730
|
+
* that is already sorted coarse first.
|
|
731
|
+
*/
|
|
732
|
+
TileAt(longitude, latitude) {
|
|
733
|
+
let found = null;
|
|
734
|
+
for (const tile of this.Tiles) {
|
|
735
|
+
if (longitude >= tile.West && longitude <= tile.East
|
|
736
|
+
&& latitude >= tile.South && latitude <= tile.North) {
|
|
737
|
+
found = tile;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return found;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* One reading at one place, in both the delivered numbers and the source's own.
|
|
744
|
+
*
|
|
745
|
+
* Reads the single tile the position falls in rather than compositing the whole grid, which
|
|
746
|
+
* is what makes charting one point over a long series cheap.
|
|
747
|
+
*/
|
|
748
|
+
SampleAt(longitude, latitude, frame = 0) {
|
|
749
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
750
|
+
const tile = this.TileAt(longitude, latitude);
|
|
751
|
+
if (!tile) {
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
const position = this.position(frame);
|
|
755
|
+
const times = this.Times;
|
|
756
|
+
const reading = {
|
|
757
|
+
Longitude: longitude,
|
|
758
|
+
Latitude: latitude,
|
|
759
|
+
Frame: position,
|
|
760
|
+
Time: times[position] || null,
|
|
761
|
+
Units: this.Units,
|
|
762
|
+
Texel: this.Grid.TexelAt(longitude, latitude),
|
|
763
|
+
Covered: false,
|
|
764
|
+
Value: null,
|
|
765
|
+
SourceValue: null
|
|
766
|
+
};
|
|
767
|
+
const at = TexelIn(tile, longitude, latitude);
|
|
768
|
+
if (!at) {
|
|
769
|
+
return reading;
|
|
770
|
+
}
|
|
771
|
+
const index = this.Tiles.indexOf(tile);
|
|
772
|
+
const values = yield this.tileLayer(index, value_map_metadata_1.ValueMapMetadata.LAYER_VALUE, position);
|
|
773
|
+
if (!values || !values.Values) {
|
|
774
|
+
return reading;
|
|
775
|
+
}
|
|
776
|
+
const cell = at.Row * values.Width + at.Column;
|
|
777
|
+
if (!values.Covered[cell]) {
|
|
778
|
+
return reading;
|
|
779
|
+
}
|
|
780
|
+
const value = values.Values[cell];
|
|
781
|
+
reading.Covered = true;
|
|
782
|
+
reading.Value = value;
|
|
783
|
+
reading.SourceValue = value;
|
|
784
|
+
if (this.Transform) {
|
|
785
|
+
const shift = yield this.tileLayer(index, value_map_metadata_1.ValueMapMetadata.LAYER_VALUE_SHIFT);
|
|
786
|
+
if (!shift || !shift.Values) {
|
|
787
|
+
throw new Error(this.missingShift());
|
|
788
|
+
}
|
|
789
|
+
reading.SourceValue = shift.Covered[cell] ? value - shift.Values[cell] : null;
|
|
790
|
+
}
|
|
791
|
+
return reading;
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Everything the archive knows about one place, for a caller inspecting a single point.
|
|
796
|
+
*/
|
|
797
|
+
DetailsAt(longitude, latitude, frame = 0) {
|
|
798
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
799
|
+
const reading = yield this.SampleAt(longitude, latitude, frame);
|
|
800
|
+
if (!reading) {
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
const tile = this.TileAt(longitude, latitude);
|
|
804
|
+
const at = TexelIn(tile, longitude, latitude);
|
|
805
|
+
const position = this.Tiles.indexOf(tile);
|
|
806
|
+
return Object.assign({}, reading, {
|
|
807
|
+
Attribute: this.Attribute,
|
|
808
|
+
Label: this.Label,
|
|
809
|
+
Dataset: this.Dataset,
|
|
810
|
+
Frames: this.Length,
|
|
811
|
+
Transform: this.Transform || null,
|
|
812
|
+
VerticalDatum: this.VerticalDatum,
|
|
813
|
+
Tile: {
|
|
814
|
+
Level: tile.Level,
|
|
815
|
+
TexelMetres: tile.TexelMetres,
|
|
816
|
+
ResolutionX: tile.ResolutionX,
|
|
817
|
+
ResolutionY: tile.ResolutionY,
|
|
818
|
+
West: tile.West,
|
|
819
|
+
East: tile.East,
|
|
820
|
+
South: tile.South,
|
|
821
|
+
North: tile.North
|
|
822
|
+
},
|
|
823
|
+
TileTexel: at,
|
|
824
|
+
ValueMin: yield this.tileReading(position, value_map_metadata_1.ValueMapMetadata.LAYER_VALUE_MIN, at),
|
|
825
|
+
ValueMax: yield this.tileReading(position, value_map_metadata_1.ValueMapMetadata.LAYER_VALUE_MAX, at),
|
|
826
|
+
Shift: yield this.tileReading(position, value_map_metadata_1.ValueMapMetadata.LAYER_VALUE_SHIFT, at),
|
|
827
|
+
Baseline: yield this.tileFlag(position, value_map_metadata_1.ValueMapMetadata.LAYER_BASELINE, at)
|
|
828
|
+
});
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Every reading at one place over the whole series, in the order the times run.
|
|
833
|
+
*/
|
|
834
|
+
SeriesAt(longitude, latitude) {
|
|
835
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
836
|
+
const tile = this.TileAt(longitude, latitude);
|
|
837
|
+
if (!tile) {
|
|
838
|
+
return [];
|
|
839
|
+
}
|
|
840
|
+
const at = TexelIn(tile, longitude, latitude);
|
|
841
|
+
const times = this.Times;
|
|
842
|
+
const position = this.Tiles.indexOf(tile);
|
|
843
|
+
const frames = yield this.tileSeries(position, value_map_metadata_1.ValueMapMetadata.LAYER_VALUE);
|
|
844
|
+
const shift = this.Transform
|
|
845
|
+
? yield this.tileLayer(position, value_map_metadata_1.ValueMapMetadata.LAYER_VALUE_SHIFT)
|
|
846
|
+
: null;
|
|
847
|
+
if (this.Transform && (!shift || !shift.Values)) {
|
|
848
|
+
throw new Error(this.missingShift());
|
|
849
|
+
}
|
|
850
|
+
return times.map((time, index) => {
|
|
851
|
+
const reading = {
|
|
852
|
+
Longitude: longitude,
|
|
853
|
+
Latitude: latitude,
|
|
854
|
+
Frame: index,
|
|
855
|
+
Time: time,
|
|
856
|
+
Units: this.Units,
|
|
857
|
+
Texel: this.Grid.TexelAt(longitude, latitude),
|
|
858
|
+
Covered: false,
|
|
859
|
+
Value: null,
|
|
860
|
+
SourceValue: null
|
|
861
|
+
};
|
|
862
|
+
const field = frames[index];
|
|
863
|
+
if (!at || !field || !field.Values) {
|
|
864
|
+
return reading;
|
|
865
|
+
}
|
|
866
|
+
const cell = at.Row * field.Width + at.Column;
|
|
867
|
+
if (!field.Covered[cell]) {
|
|
868
|
+
return reading;
|
|
869
|
+
}
|
|
870
|
+
reading.Covered = true;
|
|
871
|
+
reading.Value = field.Values[cell];
|
|
872
|
+
reading.SourceValue = field.Values[cell];
|
|
873
|
+
if (shift) {
|
|
874
|
+
reading.SourceValue = shift.Covered[cell]
|
|
875
|
+
? field.Values[cell] - shift.Values[cell]
|
|
876
|
+
: null;
|
|
877
|
+
}
|
|
878
|
+
return reading;
|
|
879
|
+
});
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* What one frame holds over an Entity, in both the delivered numbers and the source's own.
|
|
884
|
+
*/
|
|
885
|
+
SampleOf(entity, frame = 0, useGeometry = false) {
|
|
886
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
887
|
+
const texels = this.TexelsOf(entity, useGeometry);
|
|
888
|
+
const times = this.Times;
|
|
889
|
+
const position = frame >= 0 ? frame : times.length + frame;
|
|
890
|
+
const summary = {
|
|
891
|
+
Frame: position,
|
|
892
|
+
Time: times[position] || null,
|
|
893
|
+
Units: this.Units,
|
|
894
|
+
Texels: texels.length,
|
|
895
|
+
Covered: 0,
|
|
896
|
+
Peak: null,
|
|
897
|
+
Mean: null,
|
|
898
|
+
SourcePeak: null,
|
|
899
|
+
SourceMean: null
|
|
900
|
+
};
|
|
901
|
+
if (!texels.length) {
|
|
902
|
+
return summary;
|
|
903
|
+
}
|
|
904
|
+
const readings = this.ReadingsAt(texels, yield this.Frame(position));
|
|
905
|
+
summary.Covered = readings.length;
|
|
906
|
+
if (readings.length) {
|
|
907
|
+
summary.Peak = Math.max.apply(null, readings);
|
|
908
|
+
summary.Mean = Sum(readings) / readings.length;
|
|
909
|
+
}
|
|
910
|
+
if (!this.Transform) {
|
|
911
|
+
summary.SourcePeak = summary.Peak;
|
|
912
|
+
summary.SourceMean = summary.Mean;
|
|
913
|
+
return summary;
|
|
914
|
+
}
|
|
915
|
+
const sourceReadings = this.ReadingsAt(texels, yield this.Frame(position, true));
|
|
916
|
+
if (sourceReadings.length) {
|
|
917
|
+
summary.SourcePeak = Math.max.apply(null, sourceReadings);
|
|
918
|
+
summary.SourceMean = Sum(sourceReadings) / sourceReadings.length;
|
|
919
|
+
}
|
|
920
|
+
return summary;
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* What every frame holds over one Entity, for a caller charting a record through an event.
|
|
925
|
+
*/
|
|
926
|
+
SeriesOf(entity, useGeometry = false) {
|
|
927
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
928
|
+
const summaries = [];
|
|
929
|
+
for (let index = 0; index < this.Length; index++) {
|
|
930
|
+
summaries.push(yield this.SampleOf(entity, index, useGeometry));
|
|
931
|
+
}
|
|
932
|
+
return summaries;
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
identity() {
|
|
936
|
+
return this.Metadata.Identity || {};
|
|
937
|
+
}
|
|
938
|
+
/*
|
|
939
|
+
* A frame index checked against the series, with a negative one counted from the end.
|
|
940
|
+
*/
|
|
941
|
+
position(index) {
|
|
942
|
+
const frames = this.Length;
|
|
943
|
+
if (!frames) {
|
|
944
|
+
throw new Error("This value map has no frames.");
|
|
945
|
+
}
|
|
946
|
+
const at = index >= 0 ? index : frames + index;
|
|
947
|
+
if (at < 0 || at >= frames) {
|
|
948
|
+
throw new Error("Frame " + index + " is outside the " + frames
|
|
949
|
+
+ " this value map holds.");
|
|
950
|
+
}
|
|
951
|
+
return at;
|
|
952
|
+
}
|
|
953
|
+
/*
|
|
954
|
+
* One per-texel statistic over the whole series, in the archive's own units, or null.
|
|
955
|
+
*/
|
|
956
|
+
statistic(name) {
|
|
957
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
958
|
+
if (this.statistics.has(name)) {
|
|
959
|
+
return this.statistics.get(name);
|
|
960
|
+
}
|
|
961
|
+
const field = this.Layer(name) ? yield this.composite(name) : null;
|
|
962
|
+
this.statistics.set(name, field);
|
|
963
|
+
return field;
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
/*
|
|
967
|
+
* One layer drawn onto the composite grid from every tile that carries it.
|
|
968
|
+
*
|
|
969
|
+
* Nearest neighbour throughout: a texel is one mesh sample, and interpolating it with its
|
|
970
|
+
* neighbour produces a reading no cell ever reported.
|
|
971
|
+
*/
|
|
972
|
+
composite(name, index) {
|
|
973
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
974
|
+
const width = this.Grid.Width;
|
|
975
|
+
const height = this.Grid.Height;
|
|
976
|
+
const isMask = this.packing(name) === value_map_metadata_1.ValueMapMetadata.FORMAT_MASK;
|
|
977
|
+
const values = isMask ? null : new Float64Array(width * height);
|
|
978
|
+
const covered = new Uint8Array(width * height);
|
|
979
|
+
const ranges = this.Tiles.map((tile) => value_map_metadata_1.ValueMapMetadata.BytesOf(tile, name, index));
|
|
980
|
+
const rasters = yield this.decodeAll(ranges);
|
|
981
|
+
let drawn = false;
|
|
982
|
+
for (let at = 0; at < this.Tiles.length; at++) {
|
|
983
|
+
const raster = rasters[at];
|
|
984
|
+
if (!raster) {
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
drawn = true;
|
|
988
|
+
this.paste(this.Tiles[at], raster, name, values, covered);
|
|
989
|
+
}
|
|
990
|
+
if (!drawn) {
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
993
|
+
return { Width: width, Height: height, Values: values, Covered: covered };
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
/*
|
|
997
|
+
* One tile's pixels written onto the composite grid, unpacked as they land.
|
|
998
|
+
*/
|
|
999
|
+
paste(tile, raster, name, values, covered) {
|
|
1000
|
+
const rect = value_map_grid_1.ValueMapGrid.TileRect(tile, this.Extent, { Width: this.Grid.Width, Height: this.Grid.Height });
|
|
1001
|
+
const range = this.range(name);
|
|
1002
|
+
const format = this.packing(name);
|
|
1003
|
+
const width = this.Grid.Width;
|
|
1004
|
+
const height = this.Grid.Height;
|
|
1005
|
+
for (let row = 0; row < rect.Height; row++) {
|
|
1006
|
+
const target = rect.Top + row;
|
|
1007
|
+
if (target < 0 || target >= height) {
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
const sourceRow = Math.min(raster.Height - 1, Math.floor((row + 0.5) * raster.Height / rect.Height));
|
|
1011
|
+
for (let column = 0; column < rect.Width; column++) {
|
|
1012
|
+
const across = rect.Left + column;
|
|
1013
|
+
if (across < 0 || across >= width) {
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
const sourceColumn = Math.min(raster.Width - 1, Math.floor((column + 0.5) * raster.Width / rect.Width));
|
|
1017
|
+
const from = (sourceRow * raster.Width + sourceColumn) * 4;
|
|
1018
|
+
const to = target * width + across;
|
|
1019
|
+
if (raster.Data[from + 3] <= value_map_metadata_1.ValueMapMetadata.NO_DATA) {
|
|
1020
|
+
covered[to] = 0;
|
|
1021
|
+
if (values) {
|
|
1022
|
+
values[to] = 0;
|
|
1023
|
+
}
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
covered[to] = 1;
|
|
1027
|
+
if (values) {
|
|
1028
|
+
values[to] = value_map_metadata_1.ValueMapMetadata.Decode(value_map_metadata_1.ValueMapMetadata.PackedAt(raster.Data, from, format), format, range);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
/*
|
|
1034
|
+
* One tile's copy of a layer, unpacked, or null when that tile does not carry it.
|
|
1035
|
+
*
|
|
1036
|
+
* Kept per tile rather than composited, since a point sits in one tile and building the
|
|
1037
|
+
* whole grid to read a single texel costs one image decode per tile in the archive.
|
|
1038
|
+
*/
|
|
1039
|
+
tileLayer(position, name, index) {
|
|
1040
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1041
|
+
const key = position + ":" + name + ":" + (index == null ? "" : index);
|
|
1042
|
+
if (this.tileLayers.has(key)) {
|
|
1043
|
+
const held = this.tileLayers.get(key);
|
|
1044
|
+
this.tileLayers.delete(key);
|
|
1045
|
+
this.tileLayers.set(key, held);
|
|
1046
|
+
return held;
|
|
1047
|
+
}
|
|
1048
|
+
const at = value_map_metadata_1.ValueMapMetadata.BytesOf(this.Tiles[position], name, index);
|
|
1049
|
+
const rasters = yield this.decodeAll([at]);
|
|
1050
|
+
const raster = rasters[0];
|
|
1051
|
+
const field = raster
|
|
1052
|
+
? Unpack(raster, this.packing(name), this.range(name))
|
|
1053
|
+
: null;
|
|
1054
|
+
this.tileLayers.set(key, field);
|
|
1055
|
+
Trim(this.tileLayers, ValueMapReader.TILE_CACHE);
|
|
1056
|
+
return field;
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
/*
|
|
1060
|
+
* One texel's number in a named layer of one tile, or null where the layer does not cover it.
|
|
1061
|
+
*/
|
|
1062
|
+
tileReading(position, name, at) {
|
|
1063
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1064
|
+
if (!at || !this.Layer(name)) {
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
const field = yield this.tileLayer(position, name);
|
|
1068
|
+
if (!field || !field.Values) {
|
|
1069
|
+
return null;
|
|
1070
|
+
}
|
|
1071
|
+
const cell = at.Row * field.Width + at.Column;
|
|
1072
|
+
return field.Covered[cell] ? field.Values[cell] : null;
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
/*
|
|
1076
|
+
* Whether a mask layer flags one texel of one tile.
|
|
1077
|
+
*/
|
|
1078
|
+
tileFlag(position, name, at) {
|
|
1079
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1080
|
+
if (!at || !this.Layer(name)) {
|
|
1081
|
+
return false;
|
|
1082
|
+
}
|
|
1083
|
+
const field = yield this.tileLayer(position, name);
|
|
1084
|
+
if (!field) {
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
return !!field.Covered[at.Row * field.Width + at.Column];
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
/*
|
|
1091
|
+
* One tile's whole per-time series, unpacked, in one read of the blob.
|
|
1092
|
+
*
|
|
1093
|
+
* Kept out of the tile cache on purpose: a long series would evict everything else in it,
|
|
1094
|
+
* and the caller walking a chart holds the result anyway.
|
|
1095
|
+
*/
|
|
1096
|
+
tileSeries(position, name) {
|
|
1097
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1098
|
+
const tile = this.Tiles[position];
|
|
1099
|
+
const ranges = this.Times.map((_time, index) => value_map_metadata_1.ValueMapMetadata.BytesOf(tile, name, index));
|
|
1100
|
+
const rasters = yield this.decodeAll(ranges);
|
|
1101
|
+
const format = this.packing(name);
|
|
1102
|
+
const range = this.range(name);
|
|
1103
|
+
return rasters.map((raster) => raster ? Unpack(raster, format, range) : null);
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
/*
|
|
1107
|
+
* Every named slice decoded, over as few reads of the blob as the layout allows.
|
|
1108
|
+
*/
|
|
1109
|
+
decodeAll(ranges) {
|
|
1110
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1111
|
+
const reads = value_map_metadata_1.ValueMapMetadata.PlanReads(ranges);
|
|
1112
|
+
if (!reads.length) {
|
|
1113
|
+
return ranges.map(() => null);
|
|
1114
|
+
}
|
|
1115
|
+
const blocks = yield Promise.all(reads.map((read) => this.bytes(read.Offset, read.Length)));
|
|
1116
|
+
return Promise.all(ranges.map((at) => {
|
|
1117
|
+
if (!at || !at.Length) {
|
|
1118
|
+
return Promise.resolve(null);
|
|
1119
|
+
}
|
|
1120
|
+
for (let index = 0; index < reads.length; index++) {
|
|
1121
|
+
const read = reads[index];
|
|
1122
|
+
if (at.Offset < read.Offset || at.Offset + at.Length > read.Offset + read.Length) {
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
const from = at.Offset - read.Offset;
|
|
1126
|
+
return this.decoder(blocks[index].subarray(from, from + at.Length))
|
|
1127
|
+
.catch(() => null);
|
|
1128
|
+
}
|
|
1129
|
+
return Promise.resolve(null);
|
|
1130
|
+
}));
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
/*
|
|
1134
|
+
* How a named layer is packed, since a statistics layer can differ from the frames.
|
|
1135
|
+
*/
|
|
1136
|
+
packing(name) {
|
|
1137
|
+
const layer = this.Layer(name);
|
|
1138
|
+
return (layer && layer.Format) || this.Format;
|
|
1139
|
+
}
|
|
1140
|
+
/*
|
|
1141
|
+
* The range a named layer was quantised over, falling back to the frames' own.
|
|
1142
|
+
*/
|
|
1143
|
+
range(name) {
|
|
1144
|
+
return value_map_metadata_1.ValueMapMetadata.LayerRange(this.Metadata, name) || this.ValueRange;
|
|
1145
|
+
}
|
|
1146
|
+
/*
|
|
1147
|
+
* A frame's delivered values as the source's own numbers.
|
|
1148
|
+
*
|
|
1149
|
+
* Nothing to undo is the common case and is not an error: an archive that declares no
|
|
1150
|
+
* transform delivered the source's numbers untouched.
|
|
1151
|
+
*/
|
|
1152
|
+
toSource(frame) {
|
|
1153
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1154
|
+
if (!this.Transform) {
|
|
1155
|
+
return frame;
|
|
1156
|
+
}
|
|
1157
|
+
const shift = yield this.ValueShift();
|
|
1158
|
+
if (!shift || !shift.Values) {
|
|
1159
|
+
throw new Error(this.missingShift());
|
|
1160
|
+
}
|
|
1161
|
+
const values = new Float64Array(frame.Values.length);
|
|
1162
|
+
const covered = new Uint8Array(frame.Covered.length);
|
|
1163
|
+
for (let at = 0; at < values.length; at++) {
|
|
1164
|
+
// A texel the shift never sampled has no source reading to report, whatever the
|
|
1165
|
+
// frame holds.
|
|
1166
|
+
covered[at] = frame.Covered[at] && shift.Covered[at] ? 1 : 0;
|
|
1167
|
+
values[at] = frame.Values[at] - (shift.Covered[at] ? shift.Values[at] : 0);
|
|
1168
|
+
}
|
|
1169
|
+
return { Width: frame.Width, Height: frame.Height, Values: values, Covered: covered };
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
missingShift() {
|
|
1173
|
+
return "This value map declares a " + this.Transform + " transform but publishes no "
|
|
1174
|
+
+ value_map_metadata_1.ValueMapMetadata.LAYER_VALUE_SHIFT + " layer, so the source's own numbers cannot be"
|
|
1175
|
+
+ " recovered.";
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
ValueMapReader.Reader = Reader;
|
|
1179
|
+
function Open(params) {
|
|
1180
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1181
|
+
let file = params.file;
|
|
1182
|
+
if (!file) {
|
|
1183
|
+
if (!params.fileId) {
|
|
1184
|
+
throw new Error("A Client File, or a Client File ID, is required.");
|
|
1185
|
+
}
|
|
1186
|
+
const found = yield client_file_1.ClientFile.Get({ api: params.api, fileId: params.fileId });
|
|
1187
|
+
file = found ? found.clientFile : null;
|
|
1188
|
+
}
|
|
1189
|
+
if (!file) {
|
|
1190
|
+
throw new Error("Client File " + params.fileId + " was not found.");
|
|
1191
|
+
}
|
|
1192
|
+
return new Reader({
|
|
1193
|
+
file: file,
|
|
1194
|
+
bytes: params.bytes || SourceFor(file, params.blob, params.api),
|
|
1195
|
+
decoder: params.decoder,
|
|
1196
|
+
maxCompositeTexels: params.maxCompositeTexels,
|
|
1197
|
+
extent: params.extent
|
|
1198
|
+
});
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
ValueMapReader.Open = Open;
|
|
1202
|
+
/**
|
|
1203
|
+
* Where an archive's bytes come from: a blob already held, or ranged reads of the file's URL.
|
|
1204
|
+
*/
|
|
1205
|
+
function SourceFor(file, blob, api) {
|
|
1206
|
+
if (blob) {
|
|
1207
|
+
const held = blob instanceof Uint8Array ? blob : new Uint8Array(blob);
|
|
1208
|
+
return (offset, length) => Promise.resolve(held.subarray(offset, offset + length));
|
|
1209
|
+
}
|
|
1210
|
+
// The record's own URL where it carries one, since it may be a signed or CDN address that
|
|
1211
|
+
// a reconstructed one would not match.
|
|
1212
|
+
const url = file.URL || client_file_1.ClientFile.GetUrl({ api: api, fileId: file.ID });
|
|
1213
|
+
return (offset, length) => __awaiter(this, void 0, void 0, function* () {
|
|
1214
|
+
const response = yield fetch(url, {
|
|
1215
|
+
headers: { Range: "bytes=" + offset + "-" + (offset + length - 1) }
|
|
1216
|
+
});
|
|
1217
|
+
return new Uint8Array(yield response.arrayBuffer());
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
ValueMapReader.SourceFor = SourceFor;
|
|
1221
|
+
/**
|
|
1222
|
+
* Decodes a PNG where the runtime has an image decoder of its own.
|
|
1223
|
+
*
|
|
1224
|
+
* A browser has one. Node does not, and rather than pull an image library into a client library
|
|
1225
|
+
* that mostly does not decode anything, a caller there passes their own.
|
|
1226
|
+
*/
|
|
1227
|
+
ValueMapReader.DefaultDecoder = (bytes) => __awaiter(this, void 0, void 0, function* () {
|
|
1228
|
+
const globals = typeof globalThis !== "undefined" ? globalThis : {};
|
|
1229
|
+
if (typeof globals.createImageBitmap !== "function" || typeof globals.OffscreenCanvas !== "function") {
|
|
1230
|
+
throw new Error("This runtime has no image decoder, so a value map's pixels cannot be"
|
|
1231
|
+
+ " read here. Pass Decoder to read them, or use the metadata surface, which needs"
|
|
1232
|
+
+ " no decoding.");
|
|
1233
|
+
}
|
|
1234
|
+
const blob = new globals.Blob([bytes], { type: "image/png" });
|
|
1235
|
+
const bitmap = yield globals.createImageBitmap(blob);
|
|
1236
|
+
try {
|
|
1237
|
+
const canvas = new globals.OffscreenCanvas(bitmap.width, bitmap.height);
|
|
1238
|
+
const context = canvas.getContext("2d");
|
|
1239
|
+
context.drawImage(bitmap, 0, 0);
|
|
1240
|
+
const pixels = context.getImageData(0, 0, bitmap.width, bitmap.height);
|
|
1241
|
+
return { Width: bitmap.width, Height: bitmap.height, Data: pixels.data };
|
|
1242
|
+
}
|
|
1243
|
+
finally {
|
|
1244
|
+
if (bitmap.close) {
|
|
1245
|
+
bitmap.close();
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
// A ring needs three distinct corners and the repeat that closes it.
|
|
1250
|
+
ValueMapReader.MIN_RING_POINTS = 4;
|
|
1251
|
+
/**
|
|
1252
|
+
* The disconnected areas in a mask, as 4-connected groups of texels.
|
|
1253
|
+
*/
|
|
1254
|
+
function Components(mask, minTexels = 1) {
|
|
1255
|
+
const width = mask.Width;
|
|
1256
|
+
const height = mask.Height;
|
|
1257
|
+
const seen = new Uint8Array(mask.Bits.length);
|
|
1258
|
+
const found = [];
|
|
1259
|
+
for (let start = 0; start < mask.Bits.length; start++) {
|
|
1260
|
+
if (!mask.Bits[start] || seen[start]) {
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
seen[start] = 1;
|
|
1264
|
+
const pending = [start];
|
|
1265
|
+
const members = [];
|
|
1266
|
+
while (pending.length) {
|
|
1267
|
+
const at = pending.pop();
|
|
1268
|
+
const row = Math.floor(at / width);
|
|
1269
|
+
const column = at - row * width;
|
|
1270
|
+
members.push({ Row: row, Column: column });
|
|
1271
|
+
if (row > 0) {
|
|
1272
|
+
Visit(mask, seen, pending, at - width);
|
|
1273
|
+
}
|
|
1274
|
+
if (row < height - 1) {
|
|
1275
|
+
Visit(mask, seen, pending, at + width);
|
|
1276
|
+
}
|
|
1277
|
+
if (column > 0) {
|
|
1278
|
+
Visit(mask, seen, pending, at - 1);
|
|
1279
|
+
}
|
|
1280
|
+
if (column < width - 1) {
|
|
1281
|
+
Visit(mask, seen, pending, at + 1);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
if (members.length >= minTexels) {
|
|
1285
|
+
found.push(members);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
return found;
|
|
1289
|
+
}
|
|
1290
|
+
ValueMapReader.Components = Components;
|
|
1291
|
+
/**
|
|
1292
|
+
* The closed rings bounding a group of texels, in texel corner coordinates.
|
|
1293
|
+
*/
|
|
1294
|
+
function Trace(members) {
|
|
1295
|
+
const held = new Set();
|
|
1296
|
+
for (const texel of members) {
|
|
1297
|
+
held.add(texel.Row + ":" + texel.Column);
|
|
1298
|
+
}
|
|
1299
|
+
const edges = new Map();
|
|
1300
|
+
const emit = (from, to) => {
|
|
1301
|
+
const outgoing = edges.get(from);
|
|
1302
|
+
if (outgoing) {
|
|
1303
|
+
outgoing.push(to);
|
|
1304
|
+
}
|
|
1305
|
+
else {
|
|
1306
|
+
edges.set(from, [to]);
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
const corner = (x, y) => x + ":" + y;
|
|
1310
|
+
for (const texel of members) {
|
|
1311
|
+
const row = texel.Row;
|
|
1312
|
+
const column = texel.Column;
|
|
1313
|
+
if (!held.has((row - 1) + ":" + column)) {
|
|
1314
|
+
emit(corner(column, row), corner(column + 1, row));
|
|
1315
|
+
}
|
|
1316
|
+
if (!held.has(row + ":" + (column + 1))) {
|
|
1317
|
+
emit(corner(column + 1, row), corner(column + 1, row + 1));
|
|
1318
|
+
}
|
|
1319
|
+
if (!held.has((row + 1) + ":" + column)) {
|
|
1320
|
+
emit(corner(column + 1, row + 1), corner(column, row + 1));
|
|
1321
|
+
}
|
|
1322
|
+
if (!held.has(row + ":" + (column - 1))) {
|
|
1323
|
+
emit(corner(column, row + 1), corner(column, row));
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
const rings = [];
|
|
1327
|
+
const starts = [];
|
|
1328
|
+
edges.forEach((_outgoing, from) => starts.push(from));
|
|
1329
|
+
for (const start of starts) {
|
|
1330
|
+
while ((edges.get(start) || []).length) {
|
|
1331
|
+
const ring = [start];
|
|
1332
|
+
let node = edges.get(start).pop();
|
|
1333
|
+
let closed = true;
|
|
1334
|
+
while (node !== start) {
|
|
1335
|
+
ring.push(node);
|
|
1336
|
+
const outgoing = edges.get(node);
|
|
1337
|
+
if (!outgoing || !outgoing.length) {
|
|
1338
|
+
closed = false;
|
|
1339
|
+
break;
|
|
1340
|
+
}
|
|
1341
|
+
node = outgoing.pop();
|
|
1342
|
+
}
|
|
1343
|
+
if (closed) {
|
|
1344
|
+
ring.push(start);
|
|
1345
|
+
rings.push(ring.map(ToCorner));
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return rings;
|
|
1350
|
+
}
|
|
1351
|
+
ValueMapReader.Trace = Trace;
|
|
1352
|
+
/**
|
|
1353
|
+
* A ring with its texel staircase reduced, by Ramer-Douglas-Peucker.
|
|
1354
|
+
*/
|
|
1355
|
+
function Simplify(ring, tolerance) {
|
|
1356
|
+
const points = ring.slice();
|
|
1357
|
+
if (tolerance <= 0 || points.length < 4) {
|
|
1358
|
+
return points;
|
|
1359
|
+
}
|
|
1360
|
+
// Split before reducing: the algorithm anchors on the endpoints, and a closed ring's two
|
|
1361
|
+
// endpoints are the same point, which would anchor on nothing.
|
|
1362
|
+
const half = Math.floor(points.length / 2);
|
|
1363
|
+
const front = Reduce(points.slice(0, half + 1), tolerance);
|
|
1364
|
+
const back = Reduce(points.slice(half), tolerance);
|
|
1365
|
+
const reduced = front.slice(0, front.length - 1).concat(back);
|
|
1366
|
+
return reduced.length >= 4 ? reduced : points;
|
|
1367
|
+
}
|
|
1368
|
+
ValueMapReader.Simplify = Simplify;
|
|
1369
|
+
/**
|
|
1370
|
+
* How many texels a mask holds.
|
|
1371
|
+
*/
|
|
1372
|
+
function Count(mask) {
|
|
1373
|
+
let total = 0;
|
|
1374
|
+
for (let at = 0; at < mask.Bits.length; at++) {
|
|
1375
|
+
total += mask.Bits[at] ? 1 : 0;
|
|
1376
|
+
}
|
|
1377
|
+
return total;
|
|
1378
|
+
}
|
|
1379
|
+
ValueMapReader.Count = Count;
|
|
1380
|
+
/**
|
|
1381
|
+
* Two masks merged texel by texel.
|
|
1382
|
+
*/
|
|
1383
|
+
function Combine(left, right, rule) {
|
|
1384
|
+
const bits = new Uint8Array(left.Bits.length);
|
|
1385
|
+
for (let at = 0; at < bits.length; at++) {
|
|
1386
|
+
bits[at] = rule(!!left.Bits[at], !!right.Bits[at]) ? 1 : 0;
|
|
1387
|
+
}
|
|
1388
|
+
return { Width: left.Width, Height: left.Height, Bits: bits };
|
|
1389
|
+
}
|
|
1390
|
+
ValueMapReader.Combine = Combine;
|
|
1391
|
+
/**
|
|
1392
|
+
* A polygon geometry from an outer ring and its holes, ready to query Bruce with.
|
|
1393
|
+
*/
|
|
1394
|
+
function Polygon(outer, holes) {
|
|
1395
|
+
const toCartos = (ring) => ring.map((point) => ({
|
|
1396
|
+
longitude: point.Longitude,
|
|
1397
|
+
latitude: point.Latitude,
|
|
1398
|
+
altitude: 0
|
|
1399
|
+
}));
|
|
1400
|
+
const rings = [{
|
|
1401
|
+
LinearRing: geometry_1.Geometry.LineStrFromPoints(toCartos(outer)),
|
|
1402
|
+
Facing: geometry_1.Geometry.EPolygonRingType.Boundaries
|
|
1403
|
+
}];
|
|
1404
|
+
for (const hole of holes || []) {
|
|
1405
|
+
rings.push({
|
|
1406
|
+
LinearRing: geometry_1.Geometry.LineStrFromPoints(toCartos(hole)),
|
|
1407
|
+
Facing: geometry_1.Geometry.EPolygonRingType.Hole
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
return { Polygon: rings };
|
|
1411
|
+
}
|
|
1412
|
+
ValueMapReader.Polygon = Polygon;
|
|
1413
|
+
/**
|
|
1414
|
+
* An Entity's polygon rings, or nothing when its record carries no outline.
|
|
1415
|
+
*/
|
|
1416
|
+
function RingsOfEntity(entity) {
|
|
1417
|
+
const source = entity && entity.Bruce ? entity.Bruce : entity;
|
|
1418
|
+
const geometry = (source && source.VectorGeometry) || {};
|
|
1419
|
+
const rings = [];
|
|
1420
|
+
for (const face of geometry.Polygon || []) {
|
|
1421
|
+
if (!face || !face.LinearRing) {
|
|
1422
|
+
continue;
|
|
1423
|
+
}
|
|
1424
|
+
const points = geometry_1.Geometry.ParsePoints(face.LinearRing);
|
|
1425
|
+
if (points.length >= 3) {
|
|
1426
|
+
rings.push(points.map((point) => ({
|
|
1427
|
+
Longitude: point.longitude,
|
|
1428
|
+
Latitude: point.latitude
|
|
1429
|
+
})));
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
return rings;
|
|
1433
|
+
}
|
|
1434
|
+
ValueMapReader.RingsOfEntity = RingsOfEntity;
|
|
1435
|
+
/*
|
|
1436
|
+
* One decoded raster as its numbers and its own coverage, at the raster's own size.
|
|
1437
|
+
*/
|
|
1438
|
+
function Unpack(raster, format, range) {
|
|
1439
|
+
const isMask = format === value_map_metadata_1.ValueMapMetadata.FORMAT_MASK;
|
|
1440
|
+
const values = isMask ? null : new Float64Array(raster.Width * raster.Height);
|
|
1441
|
+
const covered = new Uint8Array(raster.Width * raster.Height);
|
|
1442
|
+
for (let cell = 0; cell < covered.length; cell++) {
|
|
1443
|
+
const from = cell * 4;
|
|
1444
|
+
if (raster.Data[from + 3] <= value_map_metadata_1.ValueMapMetadata.NO_DATA) {
|
|
1445
|
+
continue;
|
|
1446
|
+
}
|
|
1447
|
+
covered[cell] = 1;
|
|
1448
|
+
if (values) {
|
|
1449
|
+
values[cell] = value_map_metadata_1.ValueMapMetadata.Decode(value_map_metadata_1.ValueMapMetadata.PackedAt(raster.Data, from, format), format, range);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
return { Width: raster.Width, Height: raster.Height, Values: values, Covered: covered };
|
|
1453
|
+
}
|
|
1454
|
+
/*
|
|
1455
|
+
* One neighbour queued, when it is in the mask and has not been reached yet.
|
|
1456
|
+
*/
|
|
1457
|
+
function Visit(mask, seen, pending, at) {
|
|
1458
|
+
if (mask.Bits[at] && !seen[at]) {
|
|
1459
|
+
seen[at] = 1;
|
|
1460
|
+
pending.push(at);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
/*
|
|
1464
|
+
* One run of a ring reduced against the line between its ends.
|
|
1465
|
+
*/
|
|
1466
|
+
function Reduce(run, tolerance) {
|
|
1467
|
+
if (run.length < 3) {
|
|
1468
|
+
return run;
|
|
1469
|
+
}
|
|
1470
|
+
const first = run[0];
|
|
1471
|
+
const last = run[run.length - 1];
|
|
1472
|
+
const dx = last.X - first.X;
|
|
1473
|
+
const dy = last.Y - first.Y;
|
|
1474
|
+
const span = Math.sqrt(dx * dx + dy * dy);
|
|
1475
|
+
let worstIndex = 0;
|
|
1476
|
+
let worst = -1;
|
|
1477
|
+
for (let index = 1; index < run.length - 1; index++) {
|
|
1478
|
+
const point = run[index];
|
|
1479
|
+
const distance = span === 0
|
|
1480
|
+
? Math.sqrt(Math.pow(point.X - first.X, 2) + Math.pow(point.Y - first.Y, 2))
|
|
1481
|
+
: Math.abs(dy * point.X - dx * point.Y + last.X * first.Y - last.Y * first.X) / span;
|
|
1482
|
+
if (distance > worst) {
|
|
1483
|
+
worst = distance;
|
|
1484
|
+
worstIndex = index;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
if (worst <= tolerance) {
|
|
1488
|
+
return [first, last];
|
|
1489
|
+
}
|
|
1490
|
+
const front = Reduce(run.slice(0, worstIndex + 1), tolerance);
|
|
1491
|
+
return front.slice(0, front.length - 1).concat(Reduce(run.slice(worstIndex), tolerance));
|
|
1492
|
+
}
|
|
1493
|
+
/*
|
|
1494
|
+
* A corner key back as its coordinates.
|
|
1495
|
+
*/
|
|
1496
|
+
function ToCorner(key) {
|
|
1497
|
+
const parts = key.split(":");
|
|
1498
|
+
return { X: Number(parts[0]), Y: Number(parts[1]) };
|
|
1499
|
+
}
|
|
1500
|
+
/*
|
|
1501
|
+
* The population standard deviation of a set of counts.
|
|
1502
|
+
*/
|
|
1503
|
+
function Deviation(counts) {
|
|
1504
|
+
if (!counts.length) {
|
|
1505
|
+
return 0;
|
|
1506
|
+
}
|
|
1507
|
+
const mean = Sum(counts) / counts.length;
|
|
1508
|
+
let total = 0;
|
|
1509
|
+
for (const count of counts) {
|
|
1510
|
+
total += Math.pow(count - mean, 2);
|
|
1511
|
+
}
|
|
1512
|
+
return Math.sqrt(total / counts.length);
|
|
1513
|
+
}
|
|
1514
|
+
function Sum(values) {
|
|
1515
|
+
let total = 0;
|
|
1516
|
+
for (const value of values) {
|
|
1517
|
+
total += value;
|
|
1518
|
+
}
|
|
1519
|
+
return total;
|
|
1520
|
+
}
|
|
1521
|
+
/*
|
|
1522
|
+
* Where a position lands inside one tile's own raster.
|
|
1523
|
+
*/
|
|
1524
|
+
function TexelIn(tile, longitude, latitude) {
|
|
1525
|
+
if (tile.East <= tile.West || tile.North <= tile.South) {
|
|
1526
|
+
return null;
|
|
1527
|
+
}
|
|
1528
|
+
const width = tile.ResolutionX;
|
|
1529
|
+
const height = tile.ResolutionY;
|
|
1530
|
+
const column = Math.max(0, Math.min(width - 1, Math.floor((longitude - tile.West) / (tile.East - tile.West) * width)));
|
|
1531
|
+
const row = Math.max(0, Math.min(height - 1, Math.floor((tile.North - latitude) / (tile.North - tile.South) * height)));
|
|
1532
|
+
return { Row: row, Column: column };
|
|
1533
|
+
}
|
|
1534
|
+
/*
|
|
1535
|
+
* The oldest entries dropped once a cache is over its limit.
|
|
1536
|
+
*/
|
|
1537
|
+
function Trim(cache, limit) {
|
|
1538
|
+
while (cache.size > limit) {
|
|
1539
|
+
const oldest = cache.keys().next();
|
|
1540
|
+
if (oldest.done) {
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
cache.delete(oldest.value);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
})(ValueMapReader = exports.ValueMapReader || (exports.ValueMapReader = {}));
|
|
1547
|
+
//# sourceMappingURL=value-map-reader.js.map
|