bruce-cesium 7.2.1 → 7.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bruce-cesium.es5.js +1784 -130
- package/dist/bruce-cesium.es5.js.map +1 -1
- package/dist/bruce-cesium.umd.js +1782 -133
- package/dist/bruce-cesium.umd.js.map +1 -1
- package/dist/lib/bruce-cesium.js +3 -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 +87 -46
- package/dist/lib/rendering/displaced-surface-primitive.js.map +1 -1
- package/dist/lib/rendering/entity-render-engine-polygon.js +214 -26
- package/dist/lib/rendering/entity-render-engine-polygon.js.map +1 -1
- package/dist/lib/rendering/texture-frame-series-animator.js +648 -51
- package/dist/lib/rendering/texture-frame-series-animator.js.map +1 -1
- package/dist/lib/rendering/texture-value-labels.js +729 -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 +3 -1
- package/dist/types/internal/image-utils.d.ts +25 -0
- package/dist/types/rendering/displaced-surface-primitive.d.ts +7 -12
- package/dist/types/rendering/texture-frame-series-animator.d.ts +280 -14
- package/dist/types/rendering/texture-value-labels.d.ts +161 -0
- package/dist/types/rendering/visuals-register.d.ts +1 -0
- package/package.json +2 -2
package/dist/bruce-cesium.umd.js
CHANGED
|
@@ -9033,7 +9033,8 @@
|
|
|
9033
9033
|
entityId: rego.entityId,
|
|
9034
9034
|
menuItemId: rego.menuItemId,
|
|
9035
9035
|
doUpdate: false,
|
|
9036
|
-
requestRender: false
|
|
9036
|
+
requestRender: false,
|
|
9037
|
+
keepVisual: rego.visual
|
|
9037
9038
|
});
|
|
9038
9039
|
}
|
|
9039
9040
|
const entityId = rego.entityId;
|
|
@@ -9326,7 +9327,9 @@
|
|
|
9326
9327
|
exports.EntityLabel.Detatch({
|
|
9327
9328
|
rego
|
|
9328
9329
|
});
|
|
9329
|
-
|
|
9330
|
+
// Never tear down the graphic the caller is registering: the renderers hand back
|
|
9331
|
+
// the same object when they reuse it, so removing it here destroys what replaces it.
|
|
9332
|
+
if (doRemove != false && rego.visual !== params.keepVisual) {
|
|
9330
9333
|
removeEntity(this.viewer, rego.visual);
|
|
9331
9334
|
}
|
|
9332
9335
|
rego.visual = null;
|
|
@@ -9380,7 +9383,9 @@
|
|
|
9380
9383
|
exports.EntityLabel.Detatch({
|
|
9381
9384
|
rego
|
|
9382
9385
|
});
|
|
9383
|
-
|
|
9386
|
+
// Never tear down the graphic the caller is registering: the renderers hand back
|
|
9387
|
+
// the same object when they reuse it, so removing it here destroys what replaces it.
|
|
9388
|
+
if (doRemove != false && rego.visual !== params.keepVisual) {
|
|
9384
9389
|
removeEntity(this.viewer, rego.visual);
|
|
9385
9390
|
}
|
|
9386
9391
|
rego.visual = null;
|
|
@@ -40171,6 +40176,107 @@
|
|
|
40171
40176
|
data[i + 3] = Math.round(srcAlpha * maskAlpha * 255);
|
|
40172
40177
|
}
|
|
40173
40178
|
}
|
|
40179
|
+
/**
|
|
40180
|
+
* The colour a ramp gives a normalised value.
|
|
40181
|
+
* @param sorted stops in ascending position, normalised into the texture's value range
|
|
40182
|
+
*/
|
|
40183
|
+
function SampleRamp(sorted, t) {
|
|
40184
|
+
let upper = 0;
|
|
40185
|
+
while (upper < sorted.length && sorted[upper].position < t) {
|
|
40186
|
+
upper++;
|
|
40187
|
+
}
|
|
40188
|
+
if (upper === 0) {
|
|
40189
|
+
return sorted[0].color;
|
|
40190
|
+
}
|
|
40191
|
+
if (upper >= sorted.length) {
|
|
40192
|
+
return sorted[sorted.length - 1].color;
|
|
40193
|
+
}
|
|
40194
|
+
const a = sorted[upper - 1];
|
|
40195
|
+
const b = sorted[upper];
|
|
40196
|
+
const span = b.position - a.position;
|
|
40197
|
+
// Equal positions are a hard step rather than a division by zero, which is how a band
|
|
40198
|
+
// gets a crisp edge instead of a gradient into its neighbour.
|
|
40199
|
+
const f = span > 0 ? (t - a.position) / span : 1;
|
|
40200
|
+
return {
|
|
40201
|
+
red: a.color.red + (b.color.red - a.color.red) * f,
|
|
40202
|
+
green: a.color.green + (b.color.green - a.color.green) * f,
|
|
40203
|
+
blue: a.color.blue + (b.color.blue - a.color.blue) * f,
|
|
40204
|
+
alpha: a.color.alpha + (b.color.alpha - a.color.alpha) * f
|
|
40205
|
+
};
|
|
40206
|
+
}
|
|
40207
|
+
/**
|
|
40208
|
+
* A ramp baked into RGBA bytes, for a shader that cannot walk stops per fragment.
|
|
40209
|
+
* @param steps entries in the lookup, each covering an equal slice of the value range
|
|
40210
|
+
*/
|
|
40211
|
+
function RampLookup(stops, low, high, steps = 256) {
|
|
40212
|
+
const sorted = stops && stops.length > 0
|
|
40213
|
+
? stops.slice().sort((a, b) => a.position - b.position)
|
|
40214
|
+
: null;
|
|
40215
|
+
const out = new Uint8Array(steps * 4);
|
|
40216
|
+
for (let i = 0; i < steps; i++) {
|
|
40217
|
+
const t = steps > 1 ? i / (steps - 1) : 0;
|
|
40218
|
+
const color = sorted
|
|
40219
|
+
? SampleRamp(sorted, t)
|
|
40220
|
+
: {
|
|
40221
|
+
red: low.red + (high.red - low.red) * t,
|
|
40222
|
+
green: low.green + (high.green - low.green) * t,
|
|
40223
|
+
blue: low.blue + (high.blue - low.blue) * t,
|
|
40224
|
+
alpha: low.alpha + (high.alpha - low.alpha) * t
|
|
40225
|
+
};
|
|
40226
|
+
out[i * 4] = Math.round(color.red);
|
|
40227
|
+
out[i * 4 + 1] = Math.round(color.green);
|
|
40228
|
+
out[i * 4 + 2] = Math.round(color.blue);
|
|
40229
|
+
out[i * 4 + 3] = Math.round(Math.min(1, Math.max(0, color.alpha)) * 255);
|
|
40230
|
+
}
|
|
40231
|
+
return out;
|
|
40232
|
+
}
|
|
40233
|
+
function ApplyGradientStops(imageData, stops) {
|
|
40234
|
+
if (!stops || stops.length === 0) {
|
|
40235
|
+
return;
|
|
40236
|
+
}
|
|
40237
|
+
const sorted = stops.slice().sort((a, b) => a.position - b.position);
|
|
40238
|
+
const data = imageData.data;
|
|
40239
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
40240
|
+
const t = data[i] / 255;
|
|
40241
|
+
const color = SampleRamp(sorted, t);
|
|
40242
|
+
data[i] = Math.round(color.red);
|
|
40243
|
+
data[i + 1] = Math.round(color.green);
|
|
40244
|
+
data[i + 2] = Math.round(color.blue);
|
|
40245
|
+
const srcAlpha = data[i + 3] / 255;
|
|
40246
|
+
data[i + 3] = Math.round(srcAlpha * color.alpha * 255);
|
|
40247
|
+
}
|
|
40248
|
+
}
|
|
40249
|
+
/**
|
|
40250
|
+
* Draws the texture's own cell grid into the tinted pixels.
|
|
40251
|
+
*
|
|
40252
|
+
* Drawn in texture space rather than screen space so it stays with the cells it describes and needs
|
|
40253
|
+
* no per-frame work as the camera moves.
|
|
40254
|
+
*/
|
|
40255
|
+
function ApplyCellBorders(imageData, cellTexels, widthTexels, color) {
|
|
40256
|
+
if (!(cellTexels > 0) || !(widthTexels > 0) || !color || color.alpha <= 0) {
|
|
40257
|
+
return;
|
|
40258
|
+
}
|
|
40259
|
+
const { width, height, data } = imageData;
|
|
40260
|
+
const edge = Math.max(1, Math.round(widthTexels));
|
|
40261
|
+
for (let y = 0; y < height; y++) {
|
|
40262
|
+
const onRow = (y % cellTexels) < edge;
|
|
40263
|
+
for (let x = 0; x < width; x++) {
|
|
40264
|
+
if (!onRow && (x % cellTexels) >= edge) {
|
|
40265
|
+
continue;
|
|
40266
|
+
}
|
|
40267
|
+
const i = (y * width + x) * 4;
|
|
40268
|
+
// Only where something is already drawn, so a border cannot invent coverage the data
|
|
40269
|
+
// does not have.
|
|
40270
|
+
if (data[i + 3] === 0) {
|
|
40271
|
+
continue;
|
|
40272
|
+
}
|
|
40273
|
+
const a = color.alpha;
|
|
40274
|
+
data[i] = Math.round(data[i] * (1 - a) + color.red * a);
|
|
40275
|
+
data[i + 1] = Math.round(data[i + 1] * (1 - a) + color.green * a);
|
|
40276
|
+
data[i + 2] = Math.round(data[i + 2] * (1 - a) + color.blue * a);
|
|
40277
|
+
}
|
|
40278
|
+
}
|
|
40279
|
+
}
|
|
40174
40280
|
function loadImage(src) {
|
|
40175
40281
|
return new Promise((res, rej) => {
|
|
40176
40282
|
const image = new Image();
|
|
@@ -40180,28 +40286,318 @@
|
|
|
40180
40286
|
});
|
|
40181
40287
|
}
|
|
40182
40288
|
|
|
40183
|
-
var TextureFrameSeriesAnimator;
|
|
40184
40289
|
(function (TextureFrameSeriesAnimator) {
|
|
40290
|
+
TextureFrameSeriesAnimator.LAYOUT_TILES = "tiles";
|
|
40291
|
+
// The only generation this renderer draws. Anything else is refused rather than half read: the
|
|
40292
|
+
// shapes differ enough that a partial reading produces a plausible wrong surface, which is worse
|
|
40293
|
+
// than an empty one.
|
|
40294
|
+
TextureFrameSeriesAnimator.SUPPORTED_VERSION = 3;
|
|
40295
|
+
// Value in R at 8 bits, or high in R and low in G at 16.
|
|
40296
|
+
TextureFrameSeriesAnimator.FORMAT_U8 = "u8";
|
|
40297
|
+
TextureFrameSeriesAnimator.FORMAT_RG16 = "rg16";
|
|
40298
|
+
// Declared as a field with one value, so a second mode can be added later without an absent
|
|
40299
|
+
// Encoding having to mean anything.
|
|
40300
|
+
TextureFrameSeriesAnimator.ENCODING_ABSOLUTE = "absolute";
|
|
40301
|
+
TextureFrameSeriesAnimator.LAYER_VALUE = "Value";
|
|
40302
|
+
TextureFrameSeriesAnimator.LAYER_VALUE_MIN = "ValueMin";
|
|
40303
|
+
TextureFrameSeriesAnimator.LAYER_VALUE_MAX = "ValueMax";
|
|
40304
|
+
TextureFrameSeriesAnimator.LAYER_BASELINE = "Baseline";
|
|
40305
|
+
// The per-texel correction that was applied, published only on an archive that was transformed.
|
|
40306
|
+
TextureFrameSeriesAnimator.LAYER_VALUE_SHIFT = "ValueShift";
|
|
40307
|
+
/**
|
|
40308
|
+
* Turns a published value map into the flat shape this renderer works in, or null when it cannot be drawn.
|
|
40309
|
+
* @param published the archive's Data.generation
|
|
40310
|
+
*/
|
|
40311
|
+
function Adapt(published) {
|
|
40312
|
+
if (!published || published.Version !== TextureFrameSeriesAnimator.SUPPORTED_VERSION) {
|
|
40313
|
+
const found = published && published.Version != null ? published.Version : "none";
|
|
40314
|
+
const at = published && published.Identity ? published.Identity.Generated : undefined;
|
|
40315
|
+
// In theory should be no files.
|
|
40316
|
+
console.warn("bruce-cesium: value map version " + found + " is not supported (this build reads "
|
|
40317
|
+
+ TextureFrameSeriesAnimator.SUPPORTED_VERSION + "), so nothing will be drawn for it."
|
|
40318
|
+
+ (at ? " The archive was generated at " + at + " and needs regenerating." : ""));
|
|
40319
|
+
return null;
|
|
40320
|
+
}
|
|
40321
|
+
const geometry = published.Geometry || {};
|
|
40322
|
+
const tiles = published.Tiles || [];
|
|
40323
|
+
if (geometry.Layout !== TextureFrameSeriesAnimator.LAYOUT_TILES || tiles.length === 0) {
|
|
40324
|
+
const at = published.Identity ? published.Identity.Generated : undefined;
|
|
40325
|
+
console.warn("bruce-cesium: value map declares no tiles, so nothing will be drawn for it."
|
|
40326
|
+
+ (at ? " The archive was generated at " + at + " and needs regenerating." : ""));
|
|
40327
|
+
return null;
|
|
40328
|
+
}
|
|
40329
|
+
const value = published.Value || {};
|
|
40330
|
+
if (value.Encoding && value.Encoding !== TextureFrameSeriesAnimator.ENCODING_ABSOLUTE) {
|
|
40331
|
+
const at = published.Identity ? published.Identity.Generated : undefined;
|
|
40332
|
+
console.warn("bruce-cesium: value map encoding " + value.Encoding + " is not supported"
|
|
40333
|
+
+ " (this build reads " + TextureFrameSeriesAnimator.ENCODING_ABSOLUTE + "), so nothing will be drawn for it."
|
|
40334
|
+
+ (at ? " The archive was generated at " + at + " and needs regenerating." : ""));
|
|
40335
|
+
return null;
|
|
40336
|
+
}
|
|
40337
|
+
const series = published.Series || {};
|
|
40338
|
+
const layerRange = declaredRange(published, TextureFrameSeriesAnimator.LAYER_VALUE_MIN);
|
|
40339
|
+
const flat = {
|
|
40340
|
+
Frames: [],
|
|
40341
|
+
ValueMin: value.Min,
|
|
40342
|
+
ValueMax: value.Max,
|
|
40343
|
+
Format: value.Format || TextureFrameSeriesAnimator.FORMAT_U8,
|
|
40344
|
+
Encoding: value.Encoding || TextureFrameSeriesAnimator.ENCODING_ABSOLUTE,
|
|
40345
|
+
Units: value.Units,
|
|
40346
|
+
West: geometry.West,
|
|
40347
|
+
East: geometry.East,
|
|
40348
|
+
South: geometry.South,
|
|
40349
|
+
North: geometry.North,
|
|
40350
|
+
ResolutionX: geometry.ResolutionX,
|
|
40351
|
+
ResolutionY: geometry.ResolutionY,
|
|
40352
|
+
PixelPlacement: geometry.PixelPlacement,
|
|
40353
|
+
Times: series.Times,
|
|
40354
|
+
ExtremesValueMin: layerRange ? layerRange.min : undefined,
|
|
40355
|
+
ExtremesValueMax: layerRange ? layerRange.max : undefined,
|
|
40356
|
+
// The delivered values are what gets drawn. What the source called them, and any shift
|
|
40357
|
+
// applied on the way, travel for labels and analysis rather than for placement.
|
|
40358
|
+
SourceVerticalDatum: published.ValueSource ? published.ValueSource.VerticalDatum : undefined,
|
|
40359
|
+
Transform: published.ValueSource ? published.ValueSource.Transform : undefined,
|
|
40360
|
+
SourceUnits: published.ValueSource ? published.ValueSource.Units : undefined,
|
|
40361
|
+
SourceValueMin: published.ValueSource ? published.ValueSource.Min : undefined,
|
|
40362
|
+
SourceValueMax: published.ValueSource ? published.ValueSource.Max : undefined
|
|
40363
|
+
};
|
|
40364
|
+
flat.Tiles = tiles.map((tile) => adaptTile(tile, series.Times));
|
|
40365
|
+
// The coarsest tile covers the whole extent, so it is what a caller wanting one time list reads.
|
|
40366
|
+
flat.Frames = flat.Tiles[0].Frames;
|
|
40367
|
+
warnIfUnshifted(published, flat);
|
|
40368
|
+
return flat;
|
|
40369
|
+
}
|
|
40370
|
+
TextureFrameSeriesAnimator.Adapt = Adapt;
|
|
40371
|
+
/*
|
|
40372
|
+
* Says so when an archive's heights are not on the ellipsoid they will be drawn against.
|
|
40373
|
+
*/
|
|
40374
|
+
function warnIfUnshifted(published, flat) {
|
|
40375
|
+
const datum = flat.SourceVerticalDatum;
|
|
40376
|
+
if (!datum || datum === TextureFrameSeriesAnimator.DATUM_ELLIPSOIDAL || flat.Transform) {
|
|
40377
|
+
return;
|
|
40378
|
+
}
|
|
40379
|
+
const at = published.Identity ? published.Identity.Generated : undefined;
|
|
40380
|
+
console.warn("bruce-cesium: value map heights are in " + datum + " and report no transform, so"
|
|
40381
|
+
+ " they are not ellipsoidal and will draw at the wrong height when placed absolutely."
|
|
40382
|
+
+ (at ? " The archive was generated at " + at + " and needs regenerating with the shift"
|
|
40383
|
+
+ " applied." : ""));
|
|
40384
|
+
}
|
|
40385
|
+
/*
|
|
40386
|
+
* The Min and Max a named layer declares, since a statistics layer is encoded over the true data
|
|
40387
|
+
* range rather than over the frames' own.
|
|
40388
|
+
*/
|
|
40389
|
+
function declaredRange(published, name) {
|
|
40390
|
+
const layers = published.Layers || [];
|
|
40391
|
+
for (const layer of layers) {
|
|
40392
|
+
if (layer.Name === name && typeof layer.Min === "number" && typeof layer.Max === "number") {
|
|
40393
|
+
return { min: layer.Min, max: layer.Max };
|
|
40394
|
+
}
|
|
40395
|
+
}
|
|
40396
|
+
return null;
|
|
40397
|
+
}
|
|
40398
|
+
/*
|
|
40399
|
+
* One tile, with its per-time value rasters lined up against the shared time list.
|
|
40400
|
+
*/
|
|
40401
|
+
function adaptTile(tile, times) {
|
|
40402
|
+
const layers = tile.Layers || {};
|
|
40403
|
+
const perTime = layers[TextureFrameSeriesAnimator.LAYER_VALUE] || [];
|
|
40404
|
+
const frames = perTime.map((entry, index) => ({
|
|
40405
|
+
Timestamp: times && times[index] ? times[index] : "",
|
|
40406
|
+
ByteOffset: entry.Bytes.Offset,
|
|
40407
|
+
ByteLength: entry.Bytes.Length
|
|
40408
|
+
}));
|
|
40409
|
+
return {
|
|
40410
|
+
Level: tile.Level,
|
|
40411
|
+
X: tile.X,
|
|
40412
|
+
Y: tile.Y,
|
|
40413
|
+
West: tile.West,
|
|
40414
|
+
East: tile.East,
|
|
40415
|
+
South: tile.South,
|
|
40416
|
+
North: tile.North,
|
|
40417
|
+
TexelMetres: tile.TexelMetres,
|
|
40418
|
+
ResolutionX: tile.ResolutionX,
|
|
40419
|
+
ResolutionY: tile.ResolutionY,
|
|
40420
|
+
Frames: frames,
|
|
40421
|
+
Floor: single(layers[TextureFrameSeriesAnimator.LAYER_VALUE_MIN]),
|
|
40422
|
+
Ceiling: single(layers[TextureFrameSeriesAnimator.LAYER_VALUE_MAX]),
|
|
40423
|
+
BaselineMask: single(layers[TextureFrameSeriesAnimator.LAYER_BASELINE])
|
|
40424
|
+
};
|
|
40425
|
+
}
|
|
40426
|
+
function single(layer) {
|
|
40427
|
+
if (!layer || !layer.Bytes) {
|
|
40428
|
+
return undefined;
|
|
40429
|
+
}
|
|
40430
|
+
return { ByteOffset: layer.Bytes.Offset, ByteLength: layer.Bytes.Length };
|
|
40431
|
+
}
|
|
40432
|
+
/**
|
|
40433
|
+
* How to turn a delivered value into the number the source published, or null when the archive
|
|
40434
|
+
* does not say enough to do it.
|
|
40435
|
+
* @param metadata the archive's Data.generation
|
|
40436
|
+
*/
|
|
40437
|
+
function SourceMap(metadata) {
|
|
40438
|
+
if (!metadata) {
|
|
40439
|
+
return null;
|
|
40440
|
+
}
|
|
40441
|
+
const { ValueMin, ValueMax, SourceValueMin, SourceValueMax } = metadata;
|
|
40442
|
+
if (typeof ValueMin !== "number" || typeof ValueMax !== "number"
|
|
40443
|
+
|| typeof SourceValueMin !== "number" || typeof SourceValueMax !== "number") {
|
|
40444
|
+
return null;
|
|
40445
|
+
}
|
|
40446
|
+
const delivered = ValueMax - ValueMin;
|
|
40447
|
+
// A single delivered value describes no line, so all that can be recovered is where it sits.
|
|
40448
|
+
const scale = delivered !== 0 ? (SourceValueMax - SourceValueMin) / delivered : 1;
|
|
40449
|
+
return { scale, offset: SourceValueMin - ValueMin * scale };
|
|
40450
|
+
}
|
|
40451
|
+
TextureFrameSeriesAnimator.SourceMap = SourceMap;
|
|
40452
|
+
/**
|
|
40453
|
+
* A delivered value in the source's own terms, or unchanged when it cannot be turned back.
|
|
40454
|
+
* @param map from SourceMap(), null when the archive does not say enough
|
|
40455
|
+
*/
|
|
40456
|
+
function ToSource(map, value) {
|
|
40457
|
+
return map ? value * map.scale + map.offset : value;
|
|
40458
|
+
}
|
|
40459
|
+
TextureFrameSeriesAnimator.ToSource = ToSource;
|
|
40460
|
+
/**
|
|
40461
|
+
* Whether the archive's frames carry a 16 bit value packed across R and G.
|
|
40462
|
+
*
|
|
40463
|
+
* Every consumer that reads a texel has to ask: taking R alone from a packed frame yields a
|
|
40464
|
+
* plausible surface quantised to 255 steps of the full range rather than an obvious failure.
|
|
40465
|
+
* @param metadata the archive's Data.generation
|
|
40466
|
+
*/
|
|
40467
|
+
function IsPackedValue(metadata) {
|
|
40468
|
+
return Boolean(metadata && metadata.Format === TextureFrameSeriesAnimator.FORMAT_RG16);
|
|
40469
|
+
}
|
|
40470
|
+
TextureFrameSeriesAnimator.IsPackedValue = IsPackedValue;
|
|
40471
|
+
/**
|
|
40472
|
+
* The normalised 0 to 1 value at a texel, from whichever format the archive packed it in.
|
|
40473
|
+
* @param metadata the archive's Data.generation
|
|
40474
|
+
* @param pixels untinted RGBA value pixels
|
|
40475
|
+
* @param at index of the texel's red channel
|
|
40476
|
+
*/
|
|
40477
|
+
function NormalisedAt(metadata, pixels, at) {
|
|
40478
|
+
if (IsPackedValue(metadata)) {
|
|
40479
|
+
return (pixels[at] * 256 + pixels[at + 1]) / 65535;
|
|
40480
|
+
}
|
|
40481
|
+
return pixels[at] / 255;
|
|
40482
|
+
}
|
|
40483
|
+
TextureFrameSeriesAnimator.NormalisedAt = NormalisedAt;
|
|
40484
|
+
// Values already sit on the ellipsoid Cesium measures against, so nothing is added to place them.
|
|
40485
|
+
TextureFrameSeriesAnimator.DATUM_ELLIPSOIDAL = "WGS84";
|
|
40486
|
+
/**
|
|
40487
|
+
* Whether the values are heights against a vertical datum rather than a thickness.
|
|
40488
|
+
*
|
|
40489
|
+
* A thickness rises from wherever the polygon sits and a height already knows where it belongs, so
|
|
40490
|
+
* this is what decides whether the polygon's own altitude may be added underneath it.
|
|
40491
|
+
* @param metadata the archive's Data.generation
|
|
40492
|
+
*/
|
|
40493
|
+
function IsDatumHeight(metadata) {
|
|
40494
|
+
return Boolean(metadata && metadata.SourceVerticalDatum);
|
|
40495
|
+
}
|
|
40496
|
+
TextureFrameSeriesAnimator.IsDatumHeight = IsDatumHeight;
|
|
40497
|
+
/**
|
|
40498
|
+
* The archive's tiles, which Adapt() guarantees are present.
|
|
40499
|
+
* @param metadata the archive's Data.generation
|
|
40500
|
+
*/
|
|
40501
|
+
function TilesOf(metadata) {
|
|
40502
|
+
return (metadata && metadata.Tiles) || [];
|
|
40503
|
+
}
|
|
40504
|
+
TextureFrameSeriesAnimator.TilesOf = TilesOf;
|
|
40505
|
+
/**
|
|
40506
|
+
* Turns a value in the attribute's own units into the 0 to 1 position the ramp is indexed by.
|
|
40507
|
+
*
|
|
40508
|
+
* Stops are authored in real units, since "hide anything under 0.1 m" is the sentence a user
|
|
40509
|
+
* actually has, and the archive's own published range is what makes that expressible.
|
|
40510
|
+
* @param metadata the archive's Data.generation
|
|
40511
|
+
* @param value in the attribute's units
|
|
40512
|
+
*/
|
|
40513
|
+
function NormalisePosition(metadata, value) {
|
|
40514
|
+
const lo = metadata && typeof metadata.ValueMin === "number" ? metadata.ValueMin : 0;
|
|
40515
|
+
const hi = metadata && typeof metadata.ValueMax === "number" ? metadata.ValueMax : 1;
|
|
40516
|
+
const span = hi - lo;
|
|
40517
|
+
if (!(span > 0)) {
|
|
40518
|
+
return 0;
|
|
40519
|
+
}
|
|
40520
|
+
return Math.min(1, Math.max(0, (value - lo) / span));
|
|
40521
|
+
}
|
|
40522
|
+
TextureFrameSeriesAnimator.NormalisePosition = NormalisePosition;
|
|
40185
40523
|
/**
|
|
40186
40524
|
* Detects whether a ClientFile's `Data.generation` metadata describes a frame archive rather than a single static image,
|
|
40187
40525
|
* so a caller can decide whether to construct an Animator or fall back to the existing static-texture path.
|
|
40188
|
-
* @param data ClientFile.Data
|
|
40526
|
+
* @param data ClientFile.Data
|
|
40189
40527
|
*/
|
|
40190
40528
|
function IsFrameArchiveMetadata(data) {
|
|
40191
|
-
|
|
40529
|
+
const generation = data && data.generation;
|
|
40530
|
+
if (!generation) {
|
|
40531
|
+
return false;
|
|
40532
|
+
}
|
|
40533
|
+
return (typeof generation.Version === "number" ||
|
|
40534
|
+
(Array.isArray(generation.Frames) && generation.Frames.length > 0));
|
|
40192
40535
|
}
|
|
40193
40536
|
TextureFrameSeriesAnimator.IsFrameArchiveMetadata = IsFrameArchiveMetadata;
|
|
40194
|
-
//
|
|
40537
|
+
// First resolvable static number out of a calculator field list, since that is all a border needs.
|
|
40538
|
+
function resolveNumber(fields) {
|
|
40539
|
+
if (!fields) {
|
|
40540
|
+
return 0;
|
|
40541
|
+
}
|
|
40542
|
+
for (const field of fields) {
|
|
40543
|
+
const value = Number(field && field.value);
|
|
40544
|
+
if (Number.isFinite(value)) {
|
|
40545
|
+
return value;
|
|
40546
|
+
}
|
|
40547
|
+
}
|
|
40548
|
+
return 0;
|
|
40549
|
+
}
|
|
40550
|
+
// First resolvable static colour out of a calculator field list.
|
|
40551
|
+
function resolveColor(fields) {
|
|
40552
|
+
if (!fields) {
|
|
40553
|
+
return null;
|
|
40554
|
+
}
|
|
40555
|
+
for (const field of fields) {
|
|
40556
|
+
const parsed = typeof (field === null || field === void 0 ? void 0 : field.value) === "string"
|
|
40557
|
+
? BModels.Color.ColorFromStr(field.value)
|
|
40558
|
+
: null;
|
|
40559
|
+
if (parsed) {
|
|
40560
|
+
return parsed;
|
|
40561
|
+
}
|
|
40562
|
+
}
|
|
40563
|
+
return null;
|
|
40564
|
+
}
|
|
40565
|
+
/*
|
|
40566
|
+
* Everything about an Animator that a style can change, as a comparable string.
|
|
40567
|
+
*/
|
|
40568
|
+
function AppearanceSignature(options) {
|
|
40569
|
+
var _a, _b, _c;
|
|
40570
|
+
return JSON.stringify([
|
|
40571
|
+
(_a = options.textureColorMask) !== null && _a !== void 0 ? _a : null,
|
|
40572
|
+
(_b = options.cellBorder) !== null && _b !== void 0 ? _b : null,
|
|
40573
|
+
(_c = options.cellTexels) !== null && _c !== void 0 ? _c : null,
|
|
40574
|
+
Boolean(options.maskBaseline)
|
|
40575
|
+
]);
|
|
40576
|
+
}
|
|
40577
|
+
TextureFrameSeriesAnimator.AppearanceSignature = AppearanceSignature;
|
|
40578
|
+
const MAX_COMPOSITE_TEXELS = 2048;
|
|
40195
40579
|
const VALUE_DILATE_PASSES = 2;
|
|
40196
40580
|
/*
|
|
40197
|
-
*
|
|
40581
|
+
* Rewrites a packed 16 bit value as the 8 bit grey the colour ramps index on.
|
|
40198
40582
|
*
|
|
40199
|
-
*
|
|
40200
|
-
*
|
|
40201
|
-
|
|
40202
|
-
|
|
40583
|
+
* All three channels, since a ramp is free to read any of them and a leftover low byte in G would
|
|
40584
|
+
* show up as a green cast on the one that reads them all.
|
|
40585
|
+
*/
|
|
40586
|
+
function flattenPacked(pixels, packed) {
|
|
40587
|
+
if (!packed) {
|
|
40588
|
+
return;
|
|
40589
|
+
}
|
|
40590
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
40591
|
+
const grey = Math.round((pixels[i] * 256 + pixels[i + 1]) / 65535 * 255);
|
|
40592
|
+
pixels[i] = grey;
|
|
40593
|
+
pixels[i + 1] = grey;
|
|
40594
|
+
pixels[i + 2] = grey;
|
|
40595
|
+
}
|
|
40596
|
+
}
|
|
40597
|
+
/*
|
|
40598
|
+
* Bleeds covered values outward into uncovered texels, leaving alpha untouched.
|
|
40203
40599
|
*/
|
|
40204
|
-
function dilateValues(value, width, height, passes) {
|
|
40600
|
+
function dilateValues(value, width, height, passes, packed) {
|
|
40205
40601
|
const texels = width * height;
|
|
40206
40602
|
const filled = new Uint8Array(texels);
|
|
40207
40603
|
for (let p = 0; p < texels; p++) {
|
|
@@ -40225,15 +40621,25 @@
|
|
|
40225
40621
|
if (nx < 0 || ny < 0 || nx >= width || ny >= height || !wasFilled[ny * width + nx]) {
|
|
40226
40622
|
continue;
|
|
40227
40623
|
}
|
|
40228
|
-
|
|
40624
|
+
const at = (ny * width + nx) * 4;
|
|
40625
|
+
// Averaged as a value rather than per byte: averaging a packed low byte
|
|
40626
|
+
// on its own wraps at every 256th step and speckles the bled edge.
|
|
40627
|
+
sum += packed ? source[at] * 256 + source[at + 1] : source[at];
|
|
40229
40628
|
hits++;
|
|
40230
40629
|
}
|
|
40231
40630
|
}
|
|
40232
40631
|
if (hits > 0) {
|
|
40233
40632
|
const v = Math.round(sum / hits);
|
|
40234
|
-
|
|
40235
|
-
|
|
40236
|
-
|
|
40633
|
+
if (packed) {
|
|
40634
|
+
value[p * 4] = (v >> 8) & 255;
|
|
40635
|
+
value[p * 4 + 1] = v & 255;
|
|
40636
|
+
value[p * 4 + 2] = 0;
|
|
40637
|
+
}
|
|
40638
|
+
else {
|
|
40639
|
+
value[p * 4] = v;
|
|
40640
|
+
value[p * 4 + 1] = v;
|
|
40641
|
+
value[p * 4 + 2] = v;
|
|
40642
|
+
}
|
|
40237
40643
|
filled[p] = 1;
|
|
40238
40644
|
}
|
|
40239
40645
|
}
|
|
@@ -40245,7 +40651,7 @@
|
|
|
40245
40651
|
const DEFAULT_HIGH_COLOR = { red: 21, green: 96, blue: 196, alpha: 0.92 };
|
|
40246
40652
|
class Animator {
|
|
40247
40653
|
constructor(options) {
|
|
40248
|
-
var _a, _b;
|
|
40654
|
+
var _a, _b, _c;
|
|
40249
40655
|
this.removeOnTick = null;
|
|
40250
40656
|
this.removeCrossfadeTick = null;
|
|
40251
40657
|
this.disposed = false;
|
|
@@ -40273,6 +40679,9 @@
|
|
|
40273
40679
|
this.poolIdx = 0;
|
|
40274
40680
|
this.valueDims = null;
|
|
40275
40681
|
this.presentVersion = 0;
|
|
40682
|
+
this.extremesLoad = null;
|
|
40683
|
+
this.floorPixels = null;
|
|
40684
|
+
this.ceilingPixels = null;
|
|
40276
40685
|
if (!options.entity.polygon) {
|
|
40277
40686
|
throw new Error("TextureFrameSeriesAnimator requires an entity with polygon graphics.");
|
|
40278
40687
|
}
|
|
@@ -40284,16 +40693,40 @@
|
|
|
40284
40693
|
this.archiveUrl = options.archiveUrl;
|
|
40285
40694
|
this.frames = options.frames;
|
|
40286
40695
|
this.crossfadeMs = (_a = options.crossfadeMs) !== null && _a !== void 0 ? _a : DEFAULT_CROSSFADE_MS;
|
|
40696
|
+
this.appearance = AppearanceSignature(options);
|
|
40287
40697
|
const mask = options.textureColorMask;
|
|
40288
40698
|
this.lowColor = (mask && BModels.Color.ColorFromStr(mask.minColor)) || DEFAULT_LOW_COLOR;
|
|
40289
40699
|
this.highColor = (mask && BModels.Color.ColorFromStr(mask.maxColor)) || DEFAULT_HIGH_COLOR;
|
|
40700
|
+
// Positions are authored in the attribute's units and normalised once here, so the
|
|
40701
|
+
// per-pixel loop stays a comparison against 0 to 1 like the two-colour path.
|
|
40702
|
+
const points = mask && mask.points;
|
|
40703
|
+
this.rampStops = (points && points.length > 0)
|
|
40704
|
+
? points.map((p) => ({
|
|
40705
|
+
position: NormalisePosition(options.metadata, p.position),
|
|
40706
|
+
color: BModels.Color.ColorFromStr(p.color) || DEFAULT_LOW_COLOR
|
|
40707
|
+
}))
|
|
40708
|
+
: null;
|
|
40709
|
+
const border = options.cellBorder;
|
|
40710
|
+
const borderWidth = border ? resolveNumber(border.width) : 0;
|
|
40711
|
+
const borderColor = border ? resolveColor(border.color) : null;
|
|
40712
|
+
// Zero width or a fully transparent colour means no grid, matching how the polygon's own
|
|
40713
|
+
// outline behaves, so an existing style that wants no border keeps getting none.
|
|
40714
|
+
this.cellBorder = (border && borderWidth > 0 && borderColor && borderColor.alpha > 0)
|
|
40715
|
+
? {
|
|
40716
|
+
cellTexels: Math.max(1, Math.round((_b = options.cellTexels) !== null && _b !== void 0 ? _b : 1)),
|
|
40717
|
+
widthTexels: borderWidth,
|
|
40718
|
+
color: borderColor
|
|
40719
|
+
}
|
|
40720
|
+
: null;
|
|
40290
40721
|
this.frameDates = this.frames.map((f) => Cesium.JulianDate.fromIso8601(f.Timestamp));
|
|
40291
40722
|
this.frameCache = new Array(this.frames.length).fill(null);
|
|
40292
40723
|
this.frameDims = new Array(this.frames.length).fill(null);
|
|
40293
40724
|
this.valueCache = new Array(this.frames.length).fill(null);
|
|
40294
40725
|
this.produceValueCanvas = Boolean(options.produceValueCanvas);
|
|
40295
|
-
this.
|
|
40296
|
-
this.
|
|
40726
|
+
this.metadata = options.metadata;
|
|
40727
|
+
this.drapeExtent = options.drapeExtent;
|
|
40728
|
+
this.tiles = TilesOf(options.metadata);
|
|
40729
|
+
this.maskBaseline = (_c = options.maskBaseline) !== null && _c !== void 0 ? _c : this.tiles.some((tile) => Boolean(tile.BaselineMask));
|
|
40297
40730
|
this.valueCanvas = this.produceValueCanvas ? document.createElement("canvas") : null;
|
|
40298
40731
|
this.driveMaterial = options.driveMaterial !== false;
|
|
40299
40732
|
this.originalMaterial = options.entity.polygon.material;
|
|
@@ -40329,6 +40762,10 @@
|
|
|
40329
40762
|
IsDisposed() {
|
|
40330
40763
|
return this.disposed;
|
|
40331
40764
|
}
|
|
40765
|
+
// What this Animator was built to look like, for deciding whether it can be reused.
|
|
40766
|
+
GetAppearanceSignature() {
|
|
40767
|
+
return this.appearance;
|
|
40768
|
+
}
|
|
40332
40769
|
/**
|
|
40333
40770
|
* The archive URL this instance was constructed with,
|
|
40334
40771
|
* lets a caller re-rendering the same entity tell whether an existing instance is already correct, without reaching into private state.
|
|
@@ -40343,6 +40780,15 @@
|
|
|
40343
40780
|
GetImageProperty() {
|
|
40344
40781
|
return this.imageProperty;
|
|
40345
40782
|
}
|
|
40783
|
+
/**
|
|
40784
|
+
* The presented frame's TINTED pixels, where alpha is what the ramp actually painted.
|
|
40785
|
+
*
|
|
40786
|
+
* Distinct from the value canvas, whose alpha only says a texel has data. A cell can hold a
|
|
40787
|
+
* reading and still be painted nothing, which is exactly what a hidden floor band does.
|
|
40788
|
+
*/
|
|
40789
|
+
GetDisplayedPixels() {
|
|
40790
|
+
return this.displayedPixels;
|
|
40791
|
+
}
|
|
40346
40792
|
/**
|
|
40347
40793
|
* The untinted canvas carrying the presented frame's value in RGB and its coverage in alpha,
|
|
40348
40794
|
* or null unless the instance was constructed with produceValueCanvas.
|
|
@@ -40455,10 +40901,20 @@
|
|
|
40455
40901
|
});
|
|
40456
40902
|
}
|
|
40457
40903
|
async fetchAndTint(idx) {
|
|
40458
|
-
const
|
|
40459
|
-
|
|
40460
|
-
|
|
40461
|
-
|
|
40904
|
+
const tiles = this.tiles;
|
|
40905
|
+
// Every tile's frame for one timestep sits contiguously, because the generator writes
|
|
40906
|
+
// frame major. So a tiled frame is still ONE range request, not one per tile.
|
|
40907
|
+
let lo = Number.MAX_SAFE_INTEGER;
|
|
40908
|
+
let hi = 0;
|
|
40909
|
+
for (const tile of tiles) {
|
|
40910
|
+
const at = tile.Frames[idx];
|
|
40911
|
+
if (!at) {
|
|
40912
|
+
continue;
|
|
40913
|
+
}
|
|
40914
|
+
lo = Math.min(lo, at.ByteOffset);
|
|
40915
|
+
hi = Math.max(hi, at.ByteOffset + at.ByteLength);
|
|
40916
|
+
}
|
|
40917
|
+
const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
|
|
40462
40918
|
const buffer = await response.arrayBuffer();
|
|
40463
40919
|
if (this.disposed) {
|
|
40464
40920
|
return;
|
|
@@ -40469,7 +40925,7 @@
|
|
|
40469
40925
|
if (this.disposed) {
|
|
40470
40926
|
return;
|
|
40471
40927
|
}
|
|
40472
|
-
const decoded = await this.
|
|
40928
|
+
const decoded = await this.composeTiles(tiles, idx, buffer, lo);
|
|
40473
40929
|
if (this.disposed) {
|
|
40474
40930
|
return;
|
|
40475
40931
|
}
|
|
@@ -40480,39 +40936,292 @@
|
|
|
40480
40936
|
this.beginCrossfadeTo(idx);
|
|
40481
40937
|
}
|
|
40482
40938
|
}
|
|
40483
|
-
|
|
40484
|
-
*
|
|
40939
|
+
/*
|
|
40940
|
+
* Draws every tile of one frame into a single raster covering the drape extent.
|
|
40485
40941
|
*/
|
|
40486
|
-
async
|
|
40487
|
-
const
|
|
40488
|
-
const
|
|
40489
|
-
let image;
|
|
40490
|
-
try {
|
|
40491
|
-
image = await loadImage(objectUrl);
|
|
40492
|
-
}
|
|
40493
|
-
finally {
|
|
40494
|
-
URL.revokeObjectURL(objectUrl);
|
|
40495
|
-
}
|
|
40942
|
+
async composeTiles(tiles, idx, buffer, spanStart) {
|
|
40943
|
+
const target = this.compositeExtent();
|
|
40944
|
+
const size = this.compositeSize(tiles, target);
|
|
40496
40945
|
const canvas = document.createElement("canvas");
|
|
40497
|
-
canvas.width =
|
|
40498
|
-
canvas.height =
|
|
40946
|
+
canvas.width = size.width;
|
|
40947
|
+
canvas.height = size.height;
|
|
40499
40948
|
const ctx = canvas.getContext("2d");
|
|
40500
|
-
ctx.
|
|
40501
|
-
|
|
40949
|
+
ctx.imageSmoothingEnabled = false;
|
|
40950
|
+
ctx.clearRect(0, 0, size.width, size.height);
|
|
40951
|
+
const images = await this.decodeSlices(buffer, spanStart, tiles.map((tile) => tile.Frames[idx]));
|
|
40952
|
+
for (let i = 0; i < tiles.length; i++) {
|
|
40953
|
+
const image = images[i];
|
|
40954
|
+
if (!image || this.disposed) {
|
|
40955
|
+
continue;
|
|
40956
|
+
}
|
|
40957
|
+
const at = this.tileRect(tiles[i], target, size);
|
|
40958
|
+
ctx.drawImage(image, at.x, at.y, at.w, at.h);
|
|
40959
|
+
}
|
|
40960
|
+
return this.tint(ctx.getImageData(0, 0, size.width, size.height));
|
|
40961
|
+
}
|
|
40962
|
+
/*
|
|
40963
|
+
* Where a composited frame is draped, which is the polygon's rectangle when there is one.
|
|
40964
|
+
*/
|
|
40965
|
+
compositeExtent() {
|
|
40966
|
+
if (this.drapeExtent) {
|
|
40967
|
+
return this.drapeExtent;
|
|
40968
|
+
}
|
|
40969
|
+
const m = this.metadata;
|
|
40970
|
+
return {
|
|
40971
|
+
West: m && m.West != null ? m.West : -180,
|
|
40972
|
+
East: m && m.East != null ? m.East : 180,
|
|
40973
|
+
South: m && m.South != null ? m.South : -90,
|
|
40974
|
+
North: m && m.North != null ? m.North : 90
|
|
40975
|
+
};
|
|
40976
|
+
}
|
|
40977
|
+
/*
|
|
40978
|
+
* Composite resolution, capped by what can be uploaded as a texture every frame.
|
|
40979
|
+
*/
|
|
40980
|
+
compositeSize(tiles, target) {
|
|
40981
|
+
let finest = Number.MAX_VALUE;
|
|
40982
|
+
for (const tile of tiles) {
|
|
40983
|
+
if (tile.TexelMetres > 0) {
|
|
40984
|
+
finest = Math.min(finest, tile.TexelMetres);
|
|
40985
|
+
}
|
|
40986
|
+
}
|
|
40987
|
+
if (!(finest > 0) || finest === Number.MAX_VALUE) {
|
|
40988
|
+
finest = 1;
|
|
40989
|
+
}
|
|
40990
|
+
const mid = (target.South + target.North) / 2 * Math.PI / 180;
|
|
40991
|
+
const widthM = Math.max((target.East - target.West) * 111320 * Math.cos(mid), 1);
|
|
40992
|
+
const heightM = Math.max((target.North - target.South) * 110574, 1);
|
|
40993
|
+
let width = Math.ceil(widthM / finest);
|
|
40994
|
+
let height = Math.ceil(heightM / finest);
|
|
40995
|
+
const longest = Math.max(width, height);
|
|
40996
|
+
if (longest > MAX_COMPOSITE_TEXELS) {
|
|
40997
|
+
const shrink = MAX_COMPOSITE_TEXELS / longest;
|
|
40998
|
+
width = Math.max(1, Math.round(width * shrink));
|
|
40999
|
+
height = Math.max(1, Math.round(height * shrink));
|
|
41000
|
+
}
|
|
41001
|
+
return { width: Math.max(1, width), height: Math.max(1, height) };
|
|
41002
|
+
}
|
|
41003
|
+
/*
|
|
41004
|
+
* Applies the ramp, borders and baseline mask to raw value pixels, whatever produced them.
|
|
41005
|
+
*/
|
|
41006
|
+
tint(imageData) {
|
|
41007
|
+
const canvas = { width: imageData.width, height: imageData.height };
|
|
40502
41008
|
// Copied before tinting: the ramp is a lerp between two colours, so the value cannot be
|
|
40503
41009
|
// recovered from the tinted pixels afterwards.
|
|
40504
41010
|
const valuePixels = this.produceValueCanvas ? new Uint8ClampedArray(imageData.data) : undefined;
|
|
40505
|
-
|
|
41011
|
+
// The ramp indexes on R as an 8 bit grey, so a packed frame is flattened to one here
|
|
41012
|
+
// rather than teaching every ramp function a second format.
|
|
41013
|
+
// The copy above keeps the packed pair, which is what displacement and labels read.
|
|
41014
|
+
flattenPacked(imageData.data, IsPackedValue(this.metadata));
|
|
41015
|
+
// Stops win when supplied: they can express a band and a hidden floor, which a two
|
|
41016
|
+
// colour ramp cannot. The pair stays the fallback so an older style still draws.
|
|
41017
|
+
if (this.rampStops && this.rampStops.length > 0) {
|
|
41018
|
+
ApplyGradientStops(imageData, this.rampStops);
|
|
41019
|
+
}
|
|
41020
|
+
else {
|
|
41021
|
+
ApplyGrayscaleColorMask(imageData, this.lowColor, this.highColor);
|
|
41022
|
+
}
|
|
41023
|
+
if (this.cellBorder) {
|
|
41024
|
+
ApplyCellBorders(imageData, this.cellBorder.cellTexels, this.cellBorder.widthTexels, this.cellBorder.color);
|
|
41025
|
+
}
|
|
40506
41026
|
this.applyBaselineMask(imageData.data, valuePixels, canvas.width, canvas.height);
|
|
40507
41027
|
if (valuePixels) {
|
|
40508
|
-
dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES);
|
|
41028
|
+
dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES, IsPackedValue(this.metadata));
|
|
40509
41029
|
}
|
|
40510
41030
|
return { pixels: imageData.data, width: canvas.width, height: canvas.height, valuePixels };
|
|
40511
41031
|
}
|
|
41032
|
+
/*
|
|
41033
|
+
* Paints one frame's composited pixels onto a canvas.
|
|
41034
|
+
*/
|
|
41035
|
+
writeInto(canvas, pixels, dims) {
|
|
41036
|
+
canvas.width = dims.width;
|
|
41037
|
+
canvas.height = dims.height;
|
|
41038
|
+
const ctx = canvas.getContext("2d");
|
|
41039
|
+
const image = ctx.createImageData(dims.width, dims.height);
|
|
41040
|
+
image.data.set(pixels);
|
|
41041
|
+
ctx.putImageData(image, 0, 0);
|
|
41042
|
+
}
|
|
41043
|
+
/**
|
|
41044
|
+
* The archive's per-cell floor and ceiling rasters, once EnsureExtremes has resolved.
|
|
41045
|
+
*/
|
|
41046
|
+
GetExtremes() {
|
|
41047
|
+
return { floor: this.floorPixels, ceiling: this.ceilingPixels };
|
|
41048
|
+
}
|
|
41049
|
+
/**
|
|
41050
|
+
* Fetches and decodes the floor and ceiling rasters, at most once.
|
|
41051
|
+
*/
|
|
41052
|
+
EnsureExtremes() {
|
|
41053
|
+
if (!this.extremesLoad) {
|
|
41054
|
+
this.extremesLoad = this.loadTiledExtremes();
|
|
41055
|
+
}
|
|
41056
|
+
return this.extremesLoad;
|
|
41057
|
+
}
|
|
41058
|
+
/*
|
|
41059
|
+
* Composites each tile's baseline mask into one covering the composited frames.
|
|
41060
|
+
*/
|
|
41061
|
+
async loadTiledBaselineMask() {
|
|
41062
|
+
const tiles = this.tiles;
|
|
41063
|
+
const entries = tiles.map((tile) => tile.BaselineMask);
|
|
41064
|
+
let lo = Number.MAX_SAFE_INTEGER;
|
|
41065
|
+
let hi = 0;
|
|
41066
|
+
for (const at of entries) {
|
|
41067
|
+
if (!at) {
|
|
41068
|
+
continue;
|
|
41069
|
+
}
|
|
41070
|
+
lo = Math.min(lo, at.ByteOffset);
|
|
41071
|
+
hi = Math.max(hi, at.ByteOffset + at.ByteLength);
|
|
41072
|
+
}
|
|
41073
|
+
if (!(hi > lo)) {
|
|
41074
|
+
return;
|
|
41075
|
+
}
|
|
41076
|
+
try {
|
|
41077
|
+
const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
|
|
41078
|
+
const block = await response.arrayBuffer();
|
|
41079
|
+
if (this.disposed) {
|
|
41080
|
+
return;
|
|
41081
|
+
}
|
|
41082
|
+
const images = await this.decodeSlices(block, lo, entries);
|
|
41083
|
+
if (this.disposed) {
|
|
41084
|
+
return;
|
|
41085
|
+
}
|
|
41086
|
+
const target = this.compositeExtent();
|
|
41087
|
+
const size = this.compositeSize(tiles, target);
|
|
41088
|
+
const canvas = document.createElement("canvas");
|
|
41089
|
+
canvas.width = size.width;
|
|
41090
|
+
canvas.height = size.height;
|
|
41091
|
+
const ctx = canvas.getContext("2d");
|
|
41092
|
+
ctx.imageSmoothingEnabled = false;
|
|
41093
|
+
ctx.clearRect(0, 0, size.width, size.height);
|
|
41094
|
+
for (let i = 0; i < tiles.length; i++) {
|
|
41095
|
+
if (!images[i]) {
|
|
41096
|
+
continue;
|
|
41097
|
+
}
|
|
41098
|
+
const at = this.tileRect(tiles[i], target, size);
|
|
41099
|
+
ctx.drawImage(images[i], at.x, at.y, at.w, at.h);
|
|
41100
|
+
}
|
|
41101
|
+
const pixels = ctx.getImageData(0, 0, size.width, size.height).data;
|
|
41102
|
+
const flags = new Uint8Array(size.width * size.height);
|
|
41103
|
+
for (let p = 0; p < flags.length; p++) {
|
|
41104
|
+
flags[p] = pixels[p * 4 + 3] >= 128 ? 1 : 0;
|
|
41105
|
+
}
|
|
41106
|
+
this.baselineFlags = flags;
|
|
41107
|
+
this.baselineDims = { width: size.width, height: size.height };
|
|
41108
|
+
}
|
|
41109
|
+
catch (e) {
|
|
41110
|
+
console.warn("TextureFrameSeriesAnimator: could not load the tiled baseline mask.", e);
|
|
41111
|
+
}
|
|
41112
|
+
}
|
|
41113
|
+
/*
|
|
41114
|
+
* Composites each tile's floor and ceiling into rasters matching the composited frames.
|
|
41115
|
+
*
|
|
41116
|
+
* A label indexes them by the same texel as the value canvas, so they have to be laid out the
|
|
41117
|
+
* same way rather than fetched as whole-extent rasters.
|
|
41118
|
+
*/
|
|
41119
|
+
async loadTiledExtremes() {
|
|
41120
|
+
const tiles = this.tiles;
|
|
41121
|
+
const target = this.compositeExtent();
|
|
41122
|
+
const size = this.compositeSize(tiles, target);
|
|
41123
|
+
// One request for the whole extremes block. They sit contiguously after the frames, so
|
|
41124
|
+
// fetching per tile would be 164 round trips for an 82 tile pyramid.
|
|
41125
|
+
let lo = Number.MAX_SAFE_INTEGER;
|
|
41126
|
+
let hi = 0;
|
|
41127
|
+
for (const tile of tiles) {
|
|
41128
|
+
for (const at of [tile.Floor, tile.Ceiling]) {
|
|
41129
|
+
if (!at) {
|
|
41130
|
+
continue;
|
|
41131
|
+
}
|
|
41132
|
+
lo = Math.min(lo, at.ByteOffset);
|
|
41133
|
+
hi = Math.max(hi, at.ByteOffset + at.ByteLength);
|
|
41134
|
+
}
|
|
41135
|
+
}
|
|
41136
|
+
if (!(hi > lo)) {
|
|
41137
|
+
return;
|
|
41138
|
+
}
|
|
41139
|
+
const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
|
|
41140
|
+
const block = await response.arrayBuffer();
|
|
41141
|
+
if (this.disposed) {
|
|
41142
|
+
return;
|
|
41143
|
+
}
|
|
41144
|
+
const draw = async (pick) => {
|
|
41145
|
+
const images = await this.decodeSlices(block, lo, tiles.map(pick));
|
|
41146
|
+
if (this.disposed) {
|
|
41147
|
+
return null;
|
|
41148
|
+
}
|
|
41149
|
+
const canvas = document.createElement("canvas");
|
|
41150
|
+
canvas.width = size.width;
|
|
41151
|
+
canvas.height = size.height;
|
|
41152
|
+
const ctx = canvas.getContext("2d");
|
|
41153
|
+
ctx.imageSmoothingEnabled = false;
|
|
41154
|
+
ctx.clearRect(0, 0, size.width, size.height);
|
|
41155
|
+
let drew = false;
|
|
41156
|
+
for (let i = 0; i < tiles.length; i++) {
|
|
41157
|
+
const image = images[i];
|
|
41158
|
+
if (!image) {
|
|
41159
|
+
continue;
|
|
41160
|
+
}
|
|
41161
|
+
const at = this.tileRect(tiles[i], target, size);
|
|
41162
|
+
ctx.drawImage(image, at.x, at.y, at.w, at.h);
|
|
41163
|
+
drew = true;
|
|
41164
|
+
}
|
|
41165
|
+
return drew ? ctx.getImageData(0, 0, size.width, size.height) : null;
|
|
41166
|
+
};
|
|
41167
|
+
try {
|
|
41168
|
+
this.floorPixels = await draw((tile) => tile.Floor);
|
|
41169
|
+
this.ceilingPixels = await draw((tile) => tile.Ceiling);
|
|
41170
|
+
}
|
|
41171
|
+
catch (e) {
|
|
41172
|
+
console.warn("TextureFrameSeriesAnimator: could not load the tiled extremes.", e);
|
|
41173
|
+
}
|
|
41174
|
+
}
|
|
41175
|
+
/*
|
|
41176
|
+
* Where one tile lands on the composite, snapped to whole pixels.
|
|
41177
|
+
*
|
|
41178
|
+
* Rounding each edge rather than the origin and size is what makes neighbours meet exactly:
|
|
41179
|
+
* a tile's right edge and the next tile's left edge round to the same pixel, so no seam of
|
|
41180
|
+
* background shows between them.
|
|
41181
|
+
*/
|
|
41182
|
+
tileRect(tile, target, size) {
|
|
41183
|
+
const spanLon = target.East - target.West;
|
|
41184
|
+
const spanLat = target.North - target.South;
|
|
41185
|
+
const x0 = Math.round((tile.West - target.West) / spanLon * size.width);
|
|
41186
|
+
const x1 = Math.round((tile.East - target.West) / spanLon * size.width);
|
|
41187
|
+
const y0 = Math.round((target.North - tile.North) / spanLat * size.height);
|
|
41188
|
+
const y1 = Math.round((target.North - tile.South) / spanLat * size.height);
|
|
41189
|
+
return { x: x0, y: y0, w: Math.max(1, x1 - x0), h: Math.max(1, y1 - y0) };
|
|
41190
|
+
}
|
|
41191
|
+
/*
|
|
41192
|
+
* Decodes many tile images at once.
|
|
41193
|
+
*
|
|
41194
|
+
* Serially awaiting each decode makes a frame cost as many round trips through the image
|
|
41195
|
+
* decoder as there are tiles, which for an 82 tile pyramid is seconds rather than one.
|
|
41196
|
+
*/
|
|
41197
|
+
decodeSlices(block, base, entries) {
|
|
41198
|
+
return Promise.all(entries.map(async (at) => {
|
|
41199
|
+
if (!at) {
|
|
41200
|
+
return null;
|
|
41201
|
+
}
|
|
41202
|
+
const slice = block.slice(at.ByteOffset - base, at.ByteOffset - base + at.ByteLength);
|
|
41203
|
+
const objectUrl = URL.createObjectURL(new Blob([slice], { type: "image/png" }));
|
|
41204
|
+
try {
|
|
41205
|
+
return await loadImage(objectUrl);
|
|
41206
|
+
}
|
|
41207
|
+
catch {
|
|
41208
|
+
return null;
|
|
41209
|
+
}
|
|
41210
|
+
finally {
|
|
41211
|
+
URL.revokeObjectURL(objectUrl);
|
|
41212
|
+
}
|
|
41213
|
+
}));
|
|
41214
|
+
}
|
|
40512
41215
|
/**
|
|
40513
41216
|
* Fetches and decodes the archive's static baseline mask, at most once.
|
|
40514
41217
|
*/
|
|
40515
41218
|
ensureBaselineMask() {
|
|
41219
|
+
if (this.maskBaseline && this.tiles) {
|
|
41220
|
+
if (!this.baselineLoad) {
|
|
41221
|
+
this.baselineLoad = this.loadTiledBaselineMask();
|
|
41222
|
+
}
|
|
41223
|
+
return this.baselineLoad;
|
|
41224
|
+
}
|
|
40516
41225
|
if (!this.maskBaseline || !this.baselineMaskEntry) {
|
|
40517
41226
|
return Promise.resolve();
|
|
40518
41227
|
}
|
|
@@ -40625,24 +41334,17 @@
|
|
|
40625
41334
|
presentPixels(pixels, dims, valuePixels) {
|
|
40626
41335
|
this.poolIdx = 1 - this.poolIdx;
|
|
40627
41336
|
const target = this.pool[this.poolIdx];
|
|
40628
|
-
target
|
|
40629
|
-
target.height = dims.height;
|
|
40630
|
-
const ctx = target.getContext("2d");
|
|
40631
|
-
const imgData = ctx.createImageData(dims.width, dims.height);
|
|
40632
|
-
imgData.data.set(pixels);
|
|
40633
|
-
ctx.putImageData(imgData, 0, 0);
|
|
41337
|
+
this.writeInto(target, pixels, dims);
|
|
40634
41338
|
this.displayedPixels = pixels;
|
|
40635
41339
|
if (this.valueCanvas && valuePixels) {
|
|
40636
|
-
this.valueCanvas
|
|
40637
|
-
this.valueCanvas.height = dims.height;
|
|
40638
|
-
const valueCtx = this.valueCanvas.getContext("2d");
|
|
40639
|
-
const valueImgData = valueCtx.createImageData(dims.width, dims.height);
|
|
40640
|
-
valueImgData.data.set(valuePixels);
|
|
40641
|
-
valueCtx.putImageData(valueImgData, 0, 0);
|
|
41340
|
+
this.writeInto(this.valueCanvas, valuePixels, dims);
|
|
40642
41341
|
this.displayedValuePixels = valuePixels;
|
|
40643
41342
|
this.valueDims = dims;
|
|
40644
41343
|
}
|
|
40645
41344
|
this.presentVersion++;
|
|
41345
|
+
if (this.viewer && this.viewer.scene && this.viewer.scene.requestRenderMode) {
|
|
41346
|
+
this.viewer.scene.requestRender();
|
|
41347
|
+
}
|
|
40646
41348
|
}
|
|
40647
41349
|
}
|
|
40648
41350
|
TextureFrameSeriesAnimator.Animator = Animator;
|
|
@@ -40652,7 +41354,727 @@
|
|
|
40652
41354
|
function Now() {
|
|
40653
41355
|
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
40654
41356
|
}
|
|
40655
|
-
})(TextureFrameSeriesAnimator || (TextureFrameSeriesAnimator = {}));
|
|
41357
|
+
})(exports.TextureFrameSeriesAnimator || (exports.TextureFrameSeriesAnimator = {}));
|
|
41358
|
+
|
|
41359
|
+
(function (TextureValueLabels) {
|
|
41360
|
+
// Off by default: a caller can still set a floor, but thinning is the mechanism that keeps a
|
|
41361
|
+
// distant view readable.
|
|
41362
|
+
const DEFAULT_MIN_CELL_PIXELS = 0;
|
|
41363
|
+
const MIN_VISIBLE_PIXELS = 60;
|
|
41364
|
+
const MAX_STRIDE = 512;
|
|
41365
|
+
const STRIDE_HYSTERESIS = 1.4;
|
|
41366
|
+
const DEFAULT_MIN_SPACING_PIXELS = 78;
|
|
41367
|
+
const DEFAULT_MAX_LABELS = 240;
|
|
41368
|
+
const LABEL_RADIUS = 4;
|
|
41369
|
+
const LABEL_FONT = "600 11px ui-monospace, SFMono-Regular, Menlo, monospace";
|
|
41370
|
+
// Clear space either side of a label box, so neighbours read as separate.
|
|
41371
|
+
const LABEL_GAP_PIXELS = 16;
|
|
41372
|
+
const LABEL_ANCHOR_COLOR = "rgba(255,255,255,0.85)";
|
|
41373
|
+
// 8 rather than 5: below this a cell reads as texture rather than as a boundary, and it also
|
|
41374
|
+
// bounds the worst case, since a full screen of 8 px cells fits inside MAX_GRID_CELLS.
|
|
41375
|
+
const DEFAULT_MIN_GRID_CELL_PIXELS = 8;
|
|
41376
|
+
const DEFAULT_GRID_WIDTH_PIXELS = 1;
|
|
41377
|
+
// Enough for a full screen at the minimum legible cell size, with headroom for an oblique view
|
|
41378
|
+
// whose texel window is larger than the screen. The budget is shared across tiles, so sizing it
|
|
41379
|
+
// too tightly made the grid depend on which tile happened to be walked first.
|
|
41380
|
+
const MAX_GRID_CELLS = 40000;
|
|
41381
|
+
// Probes per side when hunting a labellable cell inside one lattice block.
|
|
41382
|
+
const MAX_LABEL_PROBES_PER_SIDE = 8;
|
|
41383
|
+
const TILE_OUTLINE_ALPHA = 0.45;
|
|
41384
|
+
// Cells per grid square at the coarsest. Beyond this a tile is outlined instead.
|
|
41385
|
+
const MAX_GRID_STRIDE = 256;
|
|
41386
|
+
const LINE_HEIGHT = 13;
|
|
41387
|
+
/**
|
|
41388
|
+
* A clip outline from a projected ring, or null when the ring cannot be drawn.
|
|
41389
|
+
* @param projected one entry per ring vertex, null where the vertex has no window coordinate
|
|
41390
|
+
*/
|
|
41391
|
+
function ClipOutline(projected) {
|
|
41392
|
+
if (projected.length < 3) {
|
|
41393
|
+
return null;
|
|
41394
|
+
}
|
|
41395
|
+
const out = [];
|
|
41396
|
+
for (const at of projected) {
|
|
41397
|
+
if (!at) {
|
|
41398
|
+
return null;
|
|
41399
|
+
}
|
|
41400
|
+
out.push(at);
|
|
41401
|
+
}
|
|
41402
|
+
return out;
|
|
41403
|
+
}
|
|
41404
|
+
TextureValueLabels.ClipOutline = ClipOutline;
|
|
41405
|
+
/**
|
|
41406
|
+
* One cell's numbers as a label should print them, or null where the texture has no data there.
|
|
41407
|
+
* @param params at is the index of the cell's red channel. sourceMap is derived from the metadata
|
|
41408
|
+
* when omitted, which a caller reading many cells should hoist rather than repeat.
|
|
41409
|
+
*/
|
|
41410
|
+
function ReadingAt(params) {
|
|
41411
|
+
const { metadata, source, floor, ceiling, at } = params;
|
|
41412
|
+
if (source.data[at + 3] < 128) {
|
|
41413
|
+
return null;
|
|
41414
|
+
}
|
|
41415
|
+
const lo = metadata && typeof metadata.ValueMin === "number" ? metadata.ValueMin : 0;
|
|
41416
|
+
const hi = metadata && typeof metadata.ValueMax === "number" ? metadata.ValueMax : 1;
|
|
41417
|
+
const normalised = exports.TextureFrameSeriesAnimator.NormalisedAt(metadata, source.data, at);
|
|
41418
|
+
const toSource = params.sourceMap !== undefined
|
|
41419
|
+
? params.sourceMap
|
|
41420
|
+
: exports.TextureFrameSeriesAnimator.SourceMap(metadata);
|
|
41421
|
+
const inSource = (value) => exports.TextureFrameSeriesAnimator.ToSource(toSource, value);
|
|
41422
|
+
const reading = { value: inSource(lo + normalised * (hi - lo)) };
|
|
41423
|
+
const eLo = metadata && metadata.ExtremesValueMin;
|
|
41424
|
+
const eHi = metadata && metadata.ExtremesValueMax;
|
|
41425
|
+
if (typeof eLo === "number" && typeof eHi === "number") {
|
|
41426
|
+
if (floor) {
|
|
41427
|
+
reading.floor = inSource(eLo + decode16(floor.data, at) * (eHi - eLo));
|
|
41428
|
+
}
|
|
41429
|
+
if (ceiling) {
|
|
41430
|
+
reading.ceiling = inSource(eLo + decode16(ceiling.data, at) * (eHi - eLo));
|
|
41431
|
+
}
|
|
41432
|
+
}
|
|
41433
|
+
return reading;
|
|
41434
|
+
}
|
|
41435
|
+
TextureValueLabels.ReadingAt = ReadingAt;
|
|
41436
|
+
/**
|
|
41437
|
+
* The unit a label's value line should name, or undefined when the archive states none.
|
|
41438
|
+
* @param metadata the archive's Data.generation
|
|
41439
|
+
*/
|
|
41440
|
+
function LabelUnits(metadata) {
|
|
41441
|
+
if (!metadata) {
|
|
41442
|
+
return undefined;
|
|
41443
|
+
}
|
|
41444
|
+
const recovered = exports.TextureFrameSeriesAnimator.SourceMap(metadata);
|
|
41445
|
+
return (recovered && metadata.SourceUnits) || metadata.Units;
|
|
41446
|
+
}
|
|
41447
|
+
TextureValueLabels.LabelUnits = LabelUnits;
|
|
41448
|
+
/**
|
|
41449
|
+
* How many cells apart labels should sit, as a power of two.
|
|
41450
|
+
* @param options cellPixels is one cell's size on screen, held is the stride already in use
|
|
41451
|
+
*/
|
|
41452
|
+
function ChooseLabelStride(options) {
|
|
41453
|
+
const { cellPixels, visibleCols, visibleRows, maxLabels, minSpacingPixels } = options;
|
|
41454
|
+
const fits = (s, slack) => cellPixels * s >= minSpacingPixels / slack
|
|
41455
|
+
&& Math.ceil(visibleCols / s) * Math.ceil(visibleRows / s) <= maxLabels * slack;
|
|
41456
|
+
let stride = 1;
|
|
41457
|
+
while (stride < MAX_STRIDE && !fits(stride, 1)) {
|
|
41458
|
+
stride *= 2;
|
|
41459
|
+
}
|
|
41460
|
+
const held = options.held;
|
|
41461
|
+
if (held && fits(held, STRIDE_HYSTERESIS)) {
|
|
41462
|
+
if (held === stride / 2 && cellPixels * held >= minSpacingPixels / STRIDE_HYSTERESIS) {
|
|
41463
|
+
return held;
|
|
41464
|
+
}
|
|
41465
|
+
if (held === stride * 2 && cellPixels * stride < minSpacingPixels * STRIDE_HYSTERESIS) {
|
|
41466
|
+
return held;
|
|
41467
|
+
}
|
|
41468
|
+
}
|
|
41469
|
+
return stride;
|
|
41470
|
+
}
|
|
41471
|
+
TextureValueLabels.ChooseLabelStride = ChooseLabelStride;
|
|
41472
|
+
class Labels {
|
|
41473
|
+
constructor(options) {
|
|
41474
|
+
this.sourceMap = null;
|
|
41475
|
+
this.source = null;
|
|
41476
|
+
this.painted = null;
|
|
41477
|
+
this.floor = null;
|
|
41478
|
+
this.ceiling = null;
|
|
41479
|
+
this.disposed = false;
|
|
41480
|
+
this.drawn = 0;
|
|
41481
|
+
this.stride = 0;
|
|
41482
|
+
this.viewer = options.viewer;
|
|
41483
|
+
this.extent = options.extent;
|
|
41484
|
+
this.settings = options.settings || {};
|
|
41485
|
+
this.metadata = options.metadata;
|
|
41486
|
+
// Once per overlay rather than once per cell: a label pass probes thousands of texels
|
|
41487
|
+
// looking for one worth labelling, and the map cannot change while the metadata does not.
|
|
41488
|
+
this.sourceMap = exports.TextureFrameSeriesAnimator.SourceMap(options.metadata);
|
|
41489
|
+
this.tiles = options.tiles;
|
|
41490
|
+
this.clipRing = options.clipRing;
|
|
41491
|
+
this.canvas = document.createElement("canvas");
|
|
41492
|
+
this.canvas.style.position = "absolute";
|
|
41493
|
+
this.canvas.style.inset = "0";
|
|
41494
|
+
this.canvas.style.pointerEvents = "none";
|
|
41495
|
+
const parent = this.viewer.canvas.parentElement;
|
|
41496
|
+
if (parent) {
|
|
41497
|
+
parent.appendChild(this.canvas);
|
|
41498
|
+
}
|
|
41499
|
+
this.ctx = this.canvas.getContext("2d");
|
|
41500
|
+
}
|
|
41501
|
+
SetExtent(extent) {
|
|
41502
|
+
this.extent = extent;
|
|
41503
|
+
}
|
|
41504
|
+
/**
|
|
41505
|
+
* The current frame's decoded values, as the animator's value canvas already provides them.
|
|
41506
|
+
* @param source value pixels, one texel per cell
|
|
41507
|
+
*/
|
|
41508
|
+
SetSource(source) {
|
|
41509
|
+
this.source = source;
|
|
41510
|
+
}
|
|
41511
|
+
/**
|
|
41512
|
+
* The tinted pixels of the same frame, so the grid can skip cells the ramp painted nothing in.
|
|
41513
|
+
* @param painted RGBA of the presented frame, or null to fall back to coverage alone
|
|
41514
|
+
*/
|
|
41515
|
+
SetPainted(painted) {
|
|
41516
|
+
this.painted = painted;
|
|
41517
|
+
}
|
|
41518
|
+
/**
|
|
41519
|
+
* The archive's per-cell extremes, when it publishes them. Without these a label can still
|
|
41520
|
+
* print the current reading, just not the range behind it.
|
|
41521
|
+
*/
|
|
41522
|
+
SetExtremes(floor, ceiling) {
|
|
41523
|
+
this.floor = floor;
|
|
41524
|
+
this.ceiling = ceiling;
|
|
41525
|
+
}
|
|
41526
|
+
GetDrawnCount() {
|
|
41527
|
+
return this.drawn;
|
|
41528
|
+
}
|
|
41529
|
+
Clear() {
|
|
41530
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
41531
|
+
this.drawn = 0;
|
|
41532
|
+
}
|
|
41533
|
+
Dispose() {
|
|
41534
|
+
this.disposed = true;
|
|
41535
|
+
if (this.canvas.parentElement) {
|
|
41536
|
+
this.canvas.parentElement.removeChild(this.canvas);
|
|
41537
|
+
}
|
|
41538
|
+
}
|
|
41539
|
+
/**
|
|
41540
|
+
* Redraws every visible label. Cheap enough to call per rendered frame, because the cell
|
|
41541
|
+
* count it considers is bounded by the view rather than by the texture.
|
|
41542
|
+
*/
|
|
41543
|
+
Render() {
|
|
41544
|
+
var _a, _b, _c;
|
|
41545
|
+
if (this.disposed || !this.source) {
|
|
41546
|
+
return;
|
|
41547
|
+
}
|
|
41548
|
+
const view = this.viewer.canvas;
|
|
41549
|
+
if (this.canvas.width !== view.clientWidth || this.canvas.height !== view.clientHeight) {
|
|
41550
|
+
this.canvas.width = view.clientWidth;
|
|
41551
|
+
this.canvas.height = view.clientHeight;
|
|
41552
|
+
}
|
|
41553
|
+
this.Clear();
|
|
41554
|
+
const cols = this.source.width;
|
|
41555
|
+
const rows = this.source.height;
|
|
41556
|
+
const window = this.visibleTexels(cols, rows);
|
|
41557
|
+
if (!window) {
|
|
41558
|
+
return;
|
|
41559
|
+
}
|
|
41560
|
+
const midCol = (window.col0 + window.col1) / 2;
|
|
41561
|
+
const midRow = (window.row0 + window.row1) / 2;
|
|
41562
|
+
const a = this.project(this.lonAt(midCol, cols), this.latAt(midRow, rows));
|
|
41563
|
+
const b = this.project(this.lonAt(midCol + 1, cols), this.latAt(midRow + 1, rows));
|
|
41564
|
+
if (!a || !b) {
|
|
41565
|
+
return;
|
|
41566
|
+
}
|
|
41567
|
+
const cellPixels = Math.min(Math.abs(b.x - a.x), Math.abs(b.y - a.y));
|
|
41568
|
+
// Ahead of the label threshold on purpose: a grid still reads at a cell size where a
|
|
41569
|
+
// number no longer fits.
|
|
41570
|
+
this.drawGrid(window, cols, rows, cellPixels);
|
|
41571
|
+
if (cellPixels < ((_a = this.settings.minCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_CELL_PIXELS)) {
|
|
41572
|
+
return;
|
|
41573
|
+
}
|
|
41574
|
+
const visibleWidth = cellPixels * (window.col1 - window.col0 + 1);
|
|
41575
|
+
const visibleHeight = cellPixels * (window.row1 - window.row0 + 1);
|
|
41576
|
+
if (Math.max(visibleWidth, visibleHeight) < MIN_VISIBLE_PIXELS) {
|
|
41577
|
+
return;
|
|
41578
|
+
}
|
|
41579
|
+
const parts = this.settings.parts || ["value", "floor", "ceiling"];
|
|
41580
|
+
this.ctx.font = LABEL_FONT;
|
|
41581
|
+
this.ctx.textAlign = "center";
|
|
41582
|
+
const maxLabels = (_b = this.settings.maxLabels) !== null && _b !== void 0 ? _b : DEFAULT_MAX_LABELS;
|
|
41583
|
+
this.stride = ChooseLabelStride({
|
|
41584
|
+
cellPixels,
|
|
41585
|
+
visibleCols: window.col1 - window.col0 + 1,
|
|
41586
|
+
visibleRows: window.row1 - window.row0 + 1,
|
|
41587
|
+
maxLabels,
|
|
41588
|
+
// Measured, not assumed: the spacing that keeps labels apart is whatever the widest
|
|
41589
|
+
// one is, so changing what a label says cannot quietly start overlapping them.
|
|
41590
|
+
minSpacingPixels: Math.max((_c = this.settings.minSpacingPixels) !== null && _c !== void 0 ? _c : DEFAULT_MIN_SPACING_PIXELS, this.widestLabel(parts)),
|
|
41591
|
+
held: this.stride
|
|
41592
|
+
});
|
|
41593
|
+
const stride = this.stride;
|
|
41594
|
+
const firstRow = Math.floor(window.row0 / stride) * stride;
|
|
41595
|
+
const firstCol = Math.floor(window.col0 / stride) * stride;
|
|
41596
|
+
for (let row = firstRow; row <= window.row1 && this.drawn < maxLabels; row += stride) {
|
|
41597
|
+
for (let col = firstCol; col <= window.col1 && this.drawn < maxLabels; col += stride) {
|
|
41598
|
+
const found = this.readNear(col, row, cols, rows, stride);
|
|
41599
|
+
if (!found) {
|
|
41600
|
+
continue;
|
|
41601
|
+
}
|
|
41602
|
+
const point = this.project(this.lonAt(found.col, cols), this.latAt(found.row, rows));
|
|
41603
|
+
if (!point || point.x < 0 || point.y < 0
|
|
41604
|
+
|| point.x > this.canvas.width || point.y > this.canvas.height) {
|
|
41605
|
+
continue;
|
|
41606
|
+
}
|
|
41607
|
+
this.drawLabel(point, found.reading, parts, this.cellOutline(found.col, found.row, cols, rows));
|
|
41608
|
+
}
|
|
41609
|
+
}
|
|
41610
|
+
}
|
|
41611
|
+
/*
|
|
41612
|
+
* Outlines each covered cell, from a projected lattice of its corners.
|
|
41613
|
+
*/
|
|
41614
|
+
drawGrid(window, cols, rows, cellPixels) {
|
|
41615
|
+
var _a, _b;
|
|
41616
|
+
const color = this.settings.gridColor;
|
|
41617
|
+
if (!color) {
|
|
41618
|
+
return;
|
|
41619
|
+
}
|
|
41620
|
+
if (this.tiles && this.tiles.length) {
|
|
41621
|
+
this.ctx.save();
|
|
41622
|
+
this.applyRingClip();
|
|
41623
|
+
this.drawTileGrid(color);
|
|
41624
|
+
this.ctx.restore();
|
|
41625
|
+
return;
|
|
41626
|
+
}
|
|
41627
|
+
if (cellPixels < ((_a = this.settings.minGridCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_GRID_CELL_PIXELS)) {
|
|
41628
|
+
return;
|
|
41629
|
+
}
|
|
41630
|
+
// Corners are shared by four cells, so projecting the lattice once rather than per cell
|
|
41631
|
+
// cuts the work by about four and keeps neighbouring outlines exactly coincident.
|
|
41632
|
+
const wide = window.col1 - window.col0 + 1;
|
|
41633
|
+
const high = window.row1 - window.row0 + 1;
|
|
41634
|
+
if (wide * high > MAX_GRID_CELLS) {
|
|
41635
|
+
return;
|
|
41636
|
+
}
|
|
41637
|
+
const lattice = new Array((wide + 1) * (high + 1));
|
|
41638
|
+
for (let r = 0; r <= high; r++) {
|
|
41639
|
+
for (let c = 0; c <= wide; c++) {
|
|
41640
|
+
// Corners sit half a texel out from the centres lonAt/latAt return.
|
|
41641
|
+
const lon = this.lonAt(window.col0 + c - 0.5, cols);
|
|
41642
|
+
const lat = this.latAt(window.row0 + r - 0.5, rows);
|
|
41643
|
+
lattice[r * (wide + 1) + c] = this.project(lon, lat);
|
|
41644
|
+
}
|
|
41645
|
+
}
|
|
41646
|
+
const path = new Path2D();
|
|
41647
|
+
let drew = false;
|
|
41648
|
+
for (let r = 0; r < high; r++) {
|
|
41649
|
+
for (let c = 0; c < wide; c++) {
|
|
41650
|
+
if (!this.isCovered(window.col0 + c, window.row0 + r, cols)) {
|
|
41651
|
+
continue;
|
|
41652
|
+
}
|
|
41653
|
+
const tl = lattice[r * (wide + 1) + c];
|
|
41654
|
+
const tr = lattice[r * (wide + 1) + c + 1];
|
|
41655
|
+
const bl = lattice[(r + 1) * (wide + 1) + c];
|
|
41656
|
+
const br = lattice[(r + 1) * (wide + 1) + c + 1];
|
|
41657
|
+
if (!tl || !tr || !bl || !br) {
|
|
41658
|
+
continue;
|
|
41659
|
+
}
|
|
41660
|
+
path.moveTo(tl.x, tl.y);
|
|
41661
|
+
path.lineTo(tr.x, tr.y);
|
|
41662
|
+
path.lineTo(br.x, br.y);
|
|
41663
|
+
path.lineTo(bl.x, bl.y);
|
|
41664
|
+
path.closePath();
|
|
41665
|
+
drew = true;
|
|
41666
|
+
}
|
|
41667
|
+
}
|
|
41668
|
+
if (!drew) {
|
|
41669
|
+
return;
|
|
41670
|
+
}
|
|
41671
|
+
this.ctx.strokeStyle = color;
|
|
41672
|
+
this.ctx.lineWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
|
|
41673
|
+
this.ctx.stroke(path);
|
|
41674
|
+
+" alpha" + this.ctx.globalAlpha;
|
|
41675
|
+
}
|
|
41676
|
+
/*
|
|
41677
|
+
* Restricts drawing to the polygon, so nothing is ruled over ground it does not cover.
|
|
41678
|
+
*/
|
|
41679
|
+
applyRingClip() {
|
|
41680
|
+
const ring = this.clipRing;
|
|
41681
|
+
if (!ring || ring.length < 3) {
|
|
41682
|
+
return;
|
|
41683
|
+
}
|
|
41684
|
+
const outline = ClipOutline(ring.map(point => this.project(point.lon, point.lat)));
|
|
41685
|
+
if (!outline) {
|
|
41686
|
+
return;
|
|
41687
|
+
}
|
|
41688
|
+
const path = new Path2D();
|
|
41689
|
+
path.moveTo(outline[0].x, outline[0].y);
|
|
41690
|
+
for (let i = 1; i < outline.length; i++) {
|
|
41691
|
+
path.lineTo(outline[i].x, outline[i].y);
|
|
41692
|
+
}
|
|
41693
|
+
path.closePath();
|
|
41694
|
+
this.ctx.clip(path);
|
|
41695
|
+
}
|
|
41696
|
+
/*
|
|
41697
|
+
* The cell of whichever tile covers a point, as screen corners.
|
|
41698
|
+
*/
|
|
41699
|
+
tileCellOutline(lon, lat) {
|
|
41700
|
+
for (const tile of this.tiles) {
|
|
41701
|
+
if (lon < tile.West || lon > tile.East || lat < tile.South || lat > tile.North) {
|
|
41702
|
+
continue;
|
|
41703
|
+
}
|
|
41704
|
+
const c = (Math.min(tile.ResolutionX - 1, Math.max(0, Math.floor((lon - tile.West) / (tile.East - tile.West) * tile.ResolutionX))));
|
|
41705
|
+
const r = (Math.min(tile.ResolutionY - 1, Math.max(0, Math.floor((tile.North - lat) / (tile.North - tile.South) * tile.ResolutionY))));
|
|
41706
|
+
const w0 = tile.West + (c / tile.ResolutionX) * (tile.East - tile.West);
|
|
41707
|
+
const w1 = tile.West + ((c + 1) / tile.ResolutionX) * (tile.East - tile.West);
|
|
41708
|
+
const n0 = tile.North - (r / tile.ResolutionY) * (tile.North - tile.South);
|
|
41709
|
+
const n1 = tile.North - ((r + 1) / tile.ResolutionY) * (tile.North - tile.South);
|
|
41710
|
+
const corners = [
|
|
41711
|
+
this.project(w0, n0), this.project(w1, n0),
|
|
41712
|
+
this.project(w1, n1), this.project(w0, n1)
|
|
41713
|
+
];
|
|
41714
|
+
return corners.every((q) => q) ? corners : null;
|
|
41715
|
+
}
|
|
41716
|
+
return null;
|
|
41717
|
+
}
|
|
41718
|
+
/*
|
|
41719
|
+
* Outlines each tile's own cells, so cell size still shows where the mesh is refined.
|
|
41720
|
+
*/
|
|
41721
|
+
drawTileGrid(color) {
|
|
41722
|
+
var _a, _b, _c;
|
|
41723
|
+
const view = this.viewer.camera.computeViewRectangle(Cesium.Ellipsoid.WGS84);
|
|
41724
|
+
if (!view) {
|
|
41725
|
+
return;
|
|
41726
|
+
}
|
|
41727
|
+
const west = Cesium.Math.toDegrees(view.west);
|
|
41728
|
+
const east = Cesium.Math.toDegrees(view.east);
|
|
41729
|
+
const south = Cesium.Math.toDegrees(view.south);
|
|
41730
|
+
const north = Cesium.Math.toDegrees(view.north);
|
|
41731
|
+
const minCell = (_a = this.settings.minGridCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_GRID_CELL_PIXELS;
|
|
41732
|
+
const focus = this.groundAtScreenCentre();
|
|
41733
|
+
const path = new Path2D();
|
|
41734
|
+
// A tile too fine to rule, or one the budget cannot afford, contributes its own outline
|
|
41735
|
+
// instead of vanishing. Dropping such tiles entirely is what made whole regions blink out.
|
|
41736
|
+
const coarse = new Path2D();
|
|
41737
|
+
let drew = false;
|
|
41738
|
+
let drewCoarse = false;
|
|
41739
|
+
const outlineTile = (tile) => {
|
|
41740
|
+
const tl = this.project(tile.West, tile.North);
|
|
41741
|
+
const tr = this.project(tile.East, tile.North);
|
|
41742
|
+
const br = this.project(tile.East, tile.South);
|
|
41743
|
+
const bl = this.project(tile.West, tile.South);
|
|
41744
|
+
if (!tl || !tr || !br || !bl) {
|
|
41745
|
+
return;
|
|
41746
|
+
}
|
|
41747
|
+
coarse.moveTo(tl.x, tl.y);
|
|
41748
|
+
coarse.lineTo(tr.x, tr.y);
|
|
41749
|
+
coarse.lineTo(br.x, br.y);
|
|
41750
|
+
coarse.lineTo(bl.x, bl.y);
|
|
41751
|
+
coarse.closePath();
|
|
41752
|
+
drewCoarse = true;
|
|
41753
|
+
};
|
|
41754
|
+
// Planned for every visible tile BEFORE any is drawn.
|
|
41755
|
+
const planned = [];
|
|
41756
|
+
for (const tile of this.tiles) {
|
|
41757
|
+
if (tile.East < west || tile.West > east || tile.North < south || tile.South > north) {
|
|
41758
|
+
continue;
|
|
41759
|
+
}
|
|
41760
|
+
const c0 = Math.max(0, Math.floor((Math.max(west, tile.West) - tile.West)
|
|
41761
|
+
/ (tile.East - tile.West) * tile.ResolutionX));
|
|
41762
|
+
const c1 = Math.min(tile.ResolutionX, Math.ceil((Math.min(east, tile.East) - tile.West)
|
|
41763
|
+
/ (tile.East - tile.West) * tile.ResolutionX));
|
|
41764
|
+
const r0 = Math.max(0, Math.floor((tile.North - Math.min(north, tile.North))
|
|
41765
|
+
/ (tile.North - tile.South) * tile.ResolutionY));
|
|
41766
|
+
const r1 = Math.min(tile.ResolutionY, Math.ceil((tile.North - Math.max(south, tile.South))
|
|
41767
|
+
/ (tile.North - tile.South) * tile.ResolutionY));
|
|
41768
|
+
if (c1 <= c0 || r1 <= r0) {
|
|
41769
|
+
continue;
|
|
41770
|
+
}
|
|
41771
|
+
const focusC = focus
|
|
41772
|
+
? Math.round((focus.lon - tile.West) / (tile.East - tile.West) * tile.ResolutionX)
|
|
41773
|
+
: Math.floor((c0 + c1) / 2);
|
|
41774
|
+
const focusR = focus
|
|
41775
|
+
? Math.round((tile.North - focus.lat) / (tile.North - tile.South) * tile.ResolutionY)
|
|
41776
|
+
: Math.floor((r0 + r1) / 2);
|
|
41777
|
+
const midC = Math.min(c1 - 1, Math.max(c0, focusC));
|
|
41778
|
+
const midR = Math.min(r1 - 1, Math.max(r0, focusR));
|
|
41779
|
+
const cellLon = (tile.East - tile.West) / tile.ResolutionX;
|
|
41780
|
+
const cellLat = (tile.North - tile.South) / tile.ResolutionY;
|
|
41781
|
+
const at = this.project(tile.West + midC * cellLon, tile.North - midR * cellLat);
|
|
41782
|
+
const next = (this.project(tile.West + (midC + 1) * cellLon, tile.North - (midR + 1) * cellLat));
|
|
41783
|
+
if (!at || !next) {
|
|
41784
|
+
outlineTile(tile);
|
|
41785
|
+
continue;
|
|
41786
|
+
}
|
|
41787
|
+
const onScreen = Math.min(Math.abs(next.x - at.x), Math.abs(next.y - at.y));
|
|
41788
|
+
let step = 1;
|
|
41789
|
+
while (step < MAX_GRID_STRIDE && onScreen * step < minCell) {
|
|
41790
|
+
step *= 2;
|
|
41791
|
+
}
|
|
41792
|
+
if (onScreen * step < minCell) {
|
|
41793
|
+
outlineTile(tile);
|
|
41794
|
+
continue;
|
|
41795
|
+
}
|
|
41796
|
+
planned.push({ tile, c0, c1, r0, r1, step });
|
|
41797
|
+
}
|
|
41798
|
+
// Coarsened together rather than dropped one by one, so a busy view thins out evenly.
|
|
41799
|
+
const blocksOf = (p) => Math.ceil((p.c1 - p.c0) / p.step) * Math.ceil((p.r1 - p.r0) / p.step);
|
|
41800
|
+
let total = planned.reduce((sum, p) => sum + blocksOf(p), 0);
|
|
41801
|
+
while (total > MAX_GRID_CELLS && planned.some((p) => p.step < MAX_GRID_STRIDE)) {
|
|
41802
|
+
for (const p of planned) {
|
|
41803
|
+
p.step = Math.min(MAX_GRID_STRIDE, p.step * 2);
|
|
41804
|
+
}
|
|
41805
|
+
total = planned.reduce((sum, p) => sum + blocksOf(p), 0);
|
|
41806
|
+
}
|
|
41807
|
+
for (const { tile, c0, c1, r0, r1, step } of planned) {
|
|
41808
|
+
// Anchored to the tile's own origin so lines stay on the same boundaries as the camera
|
|
41809
|
+
// moves, and so each step up is a subset of the one below it.
|
|
41810
|
+
const firstR = Math.floor(r0 / step) * step;
|
|
41811
|
+
const firstC = Math.floor(c0 / step) * step;
|
|
41812
|
+
for (let r = firstR; r < r1; r += step) {
|
|
41813
|
+
const n0 = tile.North - (r / tile.ResolutionY) * (tile.North - tile.South);
|
|
41814
|
+
const n1 = tile.North
|
|
41815
|
+
- (Math.min(r + step, tile.ResolutionY) / tile.ResolutionY) * (tile.North - tile.South);
|
|
41816
|
+
for (let c = firstC; c < c1; c += step) {
|
|
41817
|
+
const w0 = tile.West + (c / tile.ResolutionX) * (tile.East - tile.West);
|
|
41818
|
+
const w1 = tile.West
|
|
41819
|
+
+ (Math.min(c + step, tile.ResolutionX) / tile.ResolutionX) * (tile.East - tile.West);
|
|
41820
|
+
if (!this.coveredAt((w0 + w1) / 2, (n0 + n1) / 2)) {
|
|
41821
|
+
continue;
|
|
41822
|
+
}
|
|
41823
|
+
const tl = this.project(w0, n0);
|
|
41824
|
+
const tr = this.project(w1, n0);
|
|
41825
|
+
const br = this.project(w1, n1);
|
|
41826
|
+
const bl = this.project(w0, n1);
|
|
41827
|
+
if (!tl || !tr || !br || !bl) {
|
|
41828
|
+
continue;
|
|
41829
|
+
}
|
|
41830
|
+
path.moveTo(tl.x, tl.y);
|
|
41831
|
+
path.lineTo(tr.x, tr.y);
|
|
41832
|
+
path.lineTo(br.x, br.y);
|
|
41833
|
+
path.lineTo(bl.x, bl.y);
|
|
41834
|
+
path.closePath();
|
|
41835
|
+
drew = true;
|
|
41836
|
+
}
|
|
41837
|
+
}
|
|
41838
|
+
}
|
|
41839
|
+
const gridWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
|
|
41840
|
+
if (drewCoarse) {
|
|
41841
|
+
this.ctx.save();
|
|
41842
|
+
// Dimmer than a cell edge, so a tile boundary reads as "there is finer data here"
|
|
41843
|
+
// rather than as a cell in its own right.
|
|
41844
|
+
this.ctx.globalAlpha = TILE_OUTLINE_ALPHA;
|
|
41845
|
+
this.ctx.strokeStyle = color;
|
|
41846
|
+
this.ctx.lineWidth = gridWidth;
|
|
41847
|
+
this.ctx.stroke(coarse);
|
|
41848
|
+
this.ctx.restore();
|
|
41849
|
+
}
|
|
41850
|
+
if (!drew) {
|
|
41851
|
+
return;
|
|
41852
|
+
}
|
|
41853
|
+
this.ctx.strokeStyle = color;
|
|
41854
|
+
this.ctx.lineWidth = (_c = this.settings.gridWidthPixels) !== null && _c !== void 0 ? _c : DEFAULT_GRID_WIDTH_PIXELS;
|
|
41855
|
+
this.ctx.stroke(path);
|
|
41856
|
+
}
|
|
41857
|
+
/*
|
|
41858
|
+
* Lon/lat of the ground at the centre of the view, or null when the centre misses the globe.
|
|
41859
|
+
*/
|
|
41860
|
+
groundAtScreenCentre() {
|
|
41861
|
+
const scene = this.viewer.scene;
|
|
41862
|
+
const centre = new Cesium.Cartesian2(this.viewer.canvas.clientWidth / 2, this.viewer.canvas.clientHeight / 2);
|
|
41863
|
+
const hit = scene.camera.pickEllipsoid(centre, Cesium.Ellipsoid.WGS84);
|
|
41864
|
+
if (!hit) {
|
|
41865
|
+
return null;
|
|
41866
|
+
}
|
|
41867
|
+
const carto = Cesium.Cartographic.fromCartesian(hit);
|
|
41868
|
+
return {
|
|
41869
|
+
lon: Cesium.Math.toDegrees(carto.longitude),
|
|
41870
|
+
lat: Cesium.Math.toDegrees(carto.latitude)
|
|
41871
|
+
};
|
|
41872
|
+
}
|
|
41873
|
+
/*
|
|
41874
|
+
* Whether the composited raster holds data at a lon/lat.
|
|
41875
|
+
*/
|
|
41876
|
+
coveredAt(lon, lat) {
|
|
41877
|
+
if (!this.source) {
|
|
41878
|
+
return false;
|
|
41879
|
+
}
|
|
41880
|
+
const cols = this.source.width;
|
|
41881
|
+
const rows = this.source.height;
|
|
41882
|
+
const col = Math.floor((lon - this.extent.West) / (this.extent.East - this.extent.West) * cols);
|
|
41883
|
+
const row = Math.floor((this.extent.North - lat) / (this.extent.North - this.extent.South) * rows);
|
|
41884
|
+
if (col < 0 || row < 0 || col >= cols || row >= rows) {
|
|
41885
|
+
return false;
|
|
41886
|
+
}
|
|
41887
|
+
const at = (row * cols + col) * 4 + 3;
|
|
41888
|
+
// Painted, not merely covered: a dry cell holds a reading the ramp deliberately hides, and
|
|
41889
|
+
// ruling a grid over it draws detail where the picture shows none.
|
|
41890
|
+
if (this.painted && this.painted.length === cols * rows * 4) {
|
|
41891
|
+
return this.painted[at] > 0;
|
|
41892
|
+
}
|
|
41893
|
+
return this.source.data[at] >= 128;
|
|
41894
|
+
}
|
|
41895
|
+
isCovered(col, row, cols) {
|
|
41896
|
+
return Boolean(this.source && this.source.data[(row * cols + col) * 4 + 3] >= 128);
|
|
41897
|
+
}
|
|
41898
|
+
/*
|
|
41899
|
+
* Width of the widest label this overlay could draw, in pixels.
|
|
41900
|
+
*/
|
|
41901
|
+
widestLabel(parts) {
|
|
41902
|
+
const sample = [];
|
|
41903
|
+
if (parts.indexOf("value") >= 0) {
|
|
41904
|
+
sample.push(valueLine("-000.00", LabelUnits(this.metadata)));
|
|
41905
|
+
}
|
|
41906
|
+
const range = [];
|
|
41907
|
+
if (parts.indexOf("floor") >= 0) {
|
|
41908
|
+
range.push("min: -000.00");
|
|
41909
|
+
}
|
|
41910
|
+
if (parts.indexOf("ceiling") >= 0) {
|
|
41911
|
+
range.push("max: -000.00");
|
|
41912
|
+
}
|
|
41913
|
+
if (range.length) {
|
|
41914
|
+
sample.push(range.join(" - "));
|
|
41915
|
+
}
|
|
41916
|
+
if (!sample.length) {
|
|
41917
|
+
return 0;
|
|
41918
|
+
}
|
|
41919
|
+
const widest = Math.max(...sample.map((line) => this.ctx.measureText(line).width));
|
|
41920
|
+
return widest + LABEL_GAP_PIXELS;
|
|
41921
|
+
}
|
|
41922
|
+
/*
|
|
41923
|
+
* The four screen corners of one cell, or null if any of them will not project.
|
|
41924
|
+
*/
|
|
41925
|
+
cellOutline(col, row, cols, rows) {
|
|
41926
|
+
// On a pyramid the composited texel is not a cell. Outlining it would draw a second,
|
|
41927
|
+
// uniform lattice over the tile grid, misaligned with it everywhere.
|
|
41928
|
+
if (this.tiles && this.tiles.length) {
|
|
41929
|
+
return this.tileCellOutline(this.lonAt(col, cols), this.latAt(row, rows));
|
|
41930
|
+
}
|
|
41931
|
+
const corners = [
|
|
41932
|
+
this.project(this.lonAt(col - 0.5, cols), this.latAt(row - 0.5, rows)),
|
|
41933
|
+
this.project(this.lonAt(col + 0.5, cols), this.latAt(row - 0.5, rows)),
|
|
41934
|
+
this.project(this.lonAt(col + 0.5, cols), this.latAt(row + 0.5, rows)),
|
|
41935
|
+
this.project(this.lonAt(col - 0.5, cols), this.latAt(row + 0.5, rows))
|
|
41936
|
+
];
|
|
41937
|
+
return corners.every((c) => c) ? corners : null;
|
|
41938
|
+
}
|
|
41939
|
+
drawLabel(point, reading, parts, outline) {
|
|
41940
|
+
var _a;
|
|
41941
|
+
const dp = (_a = this.settings.decimals) !== null && _a !== void 0 ? _a : (Math.abs(reading.value) < 10 ? 2 : 1);
|
|
41942
|
+
const lines = [];
|
|
41943
|
+
if (parts.indexOf("value") >= 0) {
|
|
41944
|
+
lines.push(valueLine(reading.value.toFixed(dp), LabelUnits(this.metadata)));
|
|
41945
|
+
}
|
|
41946
|
+
// Named rather than a bare pair: two numbers under a third say nothing about which is
|
|
41947
|
+
// the series low and which is its high.
|
|
41948
|
+
const range = [];
|
|
41949
|
+
if (parts.indexOf("floor") >= 0 && reading.floor !== undefined) {
|
|
41950
|
+
range.push(`min: ${reading.floor.toFixed(dp)}`);
|
|
41951
|
+
}
|
|
41952
|
+
if (parts.indexOf("ceiling") >= 0 && reading.ceiling !== undefined) {
|
|
41953
|
+
range.push(`max: ${reading.ceiling.toFixed(dp)}`);
|
|
41954
|
+
}
|
|
41955
|
+
if (range.length) {
|
|
41956
|
+
lines.push(range.join(" - "));
|
|
41957
|
+
}
|
|
41958
|
+
if (!lines.length) {
|
|
41959
|
+
return;
|
|
41960
|
+
}
|
|
41961
|
+
if (outline) {
|
|
41962
|
+
this.ctx.strokeStyle = LABEL_ANCHOR_COLOR;
|
|
41963
|
+
this.ctx.lineWidth = 1.5;
|
|
41964
|
+
this.ctx.beginPath();
|
|
41965
|
+
this.ctx.moveTo(outline[0].x, outline[0].y);
|
|
41966
|
+
for (let i = 1; i < outline.length; i++) {
|
|
41967
|
+
this.ctx.lineTo(outline[i].x, outline[i].y);
|
|
41968
|
+
}
|
|
41969
|
+
this.ctx.closePath();
|
|
41970
|
+
this.ctx.stroke();
|
|
41971
|
+
}
|
|
41972
|
+
const width = Math.max(...lines.map((l) => this.ctx.measureText(l).width)) + 10;
|
|
41973
|
+
const height = lines.length * LINE_HEIGHT + 6;
|
|
41974
|
+
const top = point.y - height / 2;
|
|
41975
|
+
this.ctx.fillStyle = "rgba(16,16,14,0.72)";
|
|
41976
|
+
this.ctx.beginPath();
|
|
41977
|
+
this.ctx.roundRect(point.x - width / 2, top, width, height, LABEL_RADIUS);
|
|
41978
|
+
this.ctx.fill();
|
|
41979
|
+
lines.forEach((line, i) => {
|
|
41980
|
+
this.ctx.fillStyle = i === 0 ? "#ffffff" : "#a8a89f";
|
|
41981
|
+
this.ctx.fillText(line, point.x, top + LINE_HEIGHT * (i + 1) - 1);
|
|
41982
|
+
});
|
|
41983
|
+
this.drawn++;
|
|
41984
|
+
}
|
|
41985
|
+
/*
|
|
41986
|
+
* The first cell with a reading at or near a lattice point, searched within its own block.
|
|
41987
|
+
*/
|
|
41988
|
+
readNear(col, row, cols, rows, stride) {
|
|
41989
|
+
const direct = this.readAt(col, row, cols);
|
|
41990
|
+
if (direct && this.paintedAt(col, row, cols)) {
|
|
41991
|
+
return { col, row, reading: direct };
|
|
41992
|
+
}
|
|
41993
|
+
if (stride <= 1) {
|
|
41994
|
+
return null;
|
|
41995
|
+
}
|
|
41996
|
+
// Bounded, and stepped rather than exhaustive, so a large stride cannot make this the
|
|
41997
|
+
// expensive part of a frame.
|
|
41998
|
+
const step = Math.max(1, Math.floor(stride / MAX_LABEL_PROBES_PER_SIDE));
|
|
41999
|
+
for (let r = row; r < Math.min(rows, row + stride); r += step) {
|
|
42000
|
+
for (let c = col; c < Math.min(cols, col + stride); c += step) {
|
|
42001
|
+
const reading = this.readAt(c, r, cols);
|
|
42002
|
+
if (reading && this.paintedAt(c, r, cols)) {
|
|
42003
|
+
return { col: c, row: r, reading };
|
|
42004
|
+
}
|
|
42005
|
+
}
|
|
42006
|
+
}
|
|
42007
|
+
return null;
|
|
42008
|
+
}
|
|
42009
|
+
paintedAt(col, row, cols) {
|
|
42010
|
+
if (!this.painted) {
|
|
42011
|
+
return true;
|
|
42012
|
+
}
|
|
42013
|
+
const at = (row * cols + col) * 4 + 3;
|
|
42014
|
+
return this.painted[at] > 0;
|
|
42015
|
+
}
|
|
42016
|
+
// Decodes one cell, skipping anything the texture marks as having no data.
|
|
42017
|
+
readAt(col, row, cols) {
|
|
42018
|
+
if (!this.source) {
|
|
42019
|
+
return null;
|
|
42020
|
+
}
|
|
42021
|
+
return ReadingAt({
|
|
42022
|
+
metadata: this.metadata,
|
|
42023
|
+
source: this.source,
|
|
42024
|
+
floor: this.floor,
|
|
42025
|
+
ceiling: this.ceiling,
|
|
42026
|
+
at: (row * cols + col) * 4,
|
|
42027
|
+
sourceMap: this.sourceMap
|
|
42028
|
+
});
|
|
42029
|
+
}
|
|
42030
|
+
lonAt(col, cols) {
|
|
42031
|
+
return this.extent.West + ((col + 0.5) / cols) * (this.extent.East - this.extent.West);
|
|
42032
|
+
}
|
|
42033
|
+
latAt(row, rows) {
|
|
42034
|
+
return this.extent.North - ((row + 0.5) / rows) * (this.extent.North - this.extent.South);
|
|
42035
|
+
}
|
|
42036
|
+
// The texel range overlapping the camera's view, so an off-screen texture costs nothing.
|
|
42037
|
+
visibleTexels(cols, rows) {
|
|
42038
|
+
const view = this.viewer.camera.computeViewRectangle(Cesium.Ellipsoid.WGS84);
|
|
42039
|
+
if (!view) {
|
|
42040
|
+
return null;
|
|
42041
|
+
}
|
|
42042
|
+
const west = Math.max(this.extent.West, Cesium.Math.toDegrees(view.west));
|
|
42043
|
+
const east = Math.min(this.extent.East, Cesium.Math.toDegrees(view.east));
|
|
42044
|
+
const south = Math.max(this.extent.South, Cesium.Math.toDegrees(view.south));
|
|
42045
|
+
const north = Math.min(this.extent.North, Cesium.Math.toDegrees(view.north));
|
|
42046
|
+
if (west >= east || south >= north) {
|
|
42047
|
+
return null;
|
|
42048
|
+
}
|
|
42049
|
+
const spanLon = this.extent.East - this.extent.West;
|
|
42050
|
+
const spanLat = this.extent.North - this.extent.South;
|
|
42051
|
+
return {
|
|
42052
|
+
col0: Math.max(0, Math.floor((west - this.extent.West) / spanLon * cols) - 1),
|
|
42053
|
+
col1: Math.min(cols - 1, Math.ceil((east - this.extent.West) / spanLon * cols) + 1),
|
|
42054
|
+
row0: Math.max(0, Math.floor((this.extent.North - north) / spanLat * rows) - 1),
|
|
42055
|
+
row1: Math.min(rows - 1, Math.ceil((this.extent.North - south) / spanLat * rows) + 1)
|
|
42056
|
+
};
|
|
42057
|
+
}
|
|
42058
|
+
project(lon, lat) {
|
|
42059
|
+
const world = Cesium.Cartesian3.fromDegrees(lon, lat, 0);
|
|
42060
|
+
const transforms = Cesium.SceneTransforms;
|
|
42061
|
+
// Renamed in newer Cesium, and this library runs on whatever the host app supplies.
|
|
42062
|
+
const fn = transforms.worldToWindowCoordinates || transforms.wgs84ToWindowCoordinates;
|
|
42063
|
+
return fn ? fn.call(transforms, this.viewer.scene, world) : null;
|
|
42064
|
+
}
|
|
42065
|
+
}
|
|
42066
|
+
TextureValueLabels.Labels = Labels;
|
|
42067
|
+
/*
|
|
42068
|
+
* The value line. Units go on it alone: repeating them on the range underneath doubles the widest
|
|
42069
|
+
* label to say nothing, and the two lines are plainly the same quantity.
|
|
42070
|
+
*/
|
|
42071
|
+
function valueLine(value, units) {
|
|
42072
|
+
return units ? `${value} (${units})` : value;
|
|
42073
|
+
}
|
|
42074
|
+
function decode16(data, i) {
|
|
42075
|
+
return (data[i] * 256 + data[i + 1]) / 65535;
|
|
42076
|
+
}
|
|
42077
|
+
})(exports.TextureValueLabels || (exports.TextureValueLabels = {}));
|
|
40656
42078
|
|
|
40657
42079
|
(function (DisplacedSurfacePrimitive) {
|
|
40658
42080
|
// Grid densities a tile can draw at, coarsest first.
|
|
@@ -40669,8 +42091,6 @@
|
|
|
40669
42091
|
const SKIRT_FRACTION = 0.015;
|
|
40670
42092
|
// Alpha below which a texel counts as no-data and is not drawn at all.
|
|
40671
42093
|
const COVERAGE_CUTOFF = 0.35;
|
|
40672
|
-
// With no exaggeration stated, the full value range is drawn as this fraction of the extent's shorter side.
|
|
40673
|
-
const AUTO_RELIEF_FRACTION = 0.02;
|
|
40674
42094
|
// Terrain is sampled on this grid and interpolated between samples.
|
|
40675
42095
|
// Per-texel sampling would be hundreds of thousands of lookups for a surface whose own data is far coarser than that.
|
|
40676
42096
|
const GROUND_SAMPLES_PER_SIDE = 64;
|
|
@@ -40691,6 +42111,7 @@ in float skirt;
|
|
|
40691
42111
|
|
|
40692
42112
|
uniform sampler2D u_valueTexture;
|
|
40693
42113
|
uniform sampler2D u_groundTexture;
|
|
42114
|
+
uniform float u_valuePacked;
|
|
40694
42115
|
uniform float u_valueMin;
|
|
40695
42116
|
uniform float u_valueRange;
|
|
40696
42117
|
uniform float u_exaggeration;
|
|
@@ -40728,18 +42149,37 @@ float sampleGround(vec2 uv) {
|
|
|
40728
42149
|
return mix(mix(h00, h10, f.x), mix(h01, h11, f.x), f.y);
|
|
40729
42150
|
}
|
|
40730
42151
|
|
|
42152
|
+
/*
|
|
42153
|
+
* The texel's normalised value, whichever way the archive packed it.
|
|
42154
|
+
*
|
|
42155
|
+
* A packed frame keeps the high byte in R, so reading R alone would quantise the whole range to 255
|
|
42156
|
+
* steps and draw a plausible wrong surface rather than an obviously broken one.
|
|
42157
|
+
*
|
|
42158
|
+
* Each channel is scaled and then added, rather than reassembled to a 0..65535 integer and divided.
|
|
42159
|
+
* That integer is outside the range mediump guarantees, and a fragment shader only gets highp where
|
|
42160
|
+
* the device offers it, so the obvious form loses the low byte on exactly the older mobile hardware
|
|
42161
|
+
* that Cesium falls back to mediump for.
|
|
42162
|
+
*/
|
|
42163
|
+
float decodeValue(vec4 texel) {
|
|
42164
|
+
float packed16 = texel.r * (255.0 * 256.0 / 65535.0) + texel.g * (255.0 / 65535.0);
|
|
42165
|
+
return mix(texel.r, packed16, u_valuePacked);
|
|
42166
|
+
}
|
|
42167
|
+
|
|
40731
42168
|
void main() {
|
|
40732
42169
|
vec4 texel = texture(u_valueTexture, st);
|
|
40733
42170
|
v_st = st;
|
|
40734
|
-
v_value = texel
|
|
42171
|
+
v_value = decodeValue(texel);
|
|
40735
42172
|
v_coverage = texel.a;
|
|
40736
42173
|
|
|
40737
|
-
|
|
42174
|
+
// Exaggeration stretches the span above the archive's minimum rather than the height itself. On an
|
|
42175
|
+
// elevation the height is measured from the ellipsoid, tens of metres from any of the data, so
|
|
42176
|
+
// scaling that instead translates the whole sheet underground rather than spreading it out.
|
|
42177
|
+
float metres = u_valueMin + v_value * u_valueRange * u_exaggeration;
|
|
40738
42178
|
|
|
40739
42179
|
// Value zero sits at the polygon's own altitude, plus the terrain underneath when the polygon
|
|
40740
42180
|
// is following the ground rather than absolutely positioned.
|
|
40741
42181
|
float ground = sampleGround(st) * u_groundWeight;
|
|
40742
|
-
float displacement = u_baseHeight + ground + metres *
|
|
42182
|
+
float displacement = u_baseHeight + ground + metres * texel.a - skirt * u_skirtDepth;
|
|
40743
42183
|
|
|
40744
42184
|
// Added to the LOW half of the encoded position: a metre-scale offset added to the high half
|
|
40745
42185
|
// would be lost to float32 rounding at an earth radius.
|
|
@@ -40749,10 +42189,10 @@ void main() {
|
|
|
40749
42189
|
`;
|
|
40750
42190
|
const FRAGMENT_SHADER_GLSL300 = `
|
|
40751
42191
|
uniform sampler2D u_valueTexture;
|
|
40752
|
-
uniform
|
|
40753
|
-
uniform vec4 u_highColor;
|
|
42192
|
+
uniform sampler2D u_rampTexture;
|
|
40754
42193
|
uniform float u_coverageCutoff;
|
|
40755
42194
|
uniform vec2 u_texelSize;
|
|
42195
|
+
uniform float u_valuePacked;
|
|
40756
42196
|
uniform float u_valueRange;
|
|
40757
42197
|
uniform float u_exaggeration;
|
|
40758
42198
|
uniform float u_metresPerTexel;
|
|
@@ -40761,18 +42201,34 @@ in vec2 v_st;
|
|
|
40761
42201
|
in float v_value;
|
|
40762
42202
|
in float v_coverage;
|
|
40763
42203
|
|
|
42204
|
+
/*
|
|
42205
|
+
* Scaled per channel for the same reason the vertex shader's decode is: reassembling the pair reaches
|
|
42206
|
+
* 65535, which mediump does not have to represent, and this is the shader that may not get highp.
|
|
42207
|
+
*/
|
|
42208
|
+
float valueAt(vec2 uv) {
|
|
42209
|
+
vec4 texel = texture(u_valueTexture, uv);
|
|
42210
|
+
float packed16 = texel.r * (255.0 * 256.0 / 65535.0) + texel.g * (255.0 / 65535.0);
|
|
42211
|
+
return mix(texel.r, packed16, u_valuePacked);
|
|
42212
|
+
}
|
|
42213
|
+
|
|
40764
42214
|
void main() {
|
|
40765
42215
|
if (v_coverage < u_coverageCutoff) {
|
|
40766
42216
|
discard;
|
|
40767
42217
|
}
|
|
40768
|
-
|
|
42218
|
+
// The ramp is baked to a lookup rather than lerped between two colours, so a banded style with a
|
|
42219
|
+
// hidden floor reads the same here as it does on the flat drape.
|
|
42220
|
+
// Without it this surface painted water the style hides, and the cell grid then correctly refused to outline any of it.
|
|
42221
|
+
vec4 color = texture(u_rampTexture, vec2(clamp(v_value, 0.0, 1.0), 0.5));
|
|
42222
|
+
if (color.a <= 0.0) {
|
|
42223
|
+
discard;
|
|
42224
|
+
}
|
|
40769
42225
|
|
|
40770
42226
|
// Relief shading from the value gradient, so the displacement reads at a distance without
|
|
40771
42227
|
// recomputing vertex normals every time the texture changes.
|
|
40772
|
-
float left =
|
|
40773
|
-
float right =
|
|
40774
|
-
float down =
|
|
40775
|
-
float up =
|
|
42228
|
+
float left = valueAt(v_st - vec2(u_texelSize.x, 0.0));
|
|
42229
|
+
float right = valueAt(v_st + vec2(u_texelSize.x, 0.0));
|
|
42230
|
+
float down = valueAt(v_st - vec2(0.0, u_texelSize.y));
|
|
42231
|
+
float up = valueAt(v_st + vec2(0.0, u_texelSize.y));
|
|
40776
42232
|
float scale = u_valueRange * u_exaggeration;
|
|
40777
42233
|
vec3 n = normalize(vec3((left - right) * scale, (down - up) * scale, 2.0 * u_metresPerTexel));
|
|
40778
42234
|
float lambert = clamp(dot(n, normalize(vec3(-0.5, -0.6, 0.62))), 0.0, 1.0);
|
|
@@ -40964,6 +42420,7 @@ void main() {
|
|
|
40964
42420
|
this.show = true;
|
|
40965
42421
|
this.valueSource = null;
|
|
40966
42422
|
this.lastPresentVersion = -1;
|
|
42423
|
+
this.rampTexture = null;
|
|
40967
42424
|
this.texture = null;
|
|
40968
42425
|
this.textureDirty = true;
|
|
40969
42426
|
this.shaderProgram = null;
|
|
@@ -40981,12 +42438,12 @@ void main() {
|
|
|
40981
42438
|
this.source = options.source || null;
|
|
40982
42439
|
this.valueMin = (_a = options.valueMin) !== null && _a !== void 0 ? _a : 0;
|
|
40983
42440
|
this.valueMax = (_b = options.valueMax) !== null && _b !== void 0 ? _b : 1;
|
|
40984
|
-
this.exaggeration = (_c = options.exaggeration) !== null && _c !== void 0 ? _c :
|
|
42441
|
+
this.exaggeration = (_c = options.exaggeration) !== null && _c !== void 0 ? _c : 1;
|
|
40985
42442
|
this.baseHeight = (_d = options.baseHeight) !== null && _d !== void 0 ? _d : 0;
|
|
42443
|
+
this.packedValue = Boolean(options.packedValue);
|
|
40986
42444
|
this.pixelPlacement = options.pixelPlacement;
|
|
40987
42445
|
this.tileSkirts = Boolean(options.tileSkirts);
|
|
40988
|
-
this.
|
|
40989
|
-
this.highColor = toCesiumColor(options.highColor || DEFAULT_HIGH_COLOR);
|
|
42446
|
+
this.rampPixels = RampLookup(options.rampStops || null, options.lowColor || DEFAULT_LOW_COLOR, options.highColor || DEFAULT_HIGH_COLOR);
|
|
40990
42447
|
this.tiles = buildTiles(hasCoverage);
|
|
40991
42448
|
}
|
|
40992
42449
|
GetFollowsGround() {
|
|
@@ -41045,6 +42502,7 @@ void main() {
|
|
|
41045
42502
|
return;
|
|
41046
42503
|
}
|
|
41047
42504
|
this.syncTexture(context);
|
|
42505
|
+
this.syncRampTexture(context);
|
|
41048
42506
|
this.requestGround();
|
|
41049
42507
|
this.syncGroundTexture(context);
|
|
41050
42508
|
// A tile needs one mesh before it has a bounding sphere to measure against.
|
|
@@ -41083,6 +42541,10 @@ void main() {
|
|
|
41083
42541
|
this.texture.destroy();
|
|
41084
42542
|
this.texture = null;
|
|
41085
42543
|
}
|
|
42544
|
+
if (this.rampTexture) {
|
|
42545
|
+
this.rampTexture.destroy();
|
|
42546
|
+
this.rampTexture = null;
|
|
42547
|
+
}
|
|
41086
42548
|
if (this.groundTexture) {
|
|
41087
42549
|
this.groundTexture.destroy();
|
|
41088
42550
|
this.groundTexture = null;
|
|
@@ -41176,6 +42638,25 @@ void main() {
|
|
|
41176
42638
|
const width = Cesium.Cartesian3.distance(southWest, southEast);
|
|
41177
42639
|
return Math.max(1, width / Math.max(1, this.source ? this.source.width : 1));
|
|
41178
42640
|
}
|
|
42641
|
+
syncRampTexture(context) {
|
|
42642
|
+
if (this.rampTexture) {
|
|
42643
|
+
return;
|
|
42644
|
+
}
|
|
42645
|
+
this.rampTexture = new Cesium.Texture({
|
|
42646
|
+
context,
|
|
42647
|
+
pixelFormat: Cesium.PixelFormat.RGBA,
|
|
42648
|
+
source: { width: this.rampPixels.length / 4, height: 1,
|
|
42649
|
+
arrayBufferView: this.rampPixels },
|
|
42650
|
+
flipY: false,
|
|
42651
|
+
sampler: new Cesium.Sampler({
|
|
42652
|
+
wrapS: Cesium.TextureWrap.CLAMP_TO_EDGE,
|
|
42653
|
+
wrapT: Cesium.TextureWrap.CLAMP_TO_EDGE,
|
|
42654
|
+
// Linear across the ramp, so a gradient between stops stays smooth.
|
|
42655
|
+
minificationFilter: Cesium.TextureMinificationFilter.LINEAR,
|
|
42656
|
+
magnificationFilter: Cesium.TextureMagnificationFilter.LINEAR
|
|
42657
|
+
})
|
|
42658
|
+
});
|
|
42659
|
+
}
|
|
41179
42660
|
syncTexture(context) {
|
|
41180
42661
|
const sizeChanged = this.texture
|
|
41181
42662
|
&& (this.texture.width !== this.source.width || this.texture.height !== this.source.height);
|
|
@@ -41183,6 +42664,14 @@ void main() {
|
|
|
41183
42664
|
if (this.texture) {
|
|
41184
42665
|
this.texture.destroy();
|
|
41185
42666
|
}
|
|
42667
|
+
// Nearest for a packed value, because filtering would blend the two bytes of the
|
|
42668
|
+
// packing rather than the values they encode, the same trap sampleGround avoids.
|
|
42669
|
+
const minification = this.packedValue
|
|
42670
|
+
? Cesium.TextureMinificationFilter.NEAREST
|
|
42671
|
+
: Cesium.TextureMinificationFilter.LINEAR;
|
|
42672
|
+
const magnification = this.packedValue
|
|
42673
|
+
? Cesium.TextureMagnificationFilter.NEAREST
|
|
42674
|
+
: Cesium.TextureMagnificationFilter.LINEAR;
|
|
41186
42675
|
this.texture = new Cesium.Texture({
|
|
41187
42676
|
context,
|
|
41188
42677
|
source: this.source,
|
|
@@ -41192,8 +42681,8 @@ void main() {
|
|
|
41192
42681
|
sampler: new Cesium.Sampler({
|
|
41193
42682
|
wrapS: Cesium.TextureWrap.CLAMP_TO_EDGE,
|
|
41194
42683
|
wrapT: Cesium.TextureWrap.CLAMP_TO_EDGE,
|
|
41195
|
-
minificationFilter:
|
|
41196
|
-
magnificationFilter:
|
|
42684
|
+
minificationFilter: minification,
|
|
42685
|
+
magnificationFilter: magnification
|
|
41197
42686
|
})
|
|
41198
42687
|
});
|
|
41199
42688
|
this.textureDirty = false;
|
|
@@ -41294,6 +42783,7 @@ void main() {
|
|
|
41294
42783
|
uniformMap: {
|
|
41295
42784
|
u_valueTexture: () => self.texture,
|
|
41296
42785
|
u_groundTexture: () => self.groundTexture,
|
|
42786
|
+
u_valuePacked: () => (self.packedValue ? 1 : 0),
|
|
41297
42787
|
u_groundWeight: () => (self.followGround && self.groundPixels ? 1 : 0),
|
|
41298
42788
|
u_groundRange: () => new Cesium.Cartesian2(GROUND_MIN_METRES, GROUND_MAX_METRES),
|
|
41299
42789
|
u_groundSize: () => new Cesium.Cartesian2(self.groundPixels ? GROUND_SAMPLES_PER_SIDE : 1, self.groundPixels ? GROUND_SAMPLES_PER_SIDE : 1),
|
|
@@ -41306,8 +42796,7 @@ void main() {
|
|
|
41306
42796
|
? Math.max(2, Math.abs(self.valueMax - self.valueMin) * self.exaggeration * SKIRT_FRACTION)
|
|
41307
42797
|
: 0,
|
|
41308
42798
|
u_coverageCutoff: () => COVERAGE_CUTOFF,
|
|
41309
|
-
|
|
41310
|
-
u_highColor: () => self.highColor,
|
|
42799
|
+
u_rampTexture: () => self.rampTexture,
|
|
41311
42800
|
u_texelSize: () => new Cesium.Cartesian2(1 / self.source.width, 1 / self.source.height)
|
|
41312
42801
|
}
|
|
41313
42802
|
});
|
|
@@ -41347,33 +42836,6 @@ void main() {
|
|
|
41347
42836
|
}
|
|
41348
42837
|
return tiles;
|
|
41349
42838
|
}
|
|
41350
|
-
/**
|
|
41351
|
-
* Exaggeration that makes the full value range stand AUTO_RELIEF_FRACTION of the extent tall.
|
|
41352
|
-
*
|
|
41353
|
-
* Derived from the whole extent rather than per tile: scaling each tile to its own size would
|
|
41354
|
-
* make neighbours disagree along their shared edge and tear the surface apart.
|
|
41355
|
-
* @param extent
|
|
41356
|
-
* @param valueMin
|
|
41357
|
-
* @param valueMax
|
|
41358
|
-
*/
|
|
41359
|
-
function autoExaggeration(extent, valueMin, valueMax) {
|
|
41360
|
-
const range = Math.abs(valueMax - valueMin);
|
|
41361
|
-
if (!(range > 0)) {
|
|
41362
|
-
return 1;
|
|
41363
|
-
}
|
|
41364
|
-
const southWest = Cesium.Cartesian3.fromDegrees(extent.West, extent.South, 0);
|
|
41365
|
-
const southEast = Cesium.Cartesian3.fromDegrees(extent.East, extent.South, 0);
|
|
41366
|
-
const northWest = Cesium.Cartesian3.fromDegrees(extent.West, extent.North, 0);
|
|
41367
|
-
const shorterSide = Math.min(Cesium.Cartesian3.distance(southWest, southEast), Cesium.Cartesian3.distance(southWest, northWest));
|
|
41368
|
-
if (!(shorterSide > 0)) {
|
|
41369
|
-
return 1;
|
|
41370
|
-
}
|
|
41371
|
-
return (shorterSide * AUTO_RELIEF_FRACTION) / range;
|
|
41372
|
-
}
|
|
41373
|
-
DisplacedSurfacePrimitive.autoExaggeration = autoExaggeration;
|
|
41374
|
-
function toCesiumColor(color) {
|
|
41375
|
-
return new Cesium.Color(color.red / 255, color.green / 255, color.blue / 255, color.alpha);
|
|
41376
|
-
}
|
|
41377
42839
|
function now() {
|
|
41378
42840
|
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
41379
42841
|
}
|
|
@@ -41381,10 +42843,11 @@ void main() {
|
|
|
41381
42843
|
|
|
41382
42844
|
const TEXTURE_FRAME_SERIES_ANIMATOR_KEY = "TextureFrameSeriesAnimator.Animator";
|
|
41383
42845
|
const EXTRUSION_ANIMATOR_KEY = "TextureFrameSeriesAnimator.ExtrusionAnimator";
|
|
42846
|
+
const TEXTURE_VALUE_LABELS_KEY = "TextureValueLabels.Overlay";
|
|
41384
42847
|
const DISPLACED_SURFACE_KEY = "DisplacedSurfacePrimitive.Surface";
|
|
41385
42848
|
(function (EntityRenderEnginePolygon) {
|
|
41386
42849
|
async function Render(params) {
|
|
41387
|
-
var _a, _b, _c, _d;
|
|
42850
|
+
var _a, _b, _c, _d, _e;
|
|
41388
42851
|
const entity = params.entity;
|
|
41389
42852
|
const style = params.style;
|
|
41390
42853
|
const pRings = BModels.Entity.GetValue({
|
|
@@ -41435,18 +42898,22 @@ void main() {
|
|
|
41435
42898
|
console.error(`Polygon.Render: failed to resolve extrusion texture for entity ${((_b = entity === null || entity === void 0 ? void 0 : entity.Bruce) === null || _b === void 0 ? void 0 : _b.ID) || "<NONE>"}:`, e);
|
|
41436
42899
|
}
|
|
41437
42900
|
}
|
|
42901
|
+
const retained = params.offline
|
|
42902
|
+
? (_c = params.rendered) === null || _c === void 0 ? void 0 : _c[TEXTURE_FRAME_SERIES_ANIMATOR_KEY]
|
|
42903
|
+
: null;
|
|
42904
|
+
const keepsAnimator = Boolean(retained && !retained.IsDisposed());
|
|
41438
42905
|
const drawsDisplacedSurface = Boolean(extrusionArchive);
|
|
41439
42906
|
// The displaced surface IS the entity's fill, drawn as its own primitive at the value's height.
|
|
41440
42907
|
// Leaving the flat polygon filled as well shows a second copy of the parcel on the ground under it.
|
|
41441
42908
|
const hasFill = (drawsDisplacedSurface ? true
|
|
41442
42909
|
: fillType === BModels.Style.EPolygonFillType.Texture
|
|
41443
|
-
? Boolean(textureDataUri || frameArchive || cFillColor.alpha > 0)
|
|
42910
|
+
? Boolean(textureDataUri || frameArchive || keepsAnimator || cFillColor.alpha > 0)
|
|
41444
42911
|
: cFillColor.alpha > 0);
|
|
41445
42912
|
const fillMaterial = drawsDisplacedSurface
|
|
41446
42913
|
? Cesium.Color.WHITE.withAlpha(0)
|
|
41447
42914
|
: textureDataUri
|
|
41448
42915
|
? new Cesium.ImageMaterialProperty({ image: textureDataUri, transparent: true })
|
|
41449
|
-
: frameArchive
|
|
42916
|
+
: (frameArchive || keepsAnimator)
|
|
41450
42917
|
? Cesium.Color.WHITE.withAlpha(0)
|
|
41451
42918
|
: cFillColor;
|
|
41452
42919
|
const lineColorTrace = style.lineColor ? BModels.Calculator.TraceGetColor(style.lineColor, entity, params.tags) : { value: null, effective: null };
|
|
@@ -41616,7 +43083,7 @@ void main() {
|
|
|
41616
43083
|
}
|
|
41617
43084
|
// Must run before any material is baked below: the Animator takes over polygon.material, so a bake
|
|
41618
43085
|
// has to know whether it is baking over a live animation or over a disposed one's restored material.
|
|
41619
|
-
const animator = syncTextureFrameArchive(cEntity, drawsDisplacedSurface ? null : frameArchive, params.viewer, style.textureColorMask, style.maskTextureBaseline);
|
|
43086
|
+
const animator = syncTextureFrameArchive(cEntity, drawsDisplacedSurface ? null : frameArchive, params.viewer, style.textureColorMask, style.maskTextureBaseline, undefined, undefined, wantsLabels(style), params.offline, polygonBbox(posses));
|
|
41620
43087
|
syncDisplacedSurface({
|
|
41621
43088
|
cEntity,
|
|
41622
43089
|
extrusionArchive,
|
|
@@ -41624,7 +43091,30 @@ void main() {
|
|
|
41624
43091
|
style,
|
|
41625
43092
|
entity,
|
|
41626
43093
|
heightRef,
|
|
41627
|
-
outerRingPosses: posses
|
|
43094
|
+
outerRingPosses: posses,
|
|
43095
|
+
offline: params.offline
|
|
43096
|
+
});
|
|
43097
|
+
const drapeExtent = polygonBbox(posses);
|
|
43098
|
+
const labelArchive = drawsDisplacedSurface ? extrusionArchive : frameArchive;
|
|
43099
|
+
syncTextureValueLabels({
|
|
43100
|
+
cEntity,
|
|
43101
|
+
viewer: params.viewer,
|
|
43102
|
+
settings: overlaySettings(style, entity, params.tags),
|
|
43103
|
+
extent: labelArchive
|
|
43104
|
+
? (drawsDisplacedSurface ? archiveExtent(labelArchive.metadata, posses) : drapeExtent)
|
|
43105
|
+
: null,
|
|
43106
|
+
animator: drawsDisplacedSurface
|
|
43107
|
+
? cEntity[EXTRUSION_ANIMATOR_KEY]
|
|
43108
|
+
: animator,
|
|
43109
|
+
metadata: labelArchive ? labelArchive.metadata : undefined,
|
|
43110
|
+
offline: params.offline,
|
|
43111
|
+
clipRing: posses.map((pos) => {
|
|
43112
|
+
const carto = Cesium.Cartographic.fromCartesian(pos);
|
|
43113
|
+
return {
|
|
43114
|
+
lon: Cesium.Math.toDegrees(carto.longitude),
|
|
43115
|
+
lat: Cesium.Math.toDegrees(carto.latitude)
|
|
43116
|
+
};
|
|
43117
|
+
})
|
|
41628
43118
|
});
|
|
41629
43119
|
if (animator) {
|
|
41630
43120
|
exports.CesiumEntityStyler.SetDefaultTextureImage({
|
|
@@ -41685,7 +43175,7 @@ void main() {
|
|
|
41685
43175
|
}
|
|
41686
43176
|
}
|
|
41687
43177
|
let borderPosses = posses.map(x => x.clone ? x.clone() : { ...x });
|
|
41688
|
-
let cEntityBorder = (
|
|
43178
|
+
let cEntityBorder = (_e = (_d = params.rendered) === null || _d === void 0 ? void 0 : _d._siblingGraphics) === null || _e === void 0 ? void 0 : _e[0];
|
|
41689
43179
|
cEntity._siblingGraphics = [];
|
|
41690
43180
|
if (!cEntityBorder || ((!cEntityBorder.polyline && units == "px") ||
|
|
41691
43181
|
(!cEntityBorder.corridor && units == "m"))) {
|
|
@@ -41936,6 +43426,11 @@ void main() {
|
|
|
41936
43426
|
* Disposes a cEntity's TextureFrameSeriesAnimator.Animator (if any).
|
|
41937
43427
|
*/
|
|
41938
43428
|
function DisposeTextureFrameSeriesAnimator(cEntity, viewer) {
|
|
43429
|
+
const labelOverlay = cEntity === null || cEntity === void 0 ? void 0 : cEntity[TEXTURE_VALUE_LABELS_KEY];
|
|
43430
|
+
if (labelOverlay) {
|
|
43431
|
+
labelOverlay.remove();
|
|
43432
|
+
cEntity[TEXTURE_VALUE_LABELS_KEY] = null;
|
|
43433
|
+
}
|
|
41939
43434
|
const existing = cEntity === null || cEntity === void 0 ? void 0 : cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY];
|
|
41940
43435
|
if (existing && !existing.IsDisposed()) {
|
|
41941
43436
|
existing.Dispose();
|
|
@@ -42061,10 +43556,13 @@ void main() {
|
|
|
42061
43556
|
* @param params
|
|
42062
43557
|
*/
|
|
42063
43558
|
function syncDisplacedSurface(params) {
|
|
42064
|
-
var _a;
|
|
42065
43559
|
const { cEntity, extrusionArchive, viewer, style, entity, heightRef, outerRingPosses } = params;
|
|
42066
43560
|
const existingAnimator = cEntity[EXTRUSION_ANIMATOR_KEY];
|
|
42067
43561
|
const existingSurface = cEntity[DISPLACED_SURFACE_KEY];
|
|
43562
|
+
// Same reason as the fill animator: an unresolvable archive is not a removed one.
|
|
43563
|
+
if (params.offline && existingSurface && !existingSurface.isDestroyed()) {
|
|
43564
|
+
return;
|
|
43565
|
+
}
|
|
42068
43566
|
// Nothing to draw and nothing left over, which is every polygon that does not use this feature.
|
|
42069
43567
|
if (!extrusionArchive && !existingAnimator && !existingSurface) {
|
|
42070
43568
|
return;
|
|
@@ -42075,12 +43573,12 @@ void main() {
|
|
|
42075
43573
|
&& existingAnimator.GetArchiveUrl() === extrusionArchive.url
|
|
42076
43574
|
&& existingSurface
|
|
42077
43575
|
&& !existingSurface.isDestroyed();
|
|
42078
|
-
const placement = surfacePlacement(entity, heightRef);
|
|
43576
|
+
const placement = surfacePlacement(entity, heightRef, extrusionArchive && exports.TextureFrameSeriesAnimator.IsDatumHeight(extrusionArchive.metadata));
|
|
42079
43577
|
// Whether the surface follows terrain is baked into its ground sampling, so a change there has
|
|
42080
43578
|
// to rebuild rather than update in place.
|
|
42081
43579
|
if (reusable && existingSurface.GetFollowsGround() === placement.followGround) {
|
|
42082
43580
|
existingSurface.SetBaseHeight(placement.baseHeight);
|
|
42083
|
-
existingSurface.SetExaggeration((
|
|
43581
|
+
existingSurface.SetExaggeration(exaggerationFor(style));
|
|
42084
43582
|
return;
|
|
42085
43583
|
}
|
|
42086
43584
|
disposeDisplacedSurface(cEntity, viewer);
|
|
@@ -42094,7 +43592,7 @@ void main() {
|
|
|
42094
43592
|
}
|
|
42095
43593
|
// Drives the value canvas only: the surface paints the colour, so the polygon's own material is
|
|
42096
43594
|
// left transparent rather than animated underneath it.
|
|
42097
|
-
const animator = new TextureFrameSeriesAnimator.Animator({
|
|
43595
|
+
const animator = new exports.TextureFrameSeriesAnimator.Animator({
|
|
42098
43596
|
viewer,
|
|
42099
43597
|
entity: cEntity,
|
|
42100
43598
|
archiveUrl: extrusionArchive.url,
|
|
@@ -42102,8 +43600,8 @@ void main() {
|
|
|
42102
43600
|
textureColorMask: style.textureColorMask,
|
|
42103
43601
|
driveMaterial: false,
|
|
42104
43602
|
produceValueCanvas: true,
|
|
42105
|
-
|
|
42106
|
-
|
|
43603
|
+
metadata: extrusionArchive.metadata,
|
|
43604
|
+
cellTexels: 1,
|
|
42107
43605
|
maskBaseline: style.maskTextureBaseline
|
|
42108
43606
|
});
|
|
42109
43607
|
cEntity[EXTRUSION_ANIMATOR_KEY] = animator;
|
|
@@ -42118,9 +43616,16 @@ void main() {
|
|
|
42118
43616
|
terrainProvider: viewer && viewer.terrainProvider,
|
|
42119
43617
|
valueMin: extrusionArchive.metadata.ValueMin,
|
|
42120
43618
|
valueMax: extrusionArchive.metadata.ValueMax,
|
|
42121
|
-
|
|
43619
|
+
packedValue: exports.TextureFrameSeriesAnimator.IsPackedValue(extrusionArchive.metadata),
|
|
43620
|
+
exaggeration: exaggerationFor(style),
|
|
42122
43621
|
lowColor: mask ? BModels.Color.ColorFromStr(mask.minColor) : null,
|
|
42123
|
-
highColor: mask ? BModels.Color.ColorFromStr(mask.maxColor) : null
|
|
43622
|
+
highColor: mask ? BModels.Color.ColorFromStr(mask.maxColor) : null,
|
|
43623
|
+
// Normalised the same way the animator normalises them, so the surface and the flat drape
|
|
43624
|
+
// agree about which values the style hides.
|
|
43625
|
+
rampStops: ((mask && mask.points) || []).map((stop) => ({
|
|
43626
|
+
position: exports.TextureFrameSeriesAnimator.NormalisePosition(extrusionArchive.metadata, stop.position),
|
|
43627
|
+
color: BModels.Color.ColorFromStr(stop.color)
|
|
43628
|
+
})).filter((stop) => Boolean(stop.color))
|
|
42124
43629
|
});
|
|
42125
43630
|
surface.BindAnimator(animator);
|
|
42126
43631
|
viewer.scene.primitives.add(surface);
|
|
@@ -42133,7 +43638,7 @@ void main() {
|
|
|
42133
43638
|
* @param entity
|
|
42134
43639
|
* @param heightRef
|
|
42135
43640
|
*/
|
|
42136
|
-
function surfacePlacement(entity, heightRef) {
|
|
43641
|
+
function surfacePlacement(entity, heightRef, datumHeight) {
|
|
42137
43642
|
const rawAltitude = BModels.Entity.GetValue({
|
|
42138
43643
|
entity,
|
|
42139
43644
|
path: ["Bruce", "Location", "altitude"]
|
|
@@ -42145,14 +43650,34 @@ void main() {
|
|
|
42145
43650
|
if (heightRef === Cesium.HeightReference.RELATIVE_TO_GROUND) {
|
|
42146
43651
|
return { baseHeight: altitude, followGround: true };
|
|
42147
43652
|
}
|
|
42148
|
-
|
|
43653
|
+
// Values that ARE heights against a datum already say where they belong, so adding the entity's
|
|
43654
|
+
// altitude on top would move a measured surface off the datum it was measured against.
|
|
43655
|
+
return { baseHeight: datumHeight ? 0 : altitude, followGround: false };
|
|
42149
43656
|
}
|
|
42150
|
-
|
|
42151
|
-
* The
|
|
42152
|
-
* generated before the extent was written still render somewhere sensible.
|
|
42153
|
-
* @param metadata
|
|
42154
|
-
* @param posses
|
|
43657
|
+
/*
|
|
43658
|
+
* The factor a displaced surface should use, which is the style's or nothing.
|
|
42155
43659
|
*/
|
|
43660
|
+
function exaggerationFor(style) {
|
|
43661
|
+
if (style.extrusionExaggeration != null) {
|
|
43662
|
+
return style.extrusionExaggeration;
|
|
43663
|
+
}
|
|
43664
|
+
return 1;
|
|
43665
|
+
}
|
|
43666
|
+
/*
|
|
43667
|
+
* The lon/lat bounding rectangle of a ring, which is the frame Cesium drapes an image material in.
|
|
43668
|
+
*/
|
|
43669
|
+
function polygonBbox(posses) {
|
|
43670
|
+
if (!posses || posses.length === 0) {
|
|
43671
|
+
return undefined;
|
|
43672
|
+
}
|
|
43673
|
+
const rectangle = Cesium.Rectangle.fromCartesianArray(posses);
|
|
43674
|
+
return {
|
|
43675
|
+
West: Cesium.Math.toDegrees(rectangle.west),
|
|
43676
|
+
East: Cesium.Math.toDegrees(rectangle.east),
|
|
43677
|
+
South: Cesium.Math.toDegrees(rectangle.south),
|
|
43678
|
+
North: Cesium.Math.toDegrees(rectangle.north)
|
|
43679
|
+
};
|
|
43680
|
+
}
|
|
42156
43681
|
function archiveExtent(metadata, posses) {
|
|
42157
43682
|
if (metadata.West != null && metadata.East != null && metadata.South != null && metadata.North != null) {
|
|
42158
43683
|
return { West: metadata.West, East: metadata.East, South: metadata.South, North: metadata.North };
|
|
@@ -42203,9 +43728,22 @@ void main() {
|
|
|
42203
43728
|
* @param viewer
|
|
42204
43729
|
* @param textureColorMask
|
|
42205
43730
|
*/
|
|
42206
|
-
function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask, maskTextureBaseline) {
|
|
43731
|
+
function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask, maskTextureBaseline, cellBorder, cellTexels, produceValueCanvas, offline, drapeExtent) {
|
|
42207
43732
|
const existing = cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY];
|
|
42208
|
-
|
|
43733
|
+
// Offline says nothing about whether the archive changed, only that it could not be asked.
|
|
43734
|
+
if (offline && existing && !existing.IsDisposed()) {
|
|
43735
|
+
return existing;
|
|
43736
|
+
}
|
|
43737
|
+
// Appearance is part of the reuse test, not just the archive URL. Recolouring a texture-driven
|
|
43738
|
+
// polygon keeps the same archive, so matching on the URL alone kept the old Animator and the
|
|
43739
|
+
// style edit appeared to do nothing at all.
|
|
43740
|
+
const appearance = exports.TextureFrameSeriesAnimator.AppearanceSignature({
|
|
43741
|
+
textureColorMask, cellBorder, cellTexels, maskBaseline: maskTextureBaseline,
|
|
43742
|
+
valueCanvas: produceValueCanvas, drape: drapeExtent
|
|
43743
|
+
});
|
|
43744
|
+
if (frameArchive && existing && !existing.IsDisposed()
|
|
43745
|
+
&& existing.GetArchiveUrl() === frameArchive.url
|
|
43746
|
+
&& existing.GetAppearanceSignature() === appearance) {
|
|
42209
43747
|
return existing;
|
|
42210
43748
|
}
|
|
42211
43749
|
if (existing && !existing.IsDisposed()) {
|
|
@@ -42215,18 +43753,121 @@ void main() {
|
|
|
42215
43753
|
if (!frameArchive) {
|
|
42216
43754
|
return null;
|
|
42217
43755
|
}
|
|
42218
|
-
const animator = new TextureFrameSeriesAnimator.Animator({
|
|
43756
|
+
const animator = new exports.TextureFrameSeriesAnimator.Animator({
|
|
42219
43757
|
viewer,
|
|
42220
43758
|
entity: cEntity,
|
|
42221
43759
|
archiveUrl: frameArchive.url,
|
|
42222
43760
|
frames: frameArchive.metadata.Frames,
|
|
42223
43761
|
textureColorMask,
|
|
42224
|
-
|
|
42225
|
-
|
|
43762
|
+
metadata: frameArchive.metadata,
|
|
43763
|
+
cellBorder,
|
|
43764
|
+
cellTexels,
|
|
43765
|
+
maskBaseline: maskTextureBaseline,
|
|
43766
|
+
produceValueCanvas,
|
|
43767
|
+
drapeExtent
|
|
42226
43768
|
});
|
|
42227
43769
|
cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY] = animator;
|
|
42228
43770
|
return animator;
|
|
42229
43771
|
}
|
|
43772
|
+
/**
|
|
43773
|
+
* Folds the cell-border and value-label style into the single overlay that draws both.
|
|
43774
|
+
*
|
|
43775
|
+
* Returns undefined when neither is asked for, which is what keeps the overlay off entirely.
|
|
43776
|
+
*/
|
|
43777
|
+
function wantsLabels(style) {
|
|
43778
|
+
const texture = style.texture;
|
|
43779
|
+
return Boolean(texture && !Array.isArray(texture) && texture.label);
|
|
43780
|
+
}
|
|
43781
|
+
/*
|
|
43782
|
+
* How the cell grid and its values are drawn, which the style does not get a say in.
|
|
43783
|
+
*
|
|
43784
|
+
* A white hairline reads over every ramp colour without competing with it, and the spacing and cap
|
|
43785
|
+
* are what a parcel-sized archive needs to stay readable while the camera moves. Exposing them
|
|
43786
|
+
* bought nothing except thresholds an author could get wrong.
|
|
43787
|
+
*/
|
|
43788
|
+
const LABEL_GRID_COLOR = "rgba(255,255,255,0.55)";
|
|
43789
|
+
const LABEL_GRID_WIDTH_PIXELS = 1;
|
|
43790
|
+
const LABEL_MIN_SPACING_PIXELS = 78;
|
|
43791
|
+
const LABEL_MAX_LABELS = 240;
|
|
43792
|
+
function overlaySettings(style, entity, tags) {
|
|
43793
|
+
if (!wantsLabels(style)) {
|
|
43794
|
+
return undefined;
|
|
43795
|
+
}
|
|
43796
|
+
return {
|
|
43797
|
+
gridColor: LABEL_GRID_COLOR,
|
|
43798
|
+
gridWidthPixels: LABEL_GRID_WIDTH_PIXELS,
|
|
43799
|
+
minSpacingPixels: LABEL_MIN_SPACING_PIXELS,
|
|
43800
|
+
maxLabels: LABEL_MAX_LABELS
|
|
43801
|
+
};
|
|
43802
|
+
}
|
|
43803
|
+
/**
|
|
43804
|
+
* Keeps a value-label overlay in step with whichever animator is driving the polygon.
|
|
43805
|
+
*
|
|
43806
|
+
* Redrawn on preRender rather than on frame changes: labels are screen-space, so panning and zooming
|
|
43807
|
+
* move them even when the data has not advanced at all.
|
|
43808
|
+
*/
|
|
43809
|
+
function syncTextureValueLabels(params) {
|
|
43810
|
+
const { cEntity, viewer, settings, extent, animator, metadata } = params;
|
|
43811
|
+
const existing = cEntity[TEXTURE_VALUE_LABELS_KEY];
|
|
43812
|
+
// Offline has no extent to rebuild the overlay from, so an existing one is left running.
|
|
43813
|
+
if (params.offline && existing) {
|
|
43814
|
+
return;
|
|
43815
|
+
}
|
|
43816
|
+
if (existing) {
|
|
43817
|
+
existing.remove();
|
|
43818
|
+
cEntity[TEXTURE_VALUE_LABELS_KEY] = null;
|
|
43819
|
+
}
|
|
43820
|
+
if (!settings || !extent || !animator) {
|
|
43821
|
+
return;
|
|
43822
|
+
}
|
|
43823
|
+
const labels = new exports.TextureValueLabels.Labels({
|
|
43824
|
+
viewer, extent, settings, metadata,
|
|
43825
|
+
tiles: exports.TextureFrameSeriesAnimator.TilesOf(metadata).map((tile) => ({
|
|
43826
|
+
West: tile.West, East: tile.East, South: tile.South, North: tile.North,
|
|
43827
|
+
ResolutionX: tile.ResolutionX, ResolutionY: tile.ResolutionY
|
|
43828
|
+
})),
|
|
43829
|
+
clipRing: params.clipRing
|
|
43830
|
+
});
|
|
43831
|
+
let lastVersion = -1;
|
|
43832
|
+
let appliedFloor = null;
|
|
43833
|
+
let appliedCeiling = null;
|
|
43834
|
+
// The extremes are what turn an anomaly frame back into a reading, so they are worth two extra
|
|
43835
|
+
// Range GETs here even though nothing else in the render path wants them.
|
|
43836
|
+
animator.EnsureExtremes();
|
|
43837
|
+
const remove = viewer.scene.preRender.addEventListener(() => {
|
|
43838
|
+
if (animator.IsDisposed()) {
|
|
43839
|
+
return;
|
|
43840
|
+
}
|
|
43841
|
+
const canvas = animator.GetValueCanvas();
|
|
43842
|
+
if (!canvas) {
|
|
43843
|
+
return;
|
|
43844
|
+
}
|
|
43845
|
+
const version = animator.GetPresentVersion();
|
|
43846
|
+
if (version !== lastVersion) {
|
|
43847
|
+
lastVersion = version;
|
|
43848
|
+
const ctx = canvas.getContext("2d");
|
|
43849
|
+
labels.SetSource(ctx.getImageData(0, 0, canvas.width, canvas.height));
|
|
43850
|
+
labels.SetPainted(animator.GetDisplayedPixels());
|
|
43851
|
+
}
|
|
43852
|
+
// Tracked by reference rather than latched on the first one to arrive: a tiled archive
|
|
43853
|
+
// composites the floor and the ceiling separately, so latching once would leave every label
|
|
43854
|
+
// showing a minimum and no maximum.
|
|
43855
|
+
const extremes = animator.GetExtremes();
|
|
43856
|
+
if (extremes.floor !== appliedFloor || extremes.ceiling !== appliedCeiling) {
|
|
43857
|
+
appliedFloor = extremes.floor;
|
|
43858
|
+
appliedCeiling = extremes.ceiling;
|
|
43859
|
+
labels.SetExtremes(appliedFloor, appliedCeiling);
|
|
43860
|
+
}
|
|
43861
|
+
labels.Render();
|
|
43862
|
+
});
|
|
43863
|
+
cEntity[TEXTURE_VALUE_LABELS_KEY] = {
|
|
43864
|
+
labels,
|
|
43865
|
+
remove: () => {
|
|
43866
|
+
remove();
|
|
43867
|
+
labels.Dispose();
|
|
43868
|
+
}
|
|
43869
|
+
};
|
|
43870
|
+
}
|
|
42230
43871
|
/**
|
|
42231
43872
|
* Derives a min/max/label time-range segment from a resolved frame archive's Frames metadata,
|
|
42232
43873
|
* for reporting up to whoever owns the menu item this entity belongs to (see IParams.onSeriesDiscovered).
|
|
@@ -42317,11 +43958,15 @@ void main() {
|
|
|
42317
43958
|
const { clientFile } = await BModels.ClientFile.Get({ api, fileId: clientFileId });
|
|
42318
43959
|
url = clientFile.URL;
|
|
42319
43960
|
const clientFileData = clientFile.Data;
|
|
42320
|
-
if (TextureFrameSeriesAnimator.IsFrameArchiveMetadata(clientFileData)) {
|
|
43961
|
+
if (exports.TextureFrameSeriesAnimator.IsFrameArchiveMetadata(clientFileData)) {
|
|
43962
|
+
const adapted = exports.TextureFrameSeriesAnimator.Adapt(clientFileData.generation);
|
|
43963
|
+
if (!adapted) {
|
|
43964
|
+
return { dataUri: null, effective: textureTrace.effective };
|
|
43965
|
+
}
|
|
42321
43966
|
return {
|
|
42322
43967
|
dataUri: null,
|
|
42323
43968
|
effective: textureTrace.effective,
|
|
42324
|
-
frameArchive: { url, metadata:
|
|
43969
|
+
frameArchive: { url, metadata: adapted }
|
|
42325
43970
|
};
|
|
42326
43971
|
}
|
|
42327
43972
|
}
|
|
@@ -42349,8 +43994,12 @@ void main() {
|
|
|
42349
43994
|
}
|
|
42350
43995
|
const url = clientFile.URL;
|
|
42351
43996
|
const clientFileData = clientFile.Data;
|
|
42352
|
-
if (TextureFrameSeriesAnimator.IsFrameArchiveMetadata(clientFileData)) {
|
|
42353
|
-
|
|
43997
|
+
if (exports.TextureFrameSeriesAnimator.IsFrameArchiveMetadata(clientFileData)) {
|
|
43998
|
+
const adapted = exports.TextureFrameSeriesAnimator.Adapt(clientFileData.generation);
|
|
43999
|
+
if (adapted) {
|
|
44000
|
+
return { dataUri: null, effective: null, frameArchive: { url, metadata: adapted } };
|
|
44001
|
+
}
|
|
44002
|
+
return { dataUri: null, effective: null };
|
|
42354
44003
|
}
|
|
42355
44004
|
const cacheKey = "texture-" + url + "-none";
|
|
42356
44005
|
let prom = _textureCache.Get(cacheKey);
|
|
@@ -43819,7 +45468,7 @@ void main() {
|
|
|
43819
45468
|
StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
|
|
43820
45469
|
})(exports.StyleUtils || (exports.StyleUtils = {}));
|
|
43821
45470
|
|
|
43822
|
-
const VERSION = "7.2.
|
|
45471
|
+
const VERSION = "7.2.3";
|
|
43823
45472
|
/**
|
|
43824
45473
|
* Updates the environment instance used by bruce-cesium to one specified.
|
|
43825
45474
|
* This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.
|