bruce-cesium 7.2.0 → 7.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bruce-cesium.es5.js +2237 -336
- package/dist/bruce-cesium.es5.js.map +1 -1
- package/dist/bruce-cesium.umd.js +2230 -333
- package/dist/bruce-cesium.umd.js.map +1 -1
- package/dist/lib/bruce-cesium.js +2 -1
- package/dist/lib/bruce-cesium.js.map +1 -1
- package/dist/lib/internal/image-utils.js +106 -1
- package/dist/lib/internal/image-utils.js.map +1 -1
- package/dist/lib/rendering/displaced-surface-primitive.js +41 -8
- package/dist/lib/rendering/displaced-surface-primitive.js.map +1 -1
- package/dist/lib/rendering/entity-render-engine-polygon.js +211 -13
- package/dist/lib/rendering/entity-render-engine-polygon.js.map +1 -1
- package/dist/lib/rendering/render-managers/common/historic-utils.js +471 -0
- package/dist/lib/rendering/render-managers/common/historic-utils.js.map +1 -0
- package/dist/lib/rendering/render-managers/entities/entities-ids-render-manager.js +14 -124
- package/dist/lib/rendering/render-managers/entities/entities-ids-render-manager.js.map +1 -1
- package/dist/lib/rendering/render-managers/entities/entities-render-manager.js +16 -13
- package/dist/lib/rendering/render-managers/entities/entities-render-manager.js.map +1 -1
- package/dist/lib/rendering/render-managers/tilesets/tileset-cad-render-manager.js +31 -109
- package/dist/lib/rendering/render-managers/tilesets/tileset-cad-render-manager.js.map +1 -1
- package/dist/lib/rendering/render-managers/tilesets/tileset-entities-render-manager.js +23 -7
- package/dist/lib/rendering/render-managers/tilesets/tileset-entities-render-manager.js.map +1 -1
- package/dist/lib/rendering/texture-frame-series-animator.js +621 -28
- package/dist/lib/rendering/texture-frame-series-animator.js.map +1 -1
- package/dist/lib/rendering/texture-value-labels.js +687 -0
- package/dist/lib/rendering/texture-value-labels.js.map +1 -0
- package/dist/lib/rendering/tileset-styler.js +28 -24
- package/dist/lib/rendering/tileset-styler.js.map +1 -1
- package/dist/lib/rendering/visuals-register.js +8 -3
- package/dist/lib/rendering/visuals-register.js.map +1 -1
- package/dist/types/bruce-cesium.d.ts +2 -1
- package/dist/types/internal/image-utils.d.ts +25 -0
- package/dist/types/rendering/displaced-surface-primitive.d.ts +5 -2
- package/dist/types/rendering/render-managers/common/historic-utils.d.ts +159 -0
- package/dist/types/rendering/render-managers/entities/entities-ids-render-manager.d.ts +1 -0
- package/dist/types/rendering/render-managers/entities/entities-render-manager.d.ts +5 -0
- package/dist/types/rendering/render-managers/tilesets/tileset-cad-render-manager.d.ts +1 -0
- package/dist/types/rendering/render-managers/tilesets/tileset-entities-render-manager.d.ts +2 -0
- package/dist/types/rendering/texture-frame-series-animator.d.ts +172 -1
- package/dist/types/rendering/texture-value-labels.d.ts +139 -0
- package/dist/types/rendering/tileset-styler.d.ts +2 -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;
|
|
@@ -10700,6 +10705,468 @@
|
|
|
10700
10705
|
}
|
|
10701
10706
|
}
|
|
10702
10707
|
|
|
10708
|
+
/**
|
|
10709
|
+
* How often a historic refresh is worth running, and which Entities it should ask about first.
|
|
10710
|
+
*
|
|
10711
|
+
* A handful of tracked vehicles can afford to re-read on almost every clock tick. Thousands of
|
|
10712
|
+
* parcels cannot, and refreshing them that often was never buying anything since the answer barely
|
|
10713
|
+
* changes between ticks.
|
|
10714
|
+
*/
|
|
10715
|
+
var HistoricFetchOrder;
|
|
10716
|
+
(function (HistoricFetchOrder) {
|
|
10717
|
+
// The cadence for a set small enough that latency is what the user notices.
|
|
10718
|
+
HistoricFetchOrder.MIN_INTERVAL_MS = 250;
|
|
10719
|
+
// The cadence for a set large enough that the request itself is what the user notices.
|
|
10720
|
+
HistoricFetchOrder.MAX_INTERVAL_MS = 10000;
|
|
10721
|
+
// Below this many Entities the cadence stays at its fastest.
|
|
10722
|
+
HistoricFetchOrder.SMALL_SET = 30;
|
|
10723
|
+
// At and above this many it stays at its slowest.
|
|
10724
|
+
HistoricFetchOrder.LARGE_SET = 5000;
|
|
10725
|
+
/**
|
|
10726
|
+
* Returns how long to wait between historic refreshes for a set of this size.
|
|
10727
|
+
*/
|
|
10728
|
+
function IntervalFor(entityCount, minMs = HistoricFetchOrder.MIN_INTERVAL_MS, maxMs = HistoricFetchOrder.MAX_INTERVAL_MS) {
|
|
10729
|
+
if (!(entityCount > HistoricFetchOrder.SMALL_SET)) {
|
|
10730
|
+
return minMs;
|
|
10731
|
+
}
|
|
10732
|
+
if (entityCount >= HistoricFetchOrder.LARGE_SET) {
|
|
10733
|
+
return maxMs;
|
|
10734
|
+
}
|
|
10735
|
+
// Log scaled, so going from 30 to 300 Entities costs more than going from 3000 to 5000.
|
|
10736
|
+
const progress = Math.log(entityCount / HistoricFetchOrder.SMALL_SET) / Math.log(HistoricFetchOrder.LARGE_SET / HistoricFetchOrder.SMALL_SET);
|
|
10737
|
+
return Math.round(minMs + ((maxMs - minMs) * progress));
|
|
10738
|
+
}
|
|
10739
|
+
HistoricFetchOrder.IntervalFor = IntervalFor;
|
|
10740
|
+
/**
|
|
10741
|
+
* Returns the Entities grouped into bands by distance from the camera, nearest band first.
|
|
10742
|
+
*
|
|
10743
|
+
* Bands rather than a true sort, so a small camera move does not reshuffle the whole queue and
|
|
10744
|
+
* restart it. What the user is looking at resolves first either way.
|
|
10745
|
+
*/
|
|
10746
|
+
function ByCameraDistance(params) {
|
|
10747
|
+
var _a;
|
|
10748
|
+
const { viewer, entities } = params;
|
|
10749
|
+
const bands = params.bands || [500, 2000, 10000, 50000];
|
|
10750
|
+
if (!(entities === null || entities === void 0 ? void 0 : entities.length)) {
|
|
10751
|
+
return [];
|
|
10752
|
+
}
|
|
10753
|
+
const cameraPos = (_a = viewer === null || viewer === void 0 ? void 0 : viewer.camera) === null || _a === void 0 ? void 0 : _a.position;
|
|
10754
|
+
if (!cameraPos) {
|
|
10755
|
+
return [entities];
|
|
10756
|
+
}
|
|
10757
|
+
// One band past the named bounds, for everything beyond the last of them.
|
|
10758
|
+
const banded = bands.map(() => []).concat([[]]);
|
|
10759
|
+
for (const entity of entities) {
|
|
10760
|
+
const pos = SafePos(entity, viewer);
|
|
10761
|
+
if (!pos) {
|
|
10762
|
+
banded[banded.length - 1].push(entity);
|
|
10763
|
+
continue;
|
|
10764
|
+
}
|
|
10765
|
+
const distance = Cesium.Cartesian3.distance(cameraPos, pos);
|
|
10766
|
+
let index = bands.findIndex(bound => distance <= bound);
|
|
10767
|
+
if (index < 0) {
|
|
10768
|
+
index = banded.length - 1;
|
|
10769
|
+
}
|
|
10770
|
+
banded[index].push(entity);
|
|
10771
|
+
}
|
|
10772
|
+
return banded.filter(band => band.length > 0);
|
|
10773
|
+
}
|
|
10774
|
+
HistoricFetchOrder.ByCameraDistance = ByCameraDistance;
|
|
10775
|
+
/*
|
|
10776
|
+
* Returns an Entity's world position, or null when it has none to place it by.
|
|
10777
|
+
*/
|
|
10778
|
+
function SafePos(entity, viewer) {
|
|
10779
|
+
try {
|
|
10780
|
+
const pos = exports.EntityUtils.GetPos({
|
|
10781
|
+
entity: entity,
|
|
10782
|
+
viewer: viewer,
|
|
10783
|
+
allowRendered: true
|
|
10784
|
+
});
|
|
10785
|
+
if (!pos || isNaN(pos.x) || isNaN(pos.y) || isNaN(pos.z)) {
|
|
10786
|
+
return null;
|
|
10787
|
+
}
|
|
10788
|
+
return pos;
|
|
10789
|
+
}
|
|
10790
|
+
catch {
|
|
10791
|
+
return null;
|
|
10792
|
+
}
|
|
10793
|
+
}
|
|
10794
|
+
})(HistoricFetchOrder || (HistoricFetchOrder = {}));
|
|
10795
|
+
/**
|
|
10796
|
+
* Decides what slice of history is worth fetching for the clock as it is currently moving.
|
|
10797
|
+
*/
|
|
10798
|
+
var HistoricFetchPlan;
|
|
10799
|
+
(function (HistoricFetchPlan) {
|
|
10800
|
+
// How many seconds of playback the detail window tries to stay ahead by.
|
|
10801
|
+
HistoricFetchPlan.DEFAULT_LEAD_SECONDS = 10;
|
|
10802
|
+
// The detail window never shrinks below this, so a paused clock still has something around it.
|
|
10803
|
+
HistoricFetchPlan.DEFAULT_MIN_DETAIL_MS = 60 * 1000;
|
|
10804
|
+
// Nor grows past it, which is what stops a fast scrub asking for the whole archive.
|
|
10805
|
+
HistoricFetchPlan.DEFAULT_MAX_DETAIL_MS = 6 * 60 * 60 * 1000;
|
|
10806
|
+
// Records the coarse pass may spend describing the whole range.
|
|
10807
|
+
HistoricFetchPlan.DEFAULT_SAMPLE = 200;
|
|
10808
|
+
/*
|
|
10809
|
+
* Returns how wide the detail window should be for the rate the clock is moving at.
|
|
10810
|
+
*/
|
|
10811
|
+
function DetailSpanFor(trajectory, leadSeconds, minMs, maxMs) {
|
|
10812
|
+
const rate = Math.abs((trajectory === null || trajectory === void 0 ? void 0 : trajectory.ratePerSecondMs) || 0);
|
|
10813
|
+
const span = rate * leadSeconds;
|
|
10814
|
+
if (!(span > minMs)) {
|
|
10815
|
+
return minMs;
|
|
10816
|
+
}
|
|
10817
|
+
return Math.min(span, maxMs);
|
|
10818
|
+
}
|
|
10819
|
+
HistoricFetchPlan.DetailSpanFor = DetailSpanFor;
|
|
10820
|
+
/**
|
|
10821
|
+
* Returns the detail window and the coarse pass for the clock as it stands.
|
|
10822
|
+
*/
|
|
10823
|
+
function Plan(params) {
|
|
10824
|
+
var _a;
|
|
10825
|
+
const { currentTime, windowStart, windowStop, trajectory, leadSeconds = HistoricFetchPlan.DEFAULT_LEAD_SECONDS, minDetailMs = HistoricFetchPlan.DEFAULT_MIN_DETAIL_MS, maxDetailMs = HistoricFetchPlan.DEFAULT_MAX_DETAIL_MS, sample = HistoricFetchPlan.DEFAULT_SAMPLE } = params;
|
|
10826
|
+
if (!(windowStop > windowStart)) {
|
|
10827
|
+
return { detail: null, coarse: null };
|
|
10828
|
+
}
|
|
10829
|
+
// A range that fits the detail budget has nothing worth sampling: reading it whole is both
|
|
10830
|
+
// cheaper than two requests and better than an approximation of it.
|
|
10831
|
+
if ((windowStop - windowStart) <= maxDetailMs) {
|
|
10832
|
+
return { detail: { start: windowStart, stop: windowStop }, coarse: null };
|
|
10833
|
+
}
|
|
10834
|
+
const span = DetailSpanFor(trajectory, leadSeconds, minDetailMs, maxDetailMs);
|
|
10835
|
+
const direction = (_a = trajectory === null || trajectory === void 0 ? void 0 : trajectory.direction) !== null && _a !== void 0 ? _a : 0;
|
|
10836
|
+
// Time running forward only ever reveals what is ahead, so the window leads rather than centres.
|
|
10837
|
+
// A little is still kept behind so a record just passed can still be interpolated from.
|
|
10838
|
+
const behind = direction === 0 ? span / 2 : span * 0.2;
|
|
10839
|
+
const ahead = direction === 0 ? span / 2 : span * 0.8;
|
|
10840
|
+
let start = direction < 0 ? currentTime - ahead : currentTime - behind;
|
|
10841
|
+
let stop = direction < 0 ? currentTime + behind : currentTime + ahead;
|
|
10842
|
+
start = Math.max(windowStart, start);
|
|
10843
|
+
stop = Math.min(windowStop, stop);
|
|
10844
|
+
if (!(stop > start)) {
|
|
10845
|
+
return { detail: null, coarse: { start: windowStart, stop: windowStop, sample } };
|
|
10846
|
+
}
|
|
10847
|
+
const detail = { start, stop };
|
|
10848
|
+
// Nothing to be coarse about once the detail window is the whole range.
|
|
10849
|
+
if (start <= windowStart && stop >= windowStop) {
|
|
10850
|
+
return { detail, coarse: null };
|
|
10851
|
+
}
|
|
10852
|
+
return { detail, coarse: { start: windowStart, stop: windowStop, sample } };
|
|
10853
|
+
}
|
|
10854
|
+
HistoricFetchPlan.Plan = Plan;
|
|
10855
|
+
/**
|
|
10856
|
+
* Tracks which way the clock is moving and how fast, from the times it is observed at.
|
|
10857
|
+
* Playback and a user dragging the timeline both look the same here, which is the point.
|
|
10858
|
+
*/
|
|
10859
|
+
class TrajectoryTracker {
|
|
10860
|
+
// Smooths the rate so one slow frame does not collapse the window.
|
|
10861
|
+
constructor(smoothing = 0.4) {
|
|
10862
|
+
this.smoothing = smoothing;
|
|
10863
|
+
this.lastTime = null;
|
|
10864
|
+
this.lastObservedAt = null;
|
|
10865
|
+
this.direction = 0;
|
|
10866
|
+
this.ratePerSecondMs = 0;
|
|
10867
|
+
}
|
|
10868
|
+
/**
|
|
10869
|
+
* Records where the clock is now. observedAt is the wall clock, in milliseconds.
|
|
10870
|
+
*/
|
|
10871
|
+
Observe(sceneTime, observedAt) {
|
|
10872
|
+
if (this.lastTime == null) {
|
|
10873
|
+
this.lastTime = sceneTime;
|
|
10874
|
+
this.lastObservedAt = observedAt;
|
|
10875
|
+
return;
|
|
10876
|
+
}
|
|
10877
|
+
const elapsed = observedAt - this.lastObservedAt;
|
|
10878
|
+
const moved = sceneTime - this.lastTime;
|
|
10879
|
+
this.lastTime = sceneTime;
|
|
10880
|
+
this.lastObservedAt = observedAt;
|
|
10881
|
+
if (!(elapsed > 0)) {
|
|
10882
|
+
return;
|
|
10883
|
+
}
|
|
10884
|
+
if (moved === 0) {
|
|
10885
|
+
this.direction = 0;
|
|
10886
|
+
this.ratePerSecondMs = 0;
|
|
10887
|
+
return;
|
|
10888
|
+
}
|
|
10889
|
+
this.direction = moved > 0 ? 1 : -1;
|
|
10890
|
+
const rate = Math.abs(moved) / (elapsed / 1000);
|
|
10891
|
+
this.ratePerSecondMs = this.ratePerSecondMs === 0
|
|
10892
|
+
? rate
|
|
10893
|
+
: (this.ratePerSecondMs * (1 - this.smoothing)) + (rate * this.smoothing);
|
|
10894
|
+
}
|
|
10895
|
+
Get() {
|
|
10896
|
+
return { direction: this.direction, ratePerSecondMs: this.ratePerSecondMs };
|
|
10897
|
+
}
|
|
10898
|
+
Reset() {
|
|
10899
|
+
this.lastTime = null;
|
|
10900
|
+
this.lastObservedAt = null;
|
|
10901
|
+
this.direction = 0;
|
|
10902
|
+
this.ratePerSecondMs = 0;
|
|
10903
|
+
}
|
|
10904
|
+
}
|
|
10905
|
+
HistoricFetchPlan.TrajectoryTracker = TrajectoryTracker;
|
|
10906
|
+
})(HistoricFetchPlan || (HistoricFetchPlan = {}));
|
|
10907
|
+
/**
|
|
10908
|
+
* Tracks which time ranges of historic data have already been requested, and the records they
|
|
10909
|
+
* returned, so moving the clock only fetches the part that is genuinely new.
|
|
10910
|
+
*
|
|
10911
|
+
* Coverage is tracked by the ranges that were asked for, not by the timestamps that came back. A
|
|
10912
|
+
* window holding no records is still covered, otherwise an empty stretch is re-requested forever.
|
|
10913
|
+
*/
|
|
10914
|
+
class HistoricRangeCache {
|
|
10915
|
+
// Ranges this far apart are treated as one, so a scrub does not leave a trail of slivers.
|
|
10916
|
+
constructor(mergeToleranceMs = 60000) {
|
|
10917
|
+
this.mergeToleranceMs = mergeToleranceMs;
|
|
10918
|
+
// Requested ranges, kept sorted and non-overlapping.
|
|
10919
|
+
this.held = [];
|
|
10920
|
+
// Ranges covered only by a sampled read, which describe their span without holding every record.
|
|
10921
|
+
this.heldCoarse = [];
|
|
10922
|
+
this.records = {};
|
|
10923
|
+
}
|
|
10924
|
+
/**
|
|
10925
|
+
* Returns the parts of the window that have not been requested yet.
|
|
10926
|
+
*/
|
|
10927
|
+
MissingRanges(start, stop) {
|
|
10928
|
+
if (!(stop > start)) {
|
|
10929
|
+
return [];
|
|
10930
|
+
}
|
|
10931
|
+
const missing = [];
|
|
10932
|
+
let cursor = start;
|
|
10933
|
+
for (const range of this.held) {
|
|
10934
|
+
if (range.stop <= cursor) {
|
|
10935
|
+
continue;
|
|
10936
|
+
}
|
|
10937
|
+
if (range.start >= stop) {
|
|
10938
|
+
break;
|
|
10939
|
+
}
|
|
10940
|
+
if (range.start > cursor) {
|
|
10941
|
+
missing.push({ start: cursor, stop: Math.min(range.start, stop) });
|
|
10942
|
+
}
|
|
10943
|
+
cursor = Math.max(cursor, range.stop);
|
|
10944
|
+
if (cursor >= stop) {
|
|
10945
|
+
break;
|
|
10946
|
+
}
|
|
10947
|
+
}
|
|
10948
|
+
if (cursor < stop) {
|
|
10949
|
+
missing.push({ start: cursor, stop: stop });
|
|
10950
|
+
}
|
|
10951
|
+
return this.mergeAdjacent(missing);
|
|
10952
|
+
}
|
|
10953
|
+
/**
|
|
10954
|
+
* Marks a range as requested and merges the records it returned into what is already held.
|
|
10955
|
+
*/
|
|
10956
|
+
Absorb(start, stop, recordsByIds, coarse = false) {
|
|
10957
|
+
if (stop > start) {
|
|
10958
|
+
if (coarse) {
|
|
10959
|
+
this.heldCoarse = this.mergeAdjacent([...this.heldCoarse, { start, stop }].sort((a, b) => a.start - b.start));
|
|
10960
|
+
}
|
|
10961
|
+
else {
|
|
10962
|
+
this.held = this.mergeAdjacent([...this.held, { start, stop }].sort((a, b) => a.start - b.start));
|
|
10963
|
+
}
|
|
10964
|
+
}
|
|
10965
|
+
if (!recordsByIds) {
|
|
10966
|
+
return;
|
|
10967
|
+
}
|
|
10968
|
+
for (const entityId of Object.keys(recordsByIds)) {
|
|
10969
|
+
const incoming = recordsByIds[entityId] || [];
|
|
10970
|
+
const current = this.records[entityId] || [];
|
|
10971
|
+
// Keyed by instant so a range fetched twice cannot double up its records.
|
|
10972
|
+
const byInstant = new Map();
|
|
10973
|
+
for (const record of current) {
|
|
10974
|
+
byInstant.set(new Date(record.dateTime).getTime(), record);
|
|
10975
|
+
}
|
|
10976
|
+
for (const record of incoming) {
|
|
10977
|
+
byInstant.set(new Date(record.dateTime).getTime(), record);
|
|
10978
|
+
}
|
|
10979
|
+
this.records[entityId] = Array.from(byInstant.values())
|
|
10980
|
+
.sort((a, b) => new Date(a.dateTime).getTime() - new Date(b.dateTime).getTime());
|
|
10981
|
+
}
|
|
10982
|
+
}
|
|
10983
|
+
GetRecords() {
|
|
10984
|
+
return this.records;
|
|
10985
|
+
}
|
|
10986
|
+
/**
|
|
10987
|
+
* Returns whether the window is already covered in full.
|
|
10988
|
+
*/
|
|
10989
|
+
Covers(start, stop) {
|
|
10990
|
+
return this.MissingRanges(start, stop).length === 0;
|
|
10991
|
+
}
|
|
10992
|
+
/**
|
|
10993
|
+
* Returns whether a sampled pass already describes the window.
|
|
10994
|
+
*/
|
|
10995
|
+
CoversCoarse(start, stop) {
|
|
10996
|
+
if (!(stop > start)) {
|
|
10997
|
+
return true;
|
|
10998
|
+
}
|
|
10999
|
+
return this.heldCoarse.some(range => range.start <= start && range.stop >= stop);
|
|
11000
|
+
}
|
|
11001
|
+
Clear() {
|
|
11002
|
+
this.held = [];
|
|
11003
|
+
this.heldCoarse = [];
|
|
11004
|
+
this.records = {};
|
|
11005
|
+
}
|
|
11006
|
+
mergeAdjacent(ranges) {
|
|
11007
|
+
if (ranges.length <= 1) {
|
|
11008
|
+
return ranges;
|
|
11009
|
+
}
|
|
11010
|
+
const sorted = [...ranges].sort((a, b) => a.start - b.start);
|
|
11011
|
+
const merged = [];
|
|
11012
|
+
let current = { ...sorted[0] };
|
|
11013
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
11014
|
+
const next = sorted[i];
|
|
11015
|
+
if (next.start - current.stop <= this.mergeToleranceMs) {
|
|
11016
|
+
current.stop = Math.max(current.stop, next.stop);
|
|
11017
|
+
}
|
|
11018
|
+
else {
|
|
11019
|
+
merged.push(current);
|
|
11020
|
+
current = { ...next };
|
|
11021
|
+
}
|
|
11022
|
+
}
|
|
11023
|
+
merged.push(current);
|
|
11024
|
+
return merged;
|
|
11025
|
+
}
|
|
11026
|
+
}
|
|
11027
|
+
/**
|
|
11028
|
+
* Pulls historic records for the visible clock range: what to ask for, in what order, and what has
|
|
11029
|
+
* already been asked for.
|
|
11030
|
+
*/
|
|
11031
|
+
class HistoricGatherer {
|
|
11032
|
+
constructor(params) {
|
|
11033
|
+
this.params = params;
|
|
11034
|
+
this.trajectory = new HistoricFetchPlan.TrajectoryTracker();
|
|
11035
|
+
// Range key to the request in flight for it, so the same range is never asked for twice at once.
|
|
11036
|
+
this.inFlight = new Map();
|
|
11037
|
+
this.disposed = false;
|
|
11038
|
+
this.cache = new HistoricRangeCache();
|
|
11039
|
+
}
|
|
11040
|
+
get Records() {
|
|
11041
|
+
return this.cache.GetRecords();
|
|
11042
|
+
}
|
|
11043
|
+
Clear() {
|
|
11044
|
+
this.cache.Clear();
|
|
11045
|
+
this.trajectory.Reset();
|
|
11046
|
+
this.inFlight.clear();
|
|
11047
|
+
}
|
|
11048
|
+
Dispose() {
|
|
11049
|
+
this.disposed = true;
|
|
11050
|
+
this.Clear();
|
|
11051
|
+
}
|
|
11052
|
+
/**
|
|
11053
|
+
* Returns how long to wait between refreshes for a set of this size.
|
|
11054
|
+
*/
|
|
11055
|
+
IntervalFor(entityCount) {
|
|
11056
|
+
return HistoricFetchOrder.IntervalFor(entityCount);
|
|
11057
|
+
}
|
|
11058
|
+
/**
|
|
11059
|
+
* Fetches whatever the current clock range needs that is not already held.
|
|
11060
|
+
* Returns whether anything was requested, alongside every record held for these Entities.
|
|
11061
|
+
*/
|
|
11062
|
+
async Gather(entities) {
|
|
11063
|
+
var _a;
|
|
11064
|
+
if (this.disposed || !(entities === null || entities === void 0 ? void 0 : entities.length)) {
|
|
11065
|
+
return { fetched: false, recordsByIds: this.cache.GetRecords() };
|
|
11066
|
+
}
|
|
11067
|
+
const { viewer } = this.params;
|
|
11068
|
+
const windowStart = new Date(viewer.clock.startTime.toString()).getTime();
|
|
11069
|
+
const windowStop = new Date(viewer.clock.stopTime.toString()).getTime();
|
|
11070
|
+
const currentTime = new Date(viewer.clock.currentTime.toString()).getTime();
|
|
11071
|
+
this.trajectory.Observe(currentTime, Date.now());
|
|
11072
|
+
const liveEdge = exports.ViewUtils.GetTimeDetails({ viewer }).isLive ? currentTime : null;
|
|
11073
|
+
const liveTail = (_a = this.params.liveTailMs) !== null && _a !== void 0 ? _a : HistoricGatherer.LIVE_TAIL_MS;
|
|
11074
|
+
const plan = HistoricFetchPlan.Plan({
|
|
11075
|
+
currentTime,
|
|
11076
|
+
windowStart,
|
|
11077
|
+
windowStop,
|
|
11078
|
+
trajectory: this.trajectory.Get(),
|
|
11079
|
+
sample: this.params.sample
|
|
11080
|
+
});
|
|
11081
|
+
let fetched = false;
|
|
11082
|
+
if (plan.coarse && !this.cache.CoversCoarse(plan.coarse.start, plan.coarse.stop)) {
|
|
11083
|
+
const records = await this.request(plan.coarse.start, plan.coarse.stop, entities, plan.coarse.sample);
|
|
11084
|
+
if (this.disposed) {
|
|
11085
|
+
return { fetched, recordsByIds: this.cache.GetRecords() };
|
|
11086
|
+
}
|
|
11087
|
+
this.cache.Absorb(plan.coarse.start, this.holdTo(plan.coarse.stop, liveEdge, liveTail), records, true);
|
|
11088
|
+
fetched = true;
|
|
11089
|
+
}
|
|
11090
|
+
if (plan.detail) {
|
|
11091
|
+
const bands = entities.length > HistoricFetchOrder.SMALL_SET
|
|
11092
|
+
? HistoricFetchOrder.ByCameraDistance({ viewer, entities })
|
|
11093
|
+
: [entities];
|
|
11094
|
+
for (const range of this.cache.MissingRanges(plan.detail.start, plan.detail.stop)) {
|
|
11095
|
+
if (this.disposed) {
|
|
11096
|
+
break;
|
|
11097
|
+
}
|
|
11098
|
+
for (const band of bands) {
|
|
11099
|
+
if (this.disposed) {
|
|
11100
|
+
break;
|
|
11101
|
+
}
|
|
11102
|
+
const records = await this.request(range.start, range.stop, band, 0);
|
|
11103
|
+
if (this.disposed) {
|
|
11104
|
+
break;
|
|
11105
|
+
}
|
|
11106
|
+
// Records land as each band arrives, while the range is only marked held once
|
|
11107
|
+
// every band has been asked for.
|
|
11108
|
+
this.cache.Absorb(0, 0, records);
|
|
11109
|
+
fetched = true;
|
|
11110
|
+
}
|
|
11111
|
+
if (this.disposed) {
|
|
11112
|
+
break;
|
|
11113
|
+
}
|
|
11114
|
+
const heldTo = this.holdTo(range.stop, liveEdge, liveTail);
|
|
11115
|
+
if (heldTo > range.start) {
|
|
11116
|
+
this.cache.Absorb(range.start, heldTo, {});
|
|
11117
|
+
}
|
|
11118
|
+
}
|
|
11119
|
+
}
|
|
11120
|
+
return { fetched, recordsByIds: this.cache.GetRecords() };
|
|
11121
|
+
}
|
|
11122
|
+
/*
|
|
11123
|
+
* Returns how much of a range that was just read may be marked as held.
|
|
11124
|
+
*/
|
|
11125
|
+
holdTo(stop, liveEdge, liveTailMs) {
|
|
11126
|
+
if (liveEdge == null) {
|
|
11127
|
+
return stop;
|
|
11128
|
+
}
|
|
11129
|
+
return Math.min(stop, liveEdge - liveTailMs);
|
|
11130
|
+
}
|
|
11131
|
+
/*
|
|
11132
|
+
* Reads one range, joining a request already in flight for it rather than repeating it.
|
|
11133
|
+
*/
|
|
11134
|
+
request(start, stop, entities, sample) {
|
|
11135
|
+
var _a;
|
|
11136
|
+
const padding = (_a = this.params.paddingMs) !== null && _a !== void 0 ? _a : 0;
|
|
11137
|
+
const dateTimeFrom = new Date(start - padding).toISOString();
|
|
11138
|
+
const dateTimeTo = new Date(stop + padding).toISOString();
|
|
11139
|
+
const entityIds = entities.map(x => { var _a; return (_a = x.Bruce) === null || _a === void 0 ? void 0 : _a.ID; }).filter(x => !!x);
|
|
11140
|
+
const key = `${dateTimeFrom}|${dateTimeTo}|${sample}|${entityIds.join(",")}`;
|
|
11141
|
+
const existing = this.inFlight.get(key);
|
|
11142
|
+
if (existing) {
|
|
11143
|
+
return existing;
|
|
11144
|
+
}
|
|
11145
|
+
const prom = BModels.EntityHistoricData.GetRecordsByEntity({
|
|
11146
|
+
attrKey: this.params.getAttrKey(),
|
|
11147
|
+
dateTimeFrom,
|
|
11148
|
+
dateTimeTo,
|
|
11149
|
+
entityIds,
|
|
11150
|
+
sample: sample > 0 ? sample : undefined,
|
|
11151
|
+
api: this.params.getApi()
|
|
11152
|
+
}).then((res) => {
|
|
11153
|
+
const records = res.recordsByIds || {};
|
|
11154
|
+
if (!this.disposed && this.params.onRecords) {
|
|
11155
|
+
this.params.onRecords(records);
|
|
11156
|
+
}
|
|
11157
|
+
return records;
|
|
11158
|
+
}).finally(() => {
|
|
11159
|
+
this.inFlight.delete(key);
|
|
11160
|
+
});
|
|
11161
|
+
this.inFlight.set(key, prom);
|
|
11162
|
+
return prom;
|
|
11163
|
+
}
|
|
11164
|
+
}
|
|
11165
|
+
// How far back from the live edge is left unheld, so a record written moments ago still falls in
|
|
11166
|
+
// something that gets asked for again. It has to outlast a write, a commit and a refresh, plus
|
|
11167
|
+
// whatever the writer's clock and this one disagree by.
|
|
11168
|
+
HistoricGatherer.LIVE_TAIL_MS = 15000;
|
|
11169
|
+
|
|
10703
11170
|
const BATCH_SIZE = 500;
|
|
10704
11171
|
const CHECK_BATCH_SIZE = 250;
|
|
10705
11172
|
function getValue$2(viewer, obj) {
|
|
@@ -10784,6 +11251,13 @@
|
|
|
10784
11251
|
this.item = item;
|
|
10785
11252
|
this.visualsManager = visualsManager;
|
|
10786
11253
|
this.onSeriesDiscovered = onSeriesDiscovered;
|
|
11254
|
+
this.historicGatherer = new HistoricGatherer({
|
|
11255
|
+
viewer: this.viewer,
|
|
11256
|
+
getApi: () => this.apiGetter.getApi(),
|
|
11257
|
+
getAttrKey: () => { var _a; return (_a = this.item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historicAttrKey; },
|
|
11258
|
+
// A second either side, to take in records sitting exactly on the boundary.
|
|
11259
|
+
paddingMs: 1000
|
|
11260
|
+
});
|
|
10787
11261
|
this.useGeojson = item.renderAsGeojson == true;
|
|
10788
11262
|
if (item.enableClustering) {
|
|
10789
11263
|
this.clustering = new PointClustering(this.visualsManager, this.item.id);
|
|
@@ -11747,6 +12221,13 @@
|
|
|
11747
12221
|
this.renderAsIndividualsChain = next;
|
|
11748
12222
|
return next;
|
|
11749
12223
|
}
|
|
12224
|
+
/**
|
|
12225
|
+
* Returns the historic records to interpolate between, fetching only what is not already held.
|
|
12226
|
+
*/
|
|
12227
|
+
async gatherHistoric(entities) {
|
|
12228
|
+
const { recordsByIds } = await this.historicGatherer.Gather(entities);
|
|
12229
|
+
return recordsByIds;
|
|
12230
|
+
}
|
|
11750
12231
|
async doRenderAsIndividuals(entities, force = false) {
|
|
11751
12232
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
11752
12233
|
// When live we just want to show the latest pos.
|
|
@@ -11761,19 +12242,7 @@
|
|
|
11761
12242
|
const isHistoric = ((_a = this.item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historic) || ((_b = this.item.BruceEntity) === null || _b === void 0 ? void 0 : _b.historicAttrKey);
|
|
11762
12243
|
// If we're interpolating then we request additional records to quickly swap between.
|
|
11763
12244
|
if (!isLive && isHistoric && entities.length && this.item.historicInterpolation) {
|
|
11764
|
-
|
|
11765
|
-
const startTmp = Cesium.JulianDate.toDate(this.viewer.clock.startTime);
|
|
11766
|
-
const stopTmp = Cesium.JulianDate.toDate(this.viewer.clock.stopTime);
|
|
11767
|
-
const startStr = new Date(startTmp.getTime() - 1000).toISOString();
|
|
11768
|
-
const stopStr = new Date(stopTmp.getTime() + 1000).toISOString();
|
|
11769
|
-
const historicData = await BModels.EntityHistoricData.GetList({
|
|
11770
|
-
attrKey: this.item.BruceEntity.historicAttrKey,
|
|
11771
|
-
dateTimeFrom: startStr,
|
|
11772
|
-
dateTimeTo: stopStr,
|
|
11773
|
-
entityIds: entities.map(x => x.Bruce.ID),
|
|
11774
|
-
api: this.apiGetter.getApi()
|
|
11775
|
-
});
|
|
11776
|
-
entitiesHistoric = historicData.recordsByIds;
|
|
12245
|
+
entitiesHistoric = await this.gatherHistoric(entities);
|
|
11777
12246
|
}
|
|
11778
12247
|
if (this.disposed) {
|
|
11779
12248
|
this.doDispose();
|
|
@@ -12887,6 +13356,13 @@
|
|
|
12887
13356
|
this.monitor = monitor;
|
|
12888
13357
|
this.item = item;
|
|
12889
13358
|
this.visualsManager = visualsManager;
|
|
13359
|
+
this.historicGatherer = new HistoricGatherer({
|
|
13360
|
+
viewer: this.viewer,
|
|
13361
|
+
getApi: () => this.apiGetter.getApi(),
|
|
13362
|
+
getAttrKey: () => { var _a; return (_a = this.item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historicAttrKey; },
|
|
13363
|
+
// Padding either side of a requested range, for clock desync between us and the server.
|
|
13364
|
+
paddingMs: 15000
|
|
13365
|
+
});
|
|
12890
13366
|
if (this.item.enableClustering) {
|
|
12891
13367
|
this.clustering = new PointClustering(visualsManager, this.item.id, (_a = this.item) === null || _a === void 0 ? void 0 : _a.clustering);
|
|
12892
13368
|
}
|
|
@@ -13236,138 +13712,22 @@
|
|
|
13236
13712
|
* @param entities
|
|
13237
13713
|
*/
|
|
13238
13714
|
async getHistoricInfo(entities) {
|
|
13239
|
-
|
|
13240
|
-
// Helps account for desync between client and server.
|
|
13241
|
-
const TIME_PADDING_MS = 15000;
|
|
13242
|
-
const minDateTimeStr = this.viewer.clock.startTime.toString();
|
|
13243
|
-
const maxDateTimeStr = this.viewer.clock.stopTime.toString();
|
|
13244
|
-
const minDateTime = new Date(minDateTimeStr).getTime();
|
|
13245
|
-
const maxDateTime = new Date(maxDateTimeStr).getTime();
|
|
13246
|
-
const tDetails = exports.ViewUtils.GetTimeDetails({
|
|
13247
|
-
viewer: this.viewer,
|
|
13248
|
-
});
|
|
13249
|
-
const isLive = tDetails.isLive;
|
|
13250
|
-
let rangesToRequest = [{
|
|
13251
|
-
start: minDateTime,
|
|
13252
|
-
stop: maxDateTime
|
|
13253
|
-
}];
|
|
13254
|
-
// If we already have cached data, determine what ranges we're missing.
|
|
13255
|
-
if (Object.keys(this.entitiesHistoric).length >= entities.length) {
|
|
13256
|
-
let foundMinDateTime = null;
|
|
13257
|
-
let foundMaxDateTime = null;
|
|
13258
|
-
// Find the min/max based on cached data.
|
|
13259
|
-
// Since we set the values sorted, we only have to check the first and last records for each entity.
|
|
13260
|
-
for (const entityId of Object.keys(this.entitiesHistoric)) {
|
|
13261
|
-
const records = this.entitiesHistoric[entityId] || [];
|
|
13262
|
-
if (records.length) {
|
|
13263
|
-
const dateTime = new Date(records[0].dateTime).getTime();
|
|
13264
|
-
if (foundMinDateTime == null || dateTime < foundMinDateTime) {
|
|
13265
|
-
foundMinDateTime = dateTime;
|
|
13266
|
-
}
|
|
13267
|
-
const dateTime2 = new Date(records[records.length - 1].dateTime).getTime();
|
|
13268
|
-
if (foundMaxDateTime == null || dateTime2 > foundMaxDateTime) {
|
|
13269
|
-
foundMaxDateTime = dateTime2;
|
|
13270
|
-
}
|
|
13271
|
-
}
|
|
13272
|
-
}
|
|
13273
|
-
// Complete overlap - we already have all the data.
|
|
13274
|
-
if (foundMinDateTime != null && foundMaxDateTime != null && foundMinDateTime <= minDateTime && foundMaxDateTime >= maxDateTime) {
|
|
13275
|
-
return [false, this.entitiesHistoric];
|
|
13276
|
-
}
|
|
13277
|
-
// Calculate missing ranges.
|
|
13278
|
-
rangesToRequest = [];
|
|
13279
|
-
// Check if we need data before our cached range.
|
|
13280
|
-
if (foundMinDateTime != null && foundMinDateTime > minDateTime) {
|
|
13281
|
-
// When live we focus on getting the next range rather than old data.
|
|
13282
|
-
// Ends up being one less request to perform every second.
|
|
13283
|
-
if (!isLive || foundMinDateTime == null) {
|
|
13284
|
-
rangesToRequest.push({
|
|
13285
|
-
start: minDateTime,
|
|
13286
|
-
stop: new Date(foundMinDateTime).getTime()
|
|
13287
|
-
});
|
|
13288
|
-
}
|
|
13289
|
-
}
|
|
13290
|
-
// Check if we need data after our cached range.
|
|
13291
|
-
if (foundMaxDateTime != null && foundMaxDateTime < maxDateTime) {
|
|
13292
|
-
rangesToRequest.push({
|
|
13293
|
-
start: new Date(foundMaxDateTime).getTime(),
|
|
13294
|
-
stop: maxDateTime
|
|
13295
|
-
});
|
|
13296
|
-
}
|
|
13297
|
-
// If the ranges to request are within a tolerance, combine them.
|
|
13298
|
-
if (rangesToRequest.length > 1) {
|
|
13299
|
-
const TOLERANCE_MS = 60000; // 1 minute.
|
|
13300
|
-
let currentRange = rangesToRequest[0];
|
|
13301
|
-
for (let i = 1; i < rangesToRequest.length; i++) {
|
|
13302
|
-
const nextRange = rangesToRequest[i];
|
|
13303
|
-
if (nextRange.start - currentRange.stop <= TOLERANCE_MS) {
|
|
13304
|
-
currentRange.stop = nextRange.stop;
|
|
13305
|
-
}
|
|
13306
|
-
else {
|
|
13307
|
-
currentRange = nextRange;
|
|
13308
|
-
}
|
|
13309
|
-
}
|
|
13310
|
-
}
|
|
13311
|
-
}
|
|
13312
|
-
const entityIds = entities.map(x => x.Bruce.ID);
|
|
13313
|
-
const combined = { ...this.entitiesHistoric };
|
|
13314
|
-
// Make requests for each missing range
|
|
13315
|
-
for (const range of rangesToRequest) {
|
|
13316
|
-
if (this.disposed) {
|
|
13317
|
-
break;
|
|
13318
|
-
}
|
|
13319
|
-
const start = new Date(range.start - TIME_PADDING_MS);
|
|
13320
|
-
const stop = new Date(range.stop + TIME_PADDING_MS);
|
|
13321
|
-
const historicData = await BModels.EntityHistoricData.GetList({
|
|
13322
|
-
attrKey: this.item.BruceEntity.historicAttrKey,
|
|
13323
|
-
dateTimeFrom: start.toISOString(),
|
|
13324
|
-
dateTimeTo: stop.toISOString(),
|
|
13325
|
-
entityIds: entityIds,
|
|
13326
|
-
api: this.apiGetter.getApi()
|
|
13327
|
-
});
|
|
13328
|
-
if (this.disposed) {
|
|
13329
|
-
break;
|
|
13330
|
-
}
|
|
13331
|
-
// Merge the new data with existing data
|
|
13332
|
-
const records = historicData.recordsByIds;
|
|
13333
|
-
const recordsIds = Object.keys(records);
|
|
13334
|
-
for (let i = 0; i < recordsIds.length; i++) {
|
|
13335
|
-
const entityId = recordsIds[i];
|
|
13336
|
-
const latest = records[entityId] || [];
|
|
13337
|
-
const current = combined[entityId] || [];
|
|
13338
|
-
// Use a Map to de-duplicate by timestamp.
|
|
13339
|
-
const tmp = new Map();
|
|
13340
|
-
for (let j = 0; j < current.length; j++) {
|
|
13341
|
-
const record = current[j];
|
|
13342
|
-
const dateTime = new Date(record.dateTime).getTime();
|
|
13343
|
-
tmp.set(dateTime, record);
|
|
13344
|
-
}
|
|
13345
|
-
for (let j = 0; j < latest.length; j++) {
|
|
13346
|
-
const record = latest[j];
|
|
13347
|
-
const dateTime = new Date(record.dateTime).getTime();
|
|
13348
|
-
tmp.set(dateTime, record);
|
|
13349
|
-
}
|
|
13350
|
-
// Convert to array and sort by date.
|
|
13351
|
-
const sorted = Array.from(tmp.values()).sort((a, b) => {
|
|
13352
|
-
return new Date(a.dateTime).getTime() - new Date(b.dateTime).getTime();
|
|
13353
|
-
});
|
|
13354
|
-
combined[entityId] = sorted;
|
|
13355
|
-
}
|
|
13356
|
-
}
|
|
13715
|
+
const { fetched, recordsByIds } = await this.historicGatherer.Gather(entities);
|
|
13357
13716
|
if (!this.disposed) {
|
|
13358
|
-
this.entitiesHistoric =
|
|
13717
|
+
this.entitiesHistoric = recordsByIds;
|
|
13359
13718
|
}
|
|
13360
|
-
return [
|
|
13719
|
+
return [fetched, recordsByIds];
|
|
13361
13720
|
}
|
|
13362
13721
|
viewerDateTimeSub() {
|
|
13363
13722
|
var _a, _b;
|
|
13364
13723
|
if ((!((_a = this.item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historic) && !((_b = this.item.BruceEntity) === null || _b === void 0 ? void 0 : _b.historicAttrKey)) || this.viewerDateTimeChangeRemoval) {
|
|
13365
13724
|
return;
|
|
13366
13725
|
}
|
|
13367
|
-
|
|
13726
|
+
// Scaled by how many Entities are on screen rather than fixed, since the cost of a refresh
|
|
13727
|
+
// is the set size and a large set barely changes between ticks anyway.
|
|
13368
13728
|
let queue = new BModels.DelayQueue(() => {
|
|
13369
13729
|
this.onGetterUpdate(Object.keys(this.renderedEntities));
|
|
13370
|
-
},
|
|
13730
|
+
}, this.historicGatherer.IntervalFor(Object.keys(this.renderedEntities).length), true);
|
|
13371
13731
|
let clockTickRemoval;
|
|
13372
13732
|
let prevTick = this.viewer.clock.currentTime.toString();
|
|
13373
13733
|
clockTickRemoval = this.viewer.clock.onTick.addEventListener(() => {
|
|
@@ -14055,6 +14415,9 @@
|
|
|
14055
14415
|
// Indicates that we are retrieving historic records.
|
|
14056
14416
|
// This means that the current scene's time is included in the request.
|
|
14057
14417
|
this.historic = false;
|
|
14418
|
+
// When false the Entity data is still gathered and the regos still updated, but no colour is
|
|
14419
|
+
// applied. A historic item that did not ask for styling needs the first without the second.
|
|
14420
|
+
this.applyStyles = true;
|
|
14058
14421
|
this.reRenderState = new EntityReRenderMaintainState();
|
|
14059
14422
|
// More expensive process.
|
|
14060
14423
|
// When an Entity is styled, the rego is reviewed and updated if needed.
|
|
@@ -14091,13 +14454,16 @@
|
|
|
14091
14454
|
}
|
|
14092
14455
|
Init(params) {
|
|
14093
14456
|
var _a, _b;
|
|
14094
|
-
let { viewer, api, cTileset, fallbackStyleId, styleMapping, expandSources, menuItemId, register, scenario, historic } = params;
|
|
14457
|
+
let { viewer, api, cTileset, fallbackStyleId, styleMapping, expandSources, menuItemId, register, scenario, historic, applyStyles } = params;
|
|
14095
14458
|
this.viewer = viewer;
|
|
14096
14459
|
this.api = api;
|
|
14097
14460
|
this.cTileset = cTileset;
|
|
14098
14461
|
this.register = register;
|
|
14099
14462
|
this.menuItemId = menuItemId;
|
|
14100
14463
|
this.historic = Boolean(historic);
|
|
14464
|
+
if (applyStyles != null) {
|
|
14465
|
+
this.applyStyles = Boolean(applyStyles);
|
|
14466
|
+
}
|
|
14101
14467
|
if (expandSources != null) {
|
|
14102
14468
|
this.expandSources = expandSources;
|
|
14103
14469
|
}
|
|
@@ -14591,33 +14957,31 @@
|
|
|
14591
14957
|
return;
|
|
14592
14958
|
}
|
|
14593
14959
|
const { settings: style, styleId: resolvedStyleId } = this.getTilesetFeatureStyleWithId(rego.entityTypeId);
|
|
14594
|
-
const fillColorTrace = (style && ((_a = style.modelStyle) === null || _a === void 0 ? void 0 : _a.fillColor)) ? BModels.Calculator.TraceGetColor(style.modelStyle.fillColor, data, tags) : { value: null, effective: null };
|
|
14595
|
-
|
|
14596
|
-
|
|
14597
|
-
if (bColor == null) {
|
|
14598
|
-
cColor = Cesium.Color.WHITE;
|
|
14599
|
-
}
|
|
14600
|
-
else {
|
|
14601
|
-
cColor = colorToCColor$3(bColor);
|
|
14602
|
-
}
|
|
14960
|
+
const fillColorTrace = (this.applyStyles && style && ((_a = style.modelStyle) === null || _a === void 0 ? void 0 : _a.fillColor)) ? BModels.Calculator.TraceGetColor(style.modelStyle.fillColor, data, tags) : { value: null, effective: null };
|
|
14961
|
+
// Without styling the feature keeps whatever the tileset baked in. Falling through would paint
|
|
14962
|
+
// it the default white, which is a visible change for an item that only asked for historic.
|
|
14603
14963
|
const override = this.overrideFeatureColor.get(rego.entityId) == true;
|
|
14604
|
-
|
|
14605
|
-
|
|
14606
|
-
|
|
14607
|
-
|
|
14608
|
-
|
|
14609
|
-
|
|
14610
|
-
|
|
14611
|
-
|
|
14964
|
+
if (this.applyStyles) {
|
|
14965
|
+
const bColor = fillColorTrace.value;
|
|
14966
|
+
const cColor = bColor == null ? Cesium.Color.WHITE : colorToCColor$3(bColor);
|
|
14967
|
+
exports.CesiumEntityStyler.SetDefaultColor({
|
|
14968
|
+
color: cColor,
|
|
14969
|
+
entity: visual,
|
|
14970
|
+
viewer: this.viewer,
|
|
14971
|
+
override: override
|
|
14972
|
+
});
|
|
14973
|
+
this.overrideFeatureColor.set(rego.entityId, true);
|
|
14974
|
+
if (resolvedStyleId != null) {
|
|
14975
|
+
rego.styleId = resolvedStyleId;
|
|
14976
|
+
}
|
|
14977
|
+
rego.styleEffective = (_b = exports.StyleEffective.Combine([
|
|
14978
|
+
{ key: "color", effective: fillColorTrace.effective }
|
|
14979
|
+
])) !== null && _b !== void 0 ? _b : rego.styleEffective;
|
|
14980
|
+
}
|
|
14981
|
+
if (((_c = data === null || data === void 0 ? void 0 : data.Bruce) === null || _c === void 0 ? void 0 : _c.InternalID) != null) {
|
|
14612
14982
|
rego.internalId = data.Bruce.InternalID;
|
|
14613
14983
|
this.styledByInternalId.set(data.Bruce.InternalID, rego.entityId);
|
|
14614
14984
|
}
|
|
14615
|
-
if (resolvedStyleId != null) {
|
|
14616
|
-
rego.styleId = resolvedStyleId;
|
|
14617
|
-
}
|
|
14618
|
-
rego.styleEffective = (_c = exports.StyleEffective.Combine([
|
|
14619
|
-
{ key: "color", effective: fillColorTrace.effective }
|
|
14620
|
-
])) !== null && _c !== void 0 ? _c : rego.styleEffective;
|
|
14621
14985
|
this.styledEntityIds.set(rego.entityId, true);
|
|
14622
14986
|
this._styleProgressQueue.Call();
|
|
14623
14987
|
// Since we only need to update it for scenarios right now.
|
|
@@ -15582,6 +15946,7 @@
|
|
|
15582
15946
|
// Series of points to help interpolate movement when the timeline changes.
|
|
15583
15947
|
this.historicPossesInitialLoaded = false;
|
|
15584
15948
|
this.historicAnimation = null;
|
|
15949
|
+
this.historicGatherer = null;
|
|
15585
15950
|
this.historicPossesLoadingProm = null;
|
|
15586
15951
|
// Queue of loaded in features that we haven't processed yet.
|
|
15587
15952
|
this.featureQueue = [];
|
|
@@ -16619,120 +16984,41 @@
|
|
|
16619
16984
|
const api = this.getters.GetBruceApi({
|
|
16620
16985
|
accountId: accountId
|
|
16621
16986
|
});
|
|
16622
|
-
//
|
|
16623
|
-
|
|
16624
|
-
|
|
16625
|
-
|
|
16626
|
-
// Helps us avoid repeated requests that are the same.
|
|
16627
|
-
const pendingRequests = new Map();
|
|
16628
|
-
/**
|
|
16629
|
-
* Returns a list of historic positions for a given time range.
|
|
16630
|
-
* @param startStr
|
|
16631
|
-
* @param stopStr
|
|
16632
|
-
* @returns
|
|
16987
|
+
// The root Entity is the only one here, so nothing is banded and the plan sees a set of one.
|
|
16988
|
+
const rootEntity = { Bruce: { ID: this.rootId } };
|
|
16989
|
+
/*
|
|
16990
|
+
* Turns historic records into the animated positions this tileset moves along.
|
|
16633
16991
|
*/
|
|
16634
|
-
const
|
|
16635
|
-
|
|
16636
|
-
if (pendingRequests.has(requestKey)) {
|
|
16637
|
-
return pendingRequests.get(requestKey);
|
|
16638
|
-
}
|
|
16639
|
-
const requestPromise = new Promise(async (res) => {
|
|
16640
|
-
try {
|
|
16641
|
-
const historicData = await BModels.EntityHistoricData.GetList({
|
|
16642
|
-
attrKey: null,
|
|
16643
|
-
dateTimeFrom: startStr,
|
|
16644
|
-
dateTimeTo: stopStr,
|
|
16645
|
-
entityIds: [this.rootId],
|
|
16646
|
-
api: api
|
|
16647
|
-
});
|
|
16648
|
-
const posses = exports.CesiumAnimatedProperty.GetSeriesPossesForHistoricEntity(this.viewer, Cesium.HeightReference.NONE, Cesium.HeightReference.CLAMP_TO_GROUND, historicData.recordsByIds[this.rootId]);
|
|
16649
|
-
res(posses);
|
|
16650
|
-
}
|
|
16651
|
-
catch (e) {
|
|
16652
|
-
console.error(e);
|
|
16653
|
-
res([]);
|
|
16654
|
-
}
|
|
16655
|
-
finally {
|
|
16656
|
-
pendingRequests.delete(requestKey);
|
|
16657
|
-
}
|
|
16658
|
-
});
|
|
16659
|
-
pendingRequests.set(requestKey, requestPromise);
|
|
16660
|
-
return requestPromise;
|
|
16992
|
+
const toPosses = (records) => {
|
|
16993
|
+
return exports.CesiumAnimatedProperty.GetSeriesPossesForHistoricEntity(this.viewer, Cesium.HeightReference.NONE, Cesium.HeightReference.CLAMP_TO_GROUND, records);
|
|
16661
16994
|
};
|
|
16995
|
+
this.historicGatherer = new HistoricGatherer({
|
|
16996
|
+
viewer: this.viewer,
|
|
16997
|
+
getApi: () => api,
|
|
16998
|
+
// The assembly root snapshots the whole Entity, so no key narrows it.
|
|
16999
|
+
getAttrKey: () => null,
|
|
17000
|
+
// Time padding in milliseconds (15 seconds).
|
|
17001
|
+
// Helps account for desync between client and server.
|
|
17002
|
+
paddingMs: 15000,
|
|
17003
|
+
onRecords: (recordsByIds) => {
|
|
17004
|
+
var _a;
|
|
17005
|
+
const records = recordsByIds[this.rootId];
|
|
17006
|
+
if (!(records === null || records === void 0 ? void 0 : records.length) || !((_a = this.historicAnimation) === null || _a === void 0 ? void 0 : _a.addPositions)) {
|
|
17007
|
+
return;
|
|
17008
|
+
}
|
|
17009
|
+
this.historicAnimation.addPositions(toPosses(records));
|
|
17010
|
+
}
|
|
17011
|
+
});
|
|
16662
17012
|
/**
|
|
16663
|
-
*
|
|
16664
|
-
*
|
|
17013
|
+
* Fetches whatever the current timeline range needs that is not already held.
|
|
17014
|
+
* Positions reach the animation through the gatherer's callback as each batch lands.
|
|
16665
17015
|
* @returns
|
|
16666
17016
|
*/
|
|
16667
17017
|
const checkTimelineRange = async () => {
|
|
16668
|
-
|
|
16669
|
-
const minDateTime = new Date(this.viewer.clock.startTime.toString()).getTime();
|
|
16670
|
-
const maxDateTime = new Date(this.viewer.clock.stopTime.toString()).getTime();
|
|
16671
|
-
// What we have loaded.
|
|
16672
|
-
const range = this.historicAnimation.getDateRange();
|
|
16673
|
-
const foundMinDateTime = (range === null || range === void 0 ? void 0 : range.minDate) ? range.minDate.getTime() : null;
|
|
16674
|
-
const foundMaxDateTime = (range === null || range === void 0 ? void 0 : range.maxDate) ? range.maxDate.getTime() : null;
|
|
16675
|
-
// See if the current range is within the range we already have.
|
|
16676
|
-
if (this.historicPossesInitialLoaded &&
|
|
16677
|
-
foundMinDateTime &&
|
|
16678
|
-
foundMaxDateTime &&
|
|
16679
|
-
minDateTime >= foundMinDateTime &&
|
|
16680
|
-
maxDateTime <= foundMaxDateTime) {
|
|
16681
|
-
return;
|
|
16682
|
-
}
|
|
16683
|
-
const tDetails = exports.ViewUtils.GetTimeDetails({
|
|
16684
|
-
viewer: this.viewer,
|
|
16685
|
-
});
|
|
16686
|
-
const isLive = tDetails.isLive;
|
|
16687
|
-
// See if the requested range is before or after the range we have.
|
|
16688
|
-
const fetchBefore = !foundMinDateTime || (!isLive && minDateTime < foundMinDateTime);
|
|
16689
|
-
const fetchAfter = !foundMaxDateTime || maxDateTime > foundMaxDateTime;
|
|
16690
|
-
if (!fetchBefore && !fetchAfter) {
|
|
16691
|
-
// Already have the data we need.
|
|
17018
|
+
if (this.disposed) {
|
|
16692
17019
|
return;
|
|
16693
17020
|
}
|
|
16694
|
-
|
|
16695
|
-
if (!foundMinDateTime || !foundMaxDateTime) {
|
|
16696
|
-
const startStr = new Date(minDateTime - TIME_PADDING_MS).toISOString();
|
|
16697
|
-
const stopStr = new Date(maxDateTime + TIME_PADDING_MS).toISOString();
|
|
16698
|
-
const newPositions = await getPossesForRange(startStr, stopStr);
|
|
16699
|
-
if (this.disposed) {
|
|
16700
|
-
return;
|
|
16701
|
-
}
|
|
16702
|
-
if (this.historicAnimation && this.historicAnimation.addPositions) {
|
|
16703
|
-
this.historicAnimation.addPositions(newPositions);
|
|
16704
|
-
}
|
|
16705
|
-
}
|
|
16706
|
-
else {
|
|
16707
|
-
// The data we want is before the range we've currently loaded.
|
|
16708
|
-
if (fetchBefore) {
|
|
16709
|
-
// Calculate the missing difference and request it.
|
|
16710
|
-
const startStr = new Date(minDateTime - TIME_PADDING_MS).toISOString();
|
|
16711
|
-
const stopStr = new Date(foundMinDateTime + TIME_PADDING_MS).toISOString();
|
|
16712
|
-
getPossesForRange(startStr, stopStr).then((newPositions) => {
|
|
16713
|
-
if (this.disposed) {
|
|
16714
|
-
return;
|
|
16715
|
-
}
|
|
16716
|
-
if (this.historicAnimation && this.historicAnimation.addPositions) {
|
|
16717
|
-
this.historicAnimation.addPositions(newPositions);
|
|
16718
|
-
}
|
|
16719
|
-
});
|
|
16720
|
-
}
|
|
16721
|
-
// The data we want is after the range we've currently loaded.
|
|
16722
|
-
if (fetchAfter) {
|
|
16723
|
-
// Calculate the missing difference and request it.
|
|
16724
|
-
const startStr = new Date(foundMaxDateTime - TIME_PADDING_MS).toISOString();
|
|
16725
|
-
const stopStr = new Date(maxDateTime + TIME_PADDING_MS).toISOString();
|
|
16726
|
-
getPossesForRange(startStr, stopStr).then((newPositions) => {
|
|
16727
|
-
if (this.disposed) {
|
|
16728
|
-
return;
|
|
16729
|
-
}
|
|
16730
|
-
if (this.historicAnimation && this.historicAnimation.addPositions) {
|
|
16731
|
-
this.historicAnimation.addPositions(newPositions);
|
|
16732
|
-
}
|
|
16733
|
-
});
|
|
16734
|
-
}
|
|
16735
|
-
}
|
|
17021
|
+
await this.historicGatherer.Gather([rootEntity]);
|
|
16736
17022
|
};
|
|
16737
17023
|
/**
|
|
16738
17024
|
* Requests the initial set of historic positions for the timeline range.
|
|
@@ -16746,11 +17032,8 @@
|
|
|
16746
17032
|
res(false);
|
|
16747
17033
|
return;
|
|
16748
17034
|
}
|
|
16749
|
-
const
|
|
16750
|
-
const
|
|
16751
|
-
const startStr = new Date(startTmp.getTime() - 1000).toISOString();
|
|
16752
|
-
const stopStr = new Date(stopTmp.getTime() + 1000).toISOString();
|
|
16753
|
-
const positions = await getPossesForRange(startStr, stopStr);
|
|
17035
|
+
const { recordsByIds } = await this.historicGatherer.Gather([rootEntity]);
|
|
17036
|
+
const positions = toPosses(recordsByIds[this.rootId] || []);
|
|
16754
17037
|
if (this.disposed) {
|
|
16755
17038
|
res(false);
|
|
16756
17039
|
return;
|
|
@@ -19531,12 +19814,11 @@
|
|
|
19531
19814
|
}
|
|
19532
19815
|
this.viewer.scene.requestRender();
|
|
19533
19816
|
exports.TilesetRenderEngine.OnTilesetReady(this.cTileset).then(() => {
|
|
19534
|
-
var _a;
|
|
19535
19817
|
try {
|
|
19536
19818
|
if (this.disposed || this.viewer.isDestroyed()) {
|
|
19537
19819
|
return;
|
|
19538
19820
|
}
|
|
19539
|
-
if (this.item
|
|
19821
|
+
if (Manager.NeedsStyler(this.item)) {
|
|
19540
19822
|
const api = this.getters.GetBruceApi();
|
|
19541
19823
|
this.styler.Init({
|
|
19542
19824
|
viewer: this.viewer,
|
|
@@ -19547,7 +19829,8 @@
|
|
|
19547
19829
|
expandSources: false,
|
|
19548
19830
|
menuItemId: this.item.id,
|
|
19549
19831
|
register: this.visualsManager,
|
|
19550
|
-
historic: (
|
|
19832
|
+
historic: Manager.IsHistoric(this.item),
|
|
19833
|
+
applyStyles: Boolean(this.item.ApplyStyles)
|
|
19551
19834
|
});
|
|
19552
19835
|
}
|
|
19553
19836
|
this.onCTilesetLoad();
|
|
@@ -19754,6 +20037,19 @@
|
|
|
19754
20037
|
get HandoffKey() {
|
|
19755
20038
|
return Manager.GetHandoffKey(this.item);
|
|
19756
20039
|
}
|
|
20040
|
+
/*
|
|
20041
|
+
* Returns whether the item asks for Entities as they were at the scene time.
|
|
20042
|
+
*/
|
|
20043
|
+
static IsHistoric(item) {
|
|
20044
|
+
var _a, _b;
|
|
20045
|
+
return Boolean(((_a = item === null || item === void 0 ? void 0 : item.BruceEntity) === null || _a === void 0 ? void 0 : _a.historic) || ((_b = item === null || item === void 0 ? void 0 : item.BruceEntity) === null || _b === void 0 ? void 0 : _b.historicAttrKey));
|
|
20046
|
+
}
|
|
20047
|
+
/*
|
|
20048
|
+
* Returns whether the styler has to run, which it does for historic even without styling.
|
|
20049
|
+
*/
|
|
20050
|
+
static NeedsStyler(item) {
|
|
20051
|
+
return Boolean(item === null || item === void 0 ? void 0 : item.ApplyStyles) || Manager.IsHistoric(item);
|
|
20052
|
+
}
|
|
19757
20053
|
static GetHandoffKey(item) {
|
|
19758
20054
|
var _a, _b;
|
|
19759
20055
|
const tilesetId = (_a = item === null || item === void 0 ? void 0 : item.tileset) === null || _a === void 0 ? void 0 : _a.TilesetID;
|
|
@@ -19763,7 +20059,10 @@
|
|
|
19763
20059
|
const accountId = ((_b = item.tileset) === null || _b === void 0 ? void 0 : _b.ClientAccountID) || "";
|
|
19764
20060
|
// Styled <-> unstyled has no cheap revert path, force a reload instead.
|
|
19765
20061
|
const applyStyles = Boolean(item.ApplyStyles);
|
|
19766
|
-
|
|
20062
|
+
// Historic drives a different gather,
|
|
20063
|
+
// so a handoff must not reuse a styler set up for the other mode.
|
|
20064
|
+
const historic = Manager.IsHistoric(item);
|
|
20065
|
+
return `${accountId}:${tilesetId}:${applyStyles}:${historic}`;
|
|
19767
20066
|
}
|
|
19768
20067
|
// Returns null (fall back to a normal Dispose()) if the tileset hasn't finished loading yet.
|
|
19769
20068
|
PrepareHandoff() {
|
|
@@ -19803,7 +20102,6 @@
|
|
|
19803
20102
|
// Takes ownership of a PrepareHandoff() payload instead of loading via Init().
|
|
19804
20103
|
// Then re-parents every already-registered rego to this menu item and re-runs styling.
|
|
19805
20104
|
AdoptHandoff(payload) {
|
|
19806
|
-
var _a;
|
|
19807
20105
|
this.cTileset = payload.cTileset;
|
|
19808
20106
|
this.tileset = payload.tileset;
|
|
19809
20107
|
this.typeId = payload.typeId;
|
|
@@ -19835,7 +20133,7 @@
|
|
|
19835
20133
|
toPriority: this.renderPriority,
|
|
19836
20134
|
requestRender: false
|
|
19837
20135
|
});
|
|
19838
|
-
if (this.item
|
|
20136
|
+
if (Manager.NeedsStyler(this.item)) {
|
|
19839
20137
|
this.styler.Init({
|
|
19840
20138
|
viewer: this.viewer,
|
|
19841
20139
|
api: this.getters.GetBruceApi(),
|
|
@@ -19845,7 +20143,8 @@
|
|
|
19845
20143
|
expandSources: false,
|
|
19846
20144
|
menuItemId: this.item.id,
|
|
19847
20145
|
register: this.visualsManager,
|
|
19848
|
-
historic: (
|
|
20146
|
+
historic: Manager.IsHistoric(this.item),
|
|
20147
|
+
applyStyles: Boolean(this.item.ApplyStyles)
|
|
19849
20148
|
});
|
|
19850
20149
|
this.queueHandoffRestyle(this.visualsManager.GetRegos({ menuItemId: this.item.id }));
|
|
19851
20150
|
}
|
|
@@ -39877,6 +40176,107 @@
|
|
|
39877
40176
|
data[i + 3] = Math.round(srcAlpha * maskAlpha * 255);
|
|
39878
40177
|
}
|
|
39879
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
|
+
}
|
|
39880
40280
|
function loadImage(src) {
|
|
39881
40281
|
return new Promise((res, rej) => {
|
|
39882
40282
|
const image = new Image();
|
|
@@ -39888,6 +40288,87 @@
|
|
|
39888
40288
|
|
|
39889
40289
|
var TextureFrameSeriesAnimator;
|
|
39890
40290
|
(function (TextureFrameSeriesAnimator) {
|
|
40291
|
+
// Values are measured up from the ground beneath them.
|
|
40292
|
+
TextureFrameSeriesAnimator.QUANTITY_THICKNESS = "thickness";
|
|
40293
|
+
// Values are heights against a vertical datum, so they need that datum to be placed.
|
|
40294
|
+
TextureFrameSeriesAnimator.QUANTITY_ELEVATION = "elevation";
|
|
40295
|
+
TextureFrameSeriesAnimator.LAYOUT_TILES = "tiles";
|
|
40296
|
+
/**
|
|
40297
|
+
* The generator that produced an archive, for telling a stale one from a current one.
|
|
40298
|
+
*
|
|
40299
|
+
* Read the version rather than sniffing for fields: an archive can legitimately omit Tiles or
|
|
40300
|
+
* GeoidSeparation and still be current, so their absence says nothing about its age.
|
|
40301
|
+
* @param metadata the archive's Data.generation
|
|
40302
|
+
*/
|
|
40303
|
+
function GeneratorVersion(metadata) {
|
|
40304
|
+
if (!metadata || typeof metadata.GeneratorVersion !== "number") {
|
|
40305
|
+
return 1;
|
|
40306
|
+
}
|
|
40307
|
+
return metadata.GeneratorVersion;
|
|
40308
|
+
}
|
|
40309
|
+
TextureFrameSeriesAnimator.GeneratorVersion = GeneratorVersion;
|
|
40310
|
+
/**
|
|
40311
|
+
* Whether an archive is an adaptive pyramid rather than one raster.
|
|
40312
|
+
* @param metadata the archive's Data.generation
|
|
40313
|
+
*/
|
|
40314
|
+
function IsTiledLayout(metadata) {
|
|
40315
|
+
return (Boolean(metadata && metadata.Layout === TextureFrameSeriesAnimator.LAYOUT_TILES &&
|
|
40316
|
+
metadata.Tiles && metadata.Tiles.length > 0));
|
|
40317
|
+
}
|
|
40318
|
+
TextureFrameSeriesAnimator.IsTiledLayout = IsTiledLayout;
|
|
40319
|
+
// Frames carry the value as the source reported it.
|
|
40320
|
+
TextureFrameSeriesAnimator.MEASURE_ABSOLUTE = "absolute";
|
|
40321
|
+
// Frames carry the value minus each cell's own minimum over the series.
|
|
40322
|
+
TextureFrameSeriesAnimator.MEASURE_ANOMALY = "anomaly";
|
|
40323
|
+
/**
|
|
40324
|
+
* Turns a value in the attribute's own units into the 0 to 1 position the ramp is indexed by.
|
|
40325
|
+
*
|
|
40326
|
+
* Stops are authored in real units, since "hide anything under 0.1 m" is the sentence a user
|
|
40327
|
+
* actually has, and the archive's own published range is what makes that expressible.
|
|
40328
|
+
* @param metadata the archive's Data.generation
|
|
40329
|
+
* @param value in the attribute's units
|
|
40330
|
+
*/
|
|
40331
|
+
function NormalisePosition(metadata, value) {
|
|
40332
|
+
const lo = metadata && typeof metadata.ValueMin === "number" ? metadata.ValueMin : 0;
|
|
40333
|
+
const hi = metadata && typeof metadata.ValueMax === "number" ? metadata.ValueMax : 1;
|
|
40334
|
+
const span = hi - lo;
|
|
40335
|
+
if (!(span > 0)) {
|
|
40336
|
+
return 0;
|
|
40337
|
+
}
|
|
40338
|
+
return Math.min(1, Math.max(0, (value - lo) / span));
|
|
40339
|
+
}
|
|
40340
|
+
TextureFrameSeriesAnimator.NormalisePosition = NormalisePosition;
|
|
40341
|
+
/**
|
|
40342
|
+
* Whether the frames are a departure from each cell's own normal rather than a raw reading.
|
|
40343
|
+
*
|
|
40344
|
+
* Worth asking before labelling anything: the two measures need different words for the same
|
|
40345
|
+
* number, and calling an anomaly "water depth" is a worse error than the coarse ramp it replaced.
|
|
40346
|
+
* @param metadata the archive's Data.generation
|
|
40347
|
+
*/
|
|
40348
|
+
function IsAnomalyMeasure(metadata) {
|
|
40349
|
+
return Boolean(metadata && metadata.Measure === TextureFrameSeriesAnimator.MEASURE_ANOMALY);
|
|
40350
|
+
}
|
|
40351
|
+
TextureFrameSeriesAnimator.IsAnomalyMeasure = IsAnomalyMeasure;
|
|
40352
|
+
/**
|
|
40353
|
+
* How much the value moves anywhere in the series, in source units.
|
|
40354
|
+
*
|
|
40355
|
+
* Falls back to the published range, which is the right answer for an anomaly archive (its range
|
|
40356
|
+
* IS the movement) and the only available one for an archive predating AnomalyMax.
|
|
40357
|
+
* @param metadata the archive's Data.generation
|
|
40358
|
+
*/
|
|
40359
|
+
function MovingRange(metadata) {
|
|
40360
|
+
if (!metadata) {
|
|
40361
|
+
return undefined;
|
|
40362
|
+
}
|
|
40363
|
+
if (typeof metadata.AnomalyMax === "number" && metadata.AnomalyMax > 0) {
|
|
40364
|
+
return metadata.AnomalyMax;
|
|
40365
|
+
}
|
|
40366
|
+
if (typeof metadata.ValueMin === "number" && typeof metadata.ValueMax === "number") {
|
|
40367
|
+
return Math.abs(metadata.ValueMax - metadata.ValueMin);
|
|
40368
|
+
}
|
|
40369
|
+
return undefined;
|
|
40370
|
+
}
|
|
40371
|
+
TextureFrameSeriesAnimator.MovingRange = MovingRange;
|
|
39891
40372
|
/**
|
|
39892
40373
|
* Detects whether a ClientFile's `Data.generation` metadata describes a frame archive rather than a single static image,
|
|
39893
40374
|
* so a caller can decide whether to construct an Animator or fall back to the existing static-texture path.
|
|
@@ -39897,15 +40378,51 @@
|
|
|
39897
40378
|
return Boolean(data && data.generation && Array.isArray(data.generation.Frames) && data.generation.Frames.length > 0);
|
|
39898
40379
|
}
|
|
39899
40380
|
TextureFrameSeriesAnimator.IsFrameArchiveMetadata = IsFrameArchiveMetadata;
|
|
39900
|
-
//
|
|
40381
|
+
// First resolvable static number out of a calculator field list, since that is all a border needs.
|
|
40382
|
+
function resolveNumber(fields) {
|
|
40383
|
+
if (!fields) {
|
|
40384
|
+
return 0;
|
|
40385
|
+
}
|
|
40386
|
+
for (const field of fields) {
|
|
40387
|
+
const value = Number(field && field.value);
|
|
40388
|
+
if (Number.isFinite(value)) {
|
|
40389
|
+
return value;
|
|
40390
|
+
}
|
|
40391
|
+
}
|
|
40392
|
+
return 0;
|
|
40393
|
+
}
|
|
40394
|
+
// First resolvable static colour out of a calculator field list.
|
|
40395
|
+
function resolveColor(fields) {
|
|
40396
|
+
if (!fields) {
|
|
40397
|
+
return null;
|
|
40398
|
+
}
|
|
40399
|
+
for (const field of fields) {
|
|
40400
|
+
const parsed = typeof (field === null || field === void 0 ? void 0 : field.value) === "string"
|
|
40401
|
+
? BModels.Color.ColorFromStr(field.value)
|
|
40402
|
+
: null;
|
|
40403
|
+
if (parsed) {
|
|
40404
|
+
return parsed;
|
|
40405
|
+
}
|
|
40406
|
+
}
|
|
40407
|
+
return null;
|
|
40408
|
+
}
|
|
40409
|
+
/*
|
|
40410
|
+
* Everything about an Animator that a style can change, as a comparable string.
|
|
40411
|
+
*/
|
|
40412
|
+
function AppearanceSignature(options) {
|
|
40413
|
+
var _a, _b, _c;
|
|
40414
|
+
return JSON.stringify([
|
|
40415
|
+
(_a = options.textureColorMask) !== null && _a !== void 0 ? _a : null,
|
|
40416
|
+
(_b = options.cellBorder) !== null && _b !== void 0 ? _b : null,
|
|
40417
|
+
(_c = options.cellTexels) !== null && _c !== void 0 ? _c : null,
|
|
40418
|
+
Boolean(options.maskBaseline)
|
|
40419
|
+
]);
|
|
40420
|
+
}
|
|
40421
|
+
TextureFrameSeriesAnimator.AppearanceSignature = AppearanceSignature;
|
|
40422
|
+
const MAX_COMPOSITE_TEXELS = 2048;
|
|
39901
40423
|
const VALUE_DILATE_PASSES = 2;
|
|
39902
40424
|
/*
|
|
39903
40425
|
* Bleeds covered values outward into uncovered texels, leaving alpha untouched.
|
|
39904
|
-
*
|
|
39905
|
-
* The GPU samples the value texture with linear filtering, so an uncovered texel's RGB still gets
|
|
39906
|
-
* averaged into the vertices next to it even though its alpha is zero. Land sits at one end of the
|
|
39907
|
-
* ramp, so without this the coastline grows a row of spikes exactly where the data stops. Masking the
|
|
39908
|
-
* baseline creates more of these edges, which is what makes this necessary rather than cosmetic.
|
|
39909
40426
|
*/
|
|
39910
40427
|
function dilateValues(value, width, height, passes) {
|
|
39911
40428
|
const texels = width * height;
|
|
@@ -39951,7 +40468,7 @@
|
|
|
39951
40468
|
const DEFAULT_HIGH_COLOR = { red: 21, green: 96, blue: 196, alpha: 0.92 };
|
|
39952
40469
|
class Animator {
|
|
39953
40470
|
constructor(options) {
|
|
39954
|
-
var _a, _b;
|
|
40471
|
+
var _a, _b, _c;
|
|
39955
40472
|
this.removeOnTick = null;
|
|
39956
40473
|
this.removeCrossfadeTick = null;
|
|
39957
40474
|
this.disposed = false;
|
|
@@ -39977,8 +40494,12 @@
|
|
|
39977
40494
|
// forcing Cesium to re-upload the texture only when something changed.
|
|
39978
40495
|
this.pool = [document.createElement("canvas"), document.createElement("canvas")];
|
|
39979
40496
|
this.poolIdx = 0;
|
|
40497
|
+
this.scratch = null;
|
|
39980
40498
|
this.valueDims = null;
|
|
39981
40499
|
this.presentVersion = 0;
|
|
40500
|
+
this.extremesLoad = null;
|
|
40501
|
+
this.floorPixels = null;
|
|
40502
|
+
this.ceilingPixels = null;
|
|
39982
40503
|
if (!options.entity.polygon) {
|
|
39983
40504
|
throw new Error("TextureFrameSeriesAnimator requires an entity with polygon graphics.");
|
|
39984
40505
|
}
|
|
@@ -39990,16 +40511,41 @@
|
|
|
39990
40511
|
this.archiveUrl = options.archiveUrl;
|
|
39991
40512
|
this.frames = options.frames;
|
|
39992
40513
|
this.crossfadeMs = (_a = options.crossfadeMs) !== null && _a !== void 0 ? _a : DEFAULT_CROSSFADE_MS;
|
|
40514
|
+
this.appearance = AppearanceSignature(options);
|
|
39993
40515
|
const mask = options.textureColorMask;
|
|
39994
40516
|
this.lowColor = (mask && BModels.Color.ColorFromStr(mask.minColor)) || DEFAULT_LOW_COLOR;
|
|
39995
40517
|
this.highColor = (mask && BModels.Color.ColorFromStr(mask.maxColor)) || DEFAULT_HIGH_COLOR;
|
|
40518
|
+
// Positions are authored in the attribute's units and normalised once here, so the
|
|
40519
|
+
// per-pixel loop stays a comparison against 0 to 1 like the two-colour path.
|
|
40520
|
+
const points = mask === null || mask === void 0 ? void 0 : mask.points;
|
|
40521
|
+
this.rampStops = (points && points.length > 0)
|
|
40522
|
+
? points.map((p) => ({
|
|
40523
|
+
position: NormalisePosition(options.metadata, p.position),
|
|
40524
|
+
color: BModels.Color.ColorFromStr(p.color) || DEFAULT_LOW_COLOR
|
|
40525
|
+
}))
|
|
40526
|
+
: null;
|
|
40527
|
+
const border = options.cellBorder;
|
|
40528
|
+
const borderWidth = border ? resolveNumber(border.width) : 0;
|
|
40529
|
+
const borderColor = border ? resolveColor(border.color) : null;
|
|
40530
|
+
// Zero width or a fully transparent colour means no grid, matching how the polygon's own
|
|
40531
|
+
// outline behaves, so an existing style that wants no border keeps getting none.
|
|
40532
|
+
this.cellBorder = (border && borderWidth > 0 && borderColor && borderColor.alpha > 0)
|
|
40533
|
+
? {
|
|
40534
|
+
cellTexels: Math.max(1, Math.round((_b = options.cellTexels) !== null && _b !== void 0 ? _b : 1)),
|
|
40535
|
+
widthTexels: borderWidth,
|
|
40536
|
+
color: borderColor
|
|
40537
|
+
}
|
|
40538
|
+
: null;
|
|
39996
40539
|
this.frameDates = this.frames.map((f) => Cesium.JulianDate.fromIso8601(f.Timestamp));
|
|
39997
40540
|
this.frameCache = new Array(this.frames.length).fill(null);
|
|
39998
40541
|
this.frameDims = new Array(this.frames.length).fill(null);
|
|
39999
40542
|
this.valueCache = new Array(this.frames.length).fill(null);
|
|
40000
40543
|
this.produceValueCanvas = Boolean(options.produceValueCanvas);
|
|
40544
|
+
this.metadata = options.metadata;
|
|
40545
|
+
this.drapeExtent = options.drapeExtent;
|
|
40546
|
+
this.tiles = IsTiledLayout(options.metadata) ? options.metadata.Tiles : undefined;
|
|
40001
40547
|
this.baselineMaskEntry = options.baselineMask || null;
|
|
40002
|
-
this.maskBaseline = (
|
|
40548
|
+
this.maskBaseline = (_c = options.maskBaseline) !== null && _c !== void 0 ? _c : Boolean(options.baselineMask);
|
|
40003
40549
|
this.valueCanvas = this.produceValueCanvas ? document.createElement("canvas") : null;
|
|
40004
40550
|
this.driveMaterial = options.driveMaterial !== false;
|
|
40005
40551
|
this.originalMaterial = options.entity.polygon.material;
|
|
@@ -40035,6 +40581,10 @@
|
|
|
40035
40581
|
IsDisposed() {
|
|
40036
40582
|
return this.disposed;
|
|
40037
40583
|
}
|
|
40584
|
+
// What this Animator was built to look like, for deciding whether it can be reused.
|
|
40585
|
+
GetAppearanceSignature() {
|
|
40586
|
+
return this.appearance;
|
|
40587
|
+
}
|
|
40038
40588
|
/**
|
|
40039
40589
|
* The archive URL this instance was constructed with,
|
|
40040
40590
|
* lets a caller re-rendering the same entity tell whether an existing instance is already correct, without reaching into private state.
|
|
@@ -40049,6 +40599,15 @@
|
|
|
40049
40599
|
GetImageProperty() {
|
|
40050
40600
|
return this.imageProperty;
|
|
40051
40601
|
}
|
|
40602
|
+
/**
|
|
40603
|
+
* The presented frame's TINTED pixels, where alpha is what the ramp actually painted.
|
|
40604
|
+
*
|
|
40605
|
+
* Distinct from the value canvas, whose alpha only says a texel has data. A cell can hold a
|
|
40606
|
+
* reading and still be painted nothing, which is exactly what a hidden floor band does.
|
|
40607
|
+
*/
|
|
40608
|
+
GetDisplayedPixels() {
|
|
40609
|
+
return this.displayedPixels;
|
|
40610
|
+
}
|
|
40052
40611
|
/**
|
|
40053
40612
|
* The untinted canvas carrying the presented frame's value in RGB and its coverage in alpha,
|
|
40054
40613
|
* or null unless the instance was constructed with produceValueCanvas.
|
|
@@ -40161,11 +40720,33 @@
|
|
|
40161
40720
|
});
|
|
40162
40721
|
}
|
|
40163
40722
|
async fetchAndTint(idx) {
|
|
40164
|
-
const
|
|
40165
|
-
|
|
40166
|
-
|
|
40167
|
-
|
|
40168
|
-
|
|
40723
|
+
const tiles = this.tiles;
|
|
40724
|
+
let buffer;
|
|
40725
|
+
let span = 0;
|
|
40726
|
+
if (tiles) {
|
|
40727
|
+
// Every tile's frame for one timestep sits contiguously, because the generator writes
|
|
40728
|
+
// frame major. So a tiled frame is still ONE range request, not one per tile.
|
|
40729
|
+
let lo = Number.MAX_SAFE_INTEGER;
|
|
40730
|
+
let hi = 0;
|
|
40731
|
+
for (const tile of tiles) {
|
|
40732
|
+
const at = tile.Frames[idx];
|
|
40733
|
+
if (!at) {
|
|
40734
|
+
continue;
|
|
40735
|
+
}
|
|
40736
|
+
lo = Math.min(lo, at.ByteOffset);
|
|
40737
|
+
hi = Math.max(hi, at.ByteOffset + at.ByteLength);
|
|
40738
|
+
}
|
|
40739
|
+
span = lo;
|
|
40740
|
+
const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
|
|
40741
|
+
buffer = await response.arrayBuffer();
|
|
40742
|
+
}
|
|
40743
|
+
else {
|
|
40744
|
+
const entry = this.frames[idx];
|
|
40745
|
+
const start = entry.ByteOffset;
|
|
40746
|
+
const end = entry.ByteOffset + entry.ByteLength - 1;
|
|
40747
|
+
const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${start}-${end}` } });
|
|
40748
|
+
buffer = await response.arrayBuffer();
|
|
40749
|
+
}
|
|
40169
40750
|
if (this.disposed) {
|
|
40170
40751
|
return;
|
|
40171
40752
|
}
|
|
@@ -40175,7 +40756,9 @@
|
|
|
40175
40756
|
if (this.disposed) {
|
|
40176
40757
|
return;
|
|
40177
40758
|
}
|
|
40178
|
-
const decoded =
|
|
40759
|
+
const decoded = tiles
|
|
40760
|
+
? await this.composeTiles(tiles, idx, buffer, span)
|
|
40761
|
+
: await this.decodeAndTint(buffer);
|
|
40179
40762
|
if (this.disposed) {
|
|
40180
40763
|
return;
|
|
40181
40764
|
}
|
|
@@ -40204,21 +40787,438 @@
|
|
|
40204
40787
|
canvas.height = image.height;
|
|
40205
40788
|
const ctx = canvas.getContext("2d");
|
|
40206
40789
|
ctx.drawImage(image, 0, 0);
|
|
40207
|
-
|
|
40790
|
+
return this.tint(ctx.getImageData(0, 0, canvas.width, canvas.height));
|
|
40791
|
+
}
|
|
40792
|
+
/*
|
|
40793
|
+
* Draws every tile of one frame into a single raster covering the drape extent.
|
|
40794
|
+
*/
|
|
40795
|
+
async composeTiles(tiles, idx, buffer, spanStart) {
|
|
40796
|
+
const target = this.compositeExtent();
|
|
40797
|
+
const size = this.compositeSize(tiles, target);
|
|
40798
|
+
const canvas = document.createElement("canvas");
|
|
40799
|
+
canvas.width = size.width;
|
|
40800
|
+
canvas.height = size.height;
|
|
40801
|
+
const ctx = canvas.getContext("2d");
|
|
40802
|
+
ctx.imageSmoothingEnabled = false;
|
|
40803
|
+
ctx.clearRect(0, 0, size.width, size.height);
|
|
40804
|
+
const images = await this.decodeSlices(buffer, spanStart, tiles.map((tile) => tile.Frames[idx]));
|
|
40805
|
+
for (let i = 0; i < tiles.length; i++) {
|
|
40806
|
+
const image = images[i];
|
|
40807
|
+
if (!image || this.disposed) {
|
|
40808
|
+
continue;
|
|
40809
|
+
}
|
|
40810
|
+
const at = this.tileRect(tiles[i], target, size);
|
|
40811
|
+
ctx.drawImage(image, at.x, at.y, at.w, at.h);
|
|
40812
|
+
}
|
|
40813
|
+
return this.tint(ctx.getImageData(0, 0, size.width, size.height));
|
|
40814
|
+
}
|
|
40815
|
+
/*
|
|
40816
|
+
* Where a composited frame is draped, which is the polygon's rectangle when there is one.
|
|
40817
|
+
*/
|
|
40818
|
+
compositeExtent() {
|
|
40819
|
+
if (this.drapeExtent) {
|
|
40820
|
+
return this.drapeExtent;
|
|
40821
|
+
}
|
|
40822
|
+
const m = this.metadata;
|
|
40823
|
+
return {
|
|
40824
|
+
West: m && m.West != null ? m.West : -180,
|
|
40825
|
+
East: m && m.East != null ? m.East : 180,
|
|
40826
|
+
South: m && m.South != null ? m.South : -90,
|
|
40827
|
+
North: m && m.North != null ? m.North : 90
|
|
40828
|
+
};
|
|
40829
|
+
}
|
|
40830
|
+
/*
|
|
40831
|
+
* Composite resolution, capped by what can be uploaded as a texture every frame.
|
|
40832
|
+
*/
|
|
40833
|
+
compositeSize(tiles, target) {
|
|
40834
|
+
let finest = Number.MAX_VALUE;
|
|
40835
|
+
for (const tile of tiles) {
|
|
40836
|
+
if (tile.TexelMetres > 0) {
|
|
40837
|
+
finest = Math.min(finest, tile.TexelMetres);
|
|
40838
|
+
}
|
|
40839
|
+
}
|
|
40840
|
+
if (!(finest > 0) || finest === Number.MAX_VALUE) {
|
|
40841
|
+
finest = 1;
|
|
40842
|
+
}
|
|
40843
|
+
const mid = (target.South + target.North) / 2 * Math.PI / 180;
|
|
40844
|
+
const widthM = Math.max((target.East - target.West) * 111320 * Math.cos(mid), 1);
|
|
40845
|
+
const heightM = Math.max((target.North - target.South) * 110574, 1);
|
|
40846
|
+
let width = Math.ceil(widthM / finest);
|
|
40847
|
+
let height = Math.ceil(heightM / finest);
|
|
40848
|
+
const longest = Math.max(width, height);
|
|
40849
|
+
if (longest > MAX_COMPOSITE_TEXELS) {
|
|
40850
|
+
const shrink = MAX_COMPOSITE_TEXELS / longest;
|
|
40851
|
+
width = Math.max(1, Math.round(width * shrink));
|
|
40852
|
+
height = Math.max(1, Math.round(height * shrink));
|
|
40853
|
+
}
|
|
40854
|
+
return { width: Math.max(1, width), height: Math.max(1, height) };
|
|
40855
|
+
}
|
|
40856
|
+
/*
|
|
40857
|
+
* Applies the ramp, borders and baseline mask to raw value pixels, whatever produced them.
|
|
40858
|
+
*/
|
|
40859
|
+
tint(imageData) {
|
|
40860
|
+
const canvas = { width: imageData.width, height: imageData.height };
|
|
40208
40861
|
// Copied before tinting: the ramp is a lerp between two colours, so the value cannot be
|
|
40209
40862
|
// recovered from the tinted pixels afterwards.
|
|
40210
40863
|
const valuePixels = this.produceValueCanvas ? new Uint8ClampedArray(imageData.data) : undefined;
|
|
40211
|
-
|
|
40864
|
+
// Stops win when supplied: they can express a band and a hidden floor, which a two
|
|
40865
|
+
// colour ramp cannot. The pair stays the fallback so an older style still draws.
|
|
40866
|
+
if (this.rampStops && this.rampStops.length > 0) {
|
|
40867
|
+
ApplyGradientStops(imageData, this.rampStops);
|
|
40868
|
+
}
|
|
40869
|
+
else {
|
|
40870
|
+
ApplyGrayscaleColorMask(imageData, this.lowColor, this.highColor);
|
|
40871
|
+
}
|
|
40872
|
+
if (this.cellBorder) {
|
|
40873
|
+
ApplyCellBorders(imageData, this.cellBorder.cellTexels, this.cellBorder.widthTexels, this.cellBorder.color);
|
|
40874
|
+
}
|
|
40212
40875
|
this.applyBaselineMask(imageData.data, valuePixels, canvas.width, canvas.height);
|
|
40213
40876
|
if (valuePixels) {
|
|
40214
40877
|
dilateValues(valuePixels, canvas.width, canvas.height, VALUE_DILATE_PASSES);
|
|
40215
40878
|
}
|
|
40216
40879
|
return { pixels: imageData.data, width: canvas.width, height: canvas.height, valuePixels };
|
|
40217
40880
|
}
|
|
40881
|
+
/*
|
|
40882
|
+
* Paints one frame's pixels onto a canvas, resampled into the drape extent when there is one.
|
|
40883
|
+
*/
|
|
40884
|
+
writeInto(canvas, pixels, dims) {
|
|
40885
|
+
const rect = this.sourceRect(dims);
|
|
40886
|
+
if (!rect) {
|
|
40887
|
+
canvas.width = dims.width;
|
|
40888
|
+
canvas.height = dims.height;
|
|
40889
|
+
const direct = canvas.getContext("2d");
|
|
40890
|
+
const image = direct.createImageData(dims.width, dims.height);
|
|
40891
|
+
image.data.set(pixels);
|
|
40892
|
+
direct.putImageData(image, 0, 0);
|
|
40893
|
+
return;
|
|
40894
|
+
}
|
|
40895
|
+
if (!this.scratch) {
|
|
40896
|
+
this.scratch = document.createElement("canvas");
|
|
40897
|
+
}
|
|
40898
|
+
this.scratch.width = dims.width;
|
|
40899
|
+
this.scratch.height = dims.height;
|
|
40900
|
+
const scratchCtx = this.scratch.getContext("2d");
|
|
40901
|
+
const image = scratchCtx.createImageData(dims.width, dims.height);
|
|
40902
|
+
image.data.set(pixels);
|
|
40903
|
+
scratchCtx.putImageData(image, 0, 0);
|
|
40904
|
+
canvas.width = dims.width;
|
|
40905
|
+
canvas.height = dims.height;
|
|
40906
|
+
const ctx = canvas.getContext("2d");
|
|
40907
|
+
// Nearest neighbour: a value texel has to survive the resample exactly, or a label reads
|
|
40908
|
+
// a blend of two cells rather than the cell it points at.
|
|
40909
|
+
ctx.imageSmoothingEnabled = false;
|
|
40910
|
+
ctx.clearRect(0, 0, dims.width, dims.height);
|
|
40911
|
+
ctx.drawImage(this.scratch, rect.x, rect.y, rect.width, rect.height, 0, 0, dims.width, dims.height);
|
|
40912
|
+
}
|
|
40913
|
+
/*
|
|
40914
|
+
* The part of the archive raster that the drape extent covers, in source pixels.
|
|
40915
|
+
*/
|
|
40916
|
+
sourceRect(dims) {
|
|
40917
|
+
const drape = this.drapeExtent;
|
|
40918
|
+
const m = this.metadata;
|
|
40919
|
+
// A composited frame was drawn into the drape extent already, so resampling it again
|
|
40920
|
+
// would apply the same correction twice.
|
|
40921
|
+
if (this.tiles) {
|
|
40922
|
+
return null;
|
|
40923
|
+
}
|
|
40924
|
+
if (!drape || !m || m.West == null || m.East == null || m.South == null || m.North == null) {
|
|
40925
|
+
return null;
|
|
40926
|
+
}
|
|
40927
|
+
const spanLon = m.East - m.West;
|
|
40928
|
+
const spanLat = m.North - m.South;
|
|
40929
|
+
if (!(spanLon > 0) || !(spanLat > 0)) {
|
|
40930
|
+
return null;
|
|
40931
|
+
}
|
|
40932
|
+
const x = (drape.West - m.West) / spanLon * dims.width;
|
|
40933
|
+
const width = (drape.East - drape.West) / spanLon * dims.width;
|
|
40934
|
+
const y = (m.North - drape.North) / spanLat * dims.height;
|
|
40935
|
+
const height = (drape.North - drape.South) / spanLat * dims.height;
|
|
40936
|
+
// A drape that already matches the archive is the common case and must not pay for a
|
|
40937
|
+
// resample, nor lose a half pixel to rounding.
|
|
40938
|
+
if (Math.abs(x) < 0.01 && Math.abs(y) < 0.01
|
|
40939
|
+
&& Math.abs(width - dims.width) < 0.01 && Math.abs(height - dims.height) < 0.01) {
|
|
40940
|
+
return null;
|
|
40941
|
+
}
|
|
40942
|
+
return { x, y, width, height };
|
|
40943
|
+
}
|
|
40944
|
+
/**
|
|
40945
|
+
* The archive's per-cell floor and ceiling rasters, once EnsureExtremes has resolved.
|
|
40946
|
+
*/
|
|
40947
|
+
GetExtremes() {
|
|
40948
|
+
return { floor: this.floorPixels, ceiling: this.ceilingPixels };
|
|
40949
|
+
}
|
|
40950
|
+
/**
|
|
40951
|
+
* Fetches and decodes the floor and ceiling rasters, at most once.
|
|
40952
|
+
*/
|
|
40953
|
+
EnsureExtremes() {
|
|
40954
|
+
if (this.extremesLoad) {
|
|
40955
|
+
return this.extremesLoad;
|
|
40956
|
+
}
|
|
40957
|
+
if (this.tiles) {
|
|
40958
|
+
this.extremesLoad = this.loadTiledExtremes();
|
|
40959
|
+
return this.extremesLoad;
|
|
40960
|
+
}
|
|
40961
|
+
const floor = this.metadata && this.metadata.Floor;
|
|
40962
|
+
const ceiling = this.metadata && this.metadata.Ceiling;
|
|
40963
|
+
if (!floor && !ceiling) {
|
|
40964
|
+
this.extremesLoad = Promise.resolve();
|
|
40965
|
+
return this.extremesLoad;
|
|
40966
|
+
}
|
|
40967
|
+
this.extremesLoad = Promise.all([
|
|
40968
|
+
floor ? this.loadImageDataAt(floor) : Promise.resolve(null),
|
|
40969
|
+
ceiling ? this.loadImageDataAt(ceiling) : Promise.resolve(null)
|
|
40970
|
+
]).then(([f, c]) => {
|
|
40971
|
+
if (this.disposed) {
|
|
40972
|
+
return;
|
|
40973
|
+
}
|
|
40974
|
+
this.floorPixels = f;
|
|
40975
|
+
this.ceilingPixels = c;
|
|
40976
|
+
}).catch((e) => {
|
|
40977
|
+
// Missing extremes cost a label its range, and must not take the animation with them.
|
|
40978
|
+
console.warn("TextureFrameSeriesAnimator: could not load the extremes rasters.", e);
|
|
40979
|
+
});
|
|
40980
|
+
return this.extremesLoad;
|
|
40981
|
+
}
|
|
40982
|
+
/*
|
|
40983
|
+
* Composites each tile's baseline mask into one covering the composited frames.
|
|
40984
|
+
*
|
|
40985
|
+
* A tiled archive publishes the mask per tile, so reading the archive level BaselineMask
|
|
40986
|
+
* finds nothing and hiding dry cells silently does nothing at all.
|
|
40987
|
+
*/
|
|
40988
|
+
async loadTiledBaselineMask() {
|
|
40989
|
+
const tiles = this.tiles;
|
|
40990
|
+
if (!tiles) {
|
|
40991
|
+
return;
|
|
40992
|
+
}
|
|
40993
|
+
const entries = tiles.map((tile) => tile.BaselineMask);
|
|
40994
|
+
let lo = Number.MAX_SAFE_INTEGER;
|
|
40995
|
+
let hi = 0;
|
|
40996
|
+
for (const at of entries) {
|
|
40997
|
+
if (!at) {
|
|
40998
|
+
continue;
|
|
40999
|
+
}
|
|
41000
|
+
lo = Math.min(lo, at.ByteOffset);
|
|
41001
|
+
hi = Math.max(hi, at.ByteOffset + at.ByteLength);
|
|
41002
|
+
}
|
|
41003
|
+
if (!(hi > lo)) {
|
|
41004
|
+
return;
|
|
41005
|
+
}
|
|
41006
|
+
try {
|
|
41007
|
+
const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
|
|
41008
|
+
const block = await response.arrayBuffer();
|
|
41009
|
+
if (this.disposed) {
|
|
41010
|
+
return;
|
|
41011
|
+
}
|
|
41012
|
+
const images = await this.decodeSlices(block, lo, entries);
|
|
41013
|
+
if (this.disposed) {
|
|
41014
|
+
return;
|
|
41015
|
+
}
|
|
41016
|
+
const target = this.compositeExtent();
|
|
41017
|
+
const size = this.compositeSize(tiles, target);
|
|
41018
|
+
const canvas = document.createElement("canvas");
|
|
41019
|
+
canvas.width = size.width;
|
|
41020
|
+
canvas.height = size.height;
|
|
41021
|
+
const ctx = canvas.getContext("2d");
|
|
41022
|
+
ctx.imageSmoothingEnabled = false;
|
|
41023
|
+
ctx.clearRect(0, 0, size.width, size.height);
|
|
41024
|
+
for (let i = 0; i < tiles.length; i++) {
|
|
41025
|
+
if (!images[i]) {
|
|
41026
|
+
continue;
|
|
41027
|
+
}
|
|
41028
|
+
const at = this.tileRect(tiles[i], target, size);
|
|
41029
|
+
ctx.drawImage(images[i], at.x, at.y, at.w, at.h);
|
|
41030
|
+
}
|
|
41031
|
+
const pixels = ctx.getImageData(0, 0, size.width, size.height).data;
|
|
41032
|
+
const flags = new Uint8Array(size.width * size.height);
|
|
41033
|
+
for (let p = 0; p < flags.length; p++) {
|
|
41034
|
+
flags[p] = pixels[p * 4 + 3] >= 128 ? 1 : 0;
|
|
41035
|
+
}
|
|
41036
|
+
this.baselineFlags = flags;
|
|
41037
|
+
this.baselineDims = { width: size.width, height: size.height };
|
|
41038
|
+
}
|
|
41039
|
+
catch (e) {
|
|
41040
|
+
console.warn("TextureFrameSeriesAnimator: could not load the tiled baseline mask.", e);
|
|
41041
|
+
}
|
|
41042
|
+
}
|
|
41043
|
+
/*
|
|
41044
|
+
* Composites each tile's floor and ceiling into rasters matching the composited frames.
|
|
41045
|
+
*
|
|
41046
|
+
* A tiled archive has no whole-extent extremes to fetch, only per-tile ones, and a label
|
|
41047
|
+
* indexes them by the same texel as the value canvas, so they have to be laid out the same way.
|
|
41048
|
+
*/
|
|
41049
|
+
async loadTiledExtremes() {
|
|
41050
|
+
const tiles = this.tiles;
|
|
41051
|
+
if (!tiles) {
|
|
41052
|
+
return;
|
|
41053
|
+
}
|
|
41054
|
+
const target = this.compositeExtent();
|
|
41055
|
+
const size = this.compositeSize(tiles, target);
|
|
41056
|
+
// One request for the whole extremes block. They sit contiguously after the frames, so
|
|
41057
|
+
// fetching per tile would be 164 round trips for an 82 tile pyramid.
|
|
41058
|
+
let lo = Number.MAX_SAFE_INTEGER;
|
|
41059
|
+
let hi = 0;
|
|
41060
|
+
for (const tile of tiles) {
|
|
41061
|
+
for (const at of [tile.Floor, tile.Ceiling]) {
|
|
41062
|
+
if (!at) {
|
|
41063
|
+
continue;
|
|
41064
|
+
}
|
|
41065
|
+
lo = Math.min(lo, at.ByteOffset);
|
|
41066
|
+
hi = Math.max(hi, at.ByteOffset + at.ByteLength);
|
|
41067
|
+
}
|
|
41068
|
+
}
|
|
41069
|
+
if (!(hi > lo)) {
|
|
41070
|
+
return;
|
|
41071
|
+
}
|
|
41072
|
+
const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${lo}-${hi - 1}` } });
|
|
41073
|
+
const block = await response.arrayBuffer();
|
|
41074
|
+
if (this.disposed) {
|
|
41075
|
+
return;
|
|
41076
|
+
}
|
|
41077
|
+
const draw = async (pick) => {
|
|
41078
|
+
const images = await this.decodeSlices(block, lo, tiles.map(pick));
|
|
41079
|
+
if (this.disposed) {
|
|
41080
|
+
return null;
|
|
41081
|
+
}
|
|
41082
|
+
const canvas = document.createElement("canvas");
|
|
41083
|
+
canvas.width = size.width;
|
|
41084
|
+
canvas.height = size.height;
|
|
41085
|
+
const ctx = canvas.getContext("2d");
|
|
41086
|
+
ctx.imageSmoothingEnabled = false;
|
|
41087
|
+
ctx.clearRect(0, 0, size.width, size.height);
|
|
41088
|
+
let drew = false;
|
|
41089
|
+
for (let i = 0; i < tiles.length; i++) {
|
|
41090
|
+
const image = images[i];
|
|
41091
|
+
if (!image) {
|
|
41092
|
+
continue;
|
|
41093
|
+
}
|
|
41094
|
+
const at = this.tileRect(tiles[i], target, size);
|
|
41095
|
+
ctx.drawImage(image, at.x, at.y, at.w, at.h);
|
|
41096
|
+
drew = true;
|
|
41097
|
+
}
|
|
41098
|
+
return drew ? ctx.getImageData(0, 0, size.width, size.height) : null;
|
|
41099
|
+
};
|
|
41100
|
+
try {
|
|
41101
|
+
this.floorPixels = await draw((tile) => tile.Floor);
|
|
41102
|
+
this.ceilingPixels = await draw((tile) => tile.Ceiling);
|
|
41103
|
+
}
|
|
41104
|
+
catch (e) {
|
|
41105
|
+
console.warn("TextureFrameSeriesAnimator: could not load the tiled extremes.", e);
|
|
41106
|
+
}
|
|
41107
|
+
}
|
|
41108
|
+
/*
|
|
41109
|
+
* Where one tile lands on the composite, snapped to whole pixels.
|
|
41110
|
+
*
|
|
41111
|
+
* Rounding each edge rather than the origin and size is what makes neighbours meet exactly:
|
|
41112
|
+
* a tile's right edge and the next tile's left edge round to the same pixel, so no seam of
|
|
41113
|
+
* background shows between them.
|
|
41114
|
+
*/
|
|
41115
|
+
tileRect(tile, target, size) {
|
|
41116
|
+
const spanLon = target.East - target.West;
|
|
41117
|
+
const spanLat = target.North - target.South;
|
|
41118
|
+
const x0 = Math.round((tile.West - target.West) / spanLon * size.width);
|
|
41119
|
+
const x1 = Math.round((tile.East - target.West) / spanLon * size.width);
|
|
41120
|
+
const y0 = Math.round((target.North - tile.North) / spanLat * size.height);
|
|
41121
|
+
const y1 = Math.round((target.North - tile.South) / spanLat * size.height);
|
|
41122
|
+
return { x: x0, y: y0, w: Math.max(1, x1 - x0), h: Math.max(1, y1 - y0) };
|
|
41123
|
+
}
|
|
41124
|
+
/*
|
|
41125
|
+
* Decodes many tile images at once.
|
|
41126
|
+
*
|
|
41127
|
+
* Serially awaiting each decode makes a frame cost as many round trips through the image
|
|
41128
|
+
* decoder as there are tiles, which for an 82 tile pyramid is seconds rather than one.
|
|
41129
|
+
*/
|
|
41130
|
+
decodeSlices(block, base, entries) {
|
|
41131
|
+
return Promise.all(entries.map(async (at) => {
|
|
41132
|
+
if (!at) {
|
|
41133
|
+
return null;
|
|
41134
|
+
}
|
|
41135
|
+
const slice = block.slice(at.ByteOffset - base, at.ByteOffset - base + at.ByteLength);
|
|
41136
|
+
const objectUrl = URL.createObjectURL(new Blob([slice], { type: "image/png" }));
|
|
41137
|
+
try {
|
|
41138
|
+
return await loadImage(objectUrl);
|
|
41139
|
+
}
|
|
41140
|
+
catch {
|
|
41141
|
+
return null;
|
|
41142
|
+
}
|
|
41143
|
+
finally {
|
|
41144
|
+
URL.revokeObjectURL(objectUrl);
|
|
41145
|
+
}
|
|
41146
|
+
}));
|
|
41147
|
+
}
|
|
41148
|
+
/*
|
|
41149
|
+
* Range GETs one entry and decodes it to an image.
|
|
41150
|
+
*/
|
|
41151
|
+
async loadImageAt(entry) {
|
|
41152
|
+
const start = entry.ByteOffset;
|
|
41153
|
+
const end = entry.ByteOffset + entry.ByteLength - 1;
|
|
41154
|
+
const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${start}-${end}` } });
|
|
41155
|
+
const buffer = await response.arrayBuffer();
|
|
41156
|
+
const objectUrl = URL.createObjectURL(new Blob([buffer], { type: "image/png" }));
|
|
41157
|
+
try {
|
|
41158
|
+
return await loadImage(objectUrl);
|
|
41159
|
+
}
|
|
41160
|
+
finally {
|
|
41161
|
+
URL.revokeObjectURL(objectUrl);
|
|
41162
|
+
}
|
|
41163
|
+
}
|
|
41164
|
+
/*
|
|
41165
|
+
* Puts a static raster through the same resample the frames get.
|
|
41166
|
+
*/
|
|
41167
|
+
remap(image) {
|
|
41168
|
+
if (!image) {
|
|
41169
|
+
return null;
|
|
41170
|
+
}
|
|
41171
|
+
const rect = this.sourceRect({ width: image.width, height: image.height });
|
|
41172
|
+
if (!rect) {
|
|
41173
|
+
return image;
|
|
41174
|
+
}
|
|
41175
|
+
const from = document.createElement("canvas");
|
|
41176
|
+
from.width = image.width;
|
|
41177
|
+
from.height = image.height;
|
|
41178
|
+
from.getContext("2d").putImageData(image, 0, 0);
|
|
41179
|
+
const to = document.createElement("canvas");
|
|
41180
|
+
to.width = image.width;
|
|
41181
|
+
to.height = image.height;
|
|
41182
|
+
const ctx = to.getContext("2d");
|
|
41183
|
+
ctx.imageSmoothingEnabled = false;
|
|
41184
|
+
ctx.clearRect(0, 0, image.width, image.height);
|
|
41185
|
+
ctx.drawImage(from, rect.x, rect.y, rect.width, rect.height, 0, 0, image.width, image.height);
|
|
41186
|
+
return ctx.getImageData(0, 0, image.width, image.height);
|
|
41187
|
+
}
|
|
41188
|
+
/*
|
|
41189
|
+
* Range GETs one entry out of the blob and decodes it to pixels.
|
|
41190
|
+
*/
|
|
41191
|
+
async loadImageDataAt(entry) {
|
|
41192
|
+
const start = entry.ByteOffset;
|
|
41193
|
+
const end = entry.ByteOffset + entry.ByteLength - 1;
|
|
41194
|
+
const response = await fetch(this.archiveUrl, { headers: { Range: `bytes=${start}-${end}` } });
|
|
41195
|
+
const buffer = await response.arrayBuffer();
|
|
41196
|
+
const blob = new Blob([buffer], { type: "image/png" });
|
|
41197
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
41198
|
+
let image;
|
|
41199
|
+
try {
|
|
41200
|
+
image = await loadImage(objectUrl);
|
|
41201
|
+
}
|
|
41202
|
+
finally {
|
|
41203
|
+
URL.revokeObjectURL(objectUrl);
|
|
41204
|
+
}
|
|
41205
|
+
const canvas = document.createElement("canvas");
|
|
41206
|
+
canvas.width = image.width;
|
|
41207
|
+
canvas.height = image.height;
|
|
41208
|
+
const ctx = canvas.getContext("2d");
|
|
41209
|
+
ctx.drawImage(image, 0, 0);
|
|
41210
|
+
return ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
41211
|
+
}
|
|
40218
41212
|
/**
|
|
40219
41213
|
* Fetches and decodes the archive's static baseline mask, at most once.
|
|
40220
41214
|
*/
|
|
40221
41215
|
ensureBaselineMask() {
|
|
41216
|
+
if (this.maskBaseline && this.tiles) {
|
|
41217
|
+
if (!this.baselineLoad) {
|
|
41218
|
+
this.baselineLoad = this.loadTiledBaselineMask();
|
|
41219
|
+
}
|
|
41220
|
+
return this.baselineLoad;
|
|
41221
|
+
}
|
|
40222
41222
|
if (!this.maskBaseline || !this.baselineMaskEntry) {
|
|
40223
41223
|
return Promise.resolve();
|
|
40224
41224
|
}
|
|
@@ -40331,24 +41331,17 @@
|
|
|
40331
41331
|
presentPixels(pixels, dims, valuePixels) {
|
|
40332
41332
|
this.poolIdx = 1 - this.poolIdx;
|
|
40333
41333
|
const target = this.pool[this.poolIdx];
|
|
40334
|
-
target
|
|
40335
|
-
target.height = dims.height;
|
|
40336
|
-
const ctx = target.getContext("2d");
|
|
40337
|
-
const imgData = ctx.createImageData(dims.width, dims.height);
|
|
40338
|
-
imgData.data.set(pixels);
|
|
40339
|
-
ctx.putImageData(imgData, 0, 0);
|
|
41334
|
+
this.writeInto(target, pixels, dims);
|
|
40340
41335
|
this.displayedPixels = pixels;
|
|
40341
41336
|
if (this.valueCanvas && valuePixels) {
|
|
40342
|
-
this.valueCanvas
|
|
40343
|
-
this.valueCanvas.height = dims.height;
|
|
40344
|
-
const valueCtx = this.valueCanvas.getContext("2d");
|
|
40345
|
-
const valueImgData = valueCtx.createImageData(dims.width, dims.height);
|
|
40346
|
-
valueImgData.data.set(valuePixels);
|
|
40347
|
-
valueCtx.putImageData(valueImgData, 0, 0);
|
|
41337
|
+
this.writeInto(this.valueCanvas, valuePixels, dims);
|
|
40348
41338
|
this.displayedValuePixels = valuePixels;
|
|
40349
41339
|
this.valueDims = dims;
|
|
40350
41340
|
}
|
|
40351
41341
|
this.presentVersion++;
|
|
41342
|
+
if (this.viewer && this.viewer.scene && this.viewer.scene.requestRenderMode) {
|
|
41343
|
+
this.viewer.scene.requestRender();
|
|
41344
|
+
}
|
|
40352
41345
|
}
|
|
40353
41346
|
}
|
|
40354
41347
|
TextureFrameSeriesAnimator.Animator = Animator;
|
|
@@ -40360,6 +41353,684 @@
|
|
|
40360
41353
|
}
|
|
40361
41354
|
})(TextureFrameSeriesAnimator || (TextureFrameSeriesAnimator = {}));
|
|
40362
41355
|
|
|
41356
|
+
(function (TextureValueLabels) {
|
|
41357
|
+
// Off by default: a caller can still set a floor, but thinning is the mechanism that keeps a
|
|
41358
|
+
// distant view readable.
|
|
41359
|
+
const DEFAULT_MIN_CELL_PIXELS = 0;
|
|
41360
|
+
const MIN_VISIBLE_PIXELS = 60;
|
|
41361
|
+
const MAX_STRIDE = 512;
|
|
41362
|
+
const STRIDE_HYSTERESIS = 1.4;
|
|
41363
|
+
const DEFAULT_MIN_SPACING_PIXELS = 78;
|
|
41364
|
+
const DEFAULT_MAX_LABELS = 240;
|
|
41365
|
+
const LABEL_RADIUS = 4;
|
|
41366
|
+
const LABEL_FONT = "600 11px ui-monospace, SFMono-Regular, Menlo, monospace";
|
|
41367
|
+
// Clear space either side of a label box, so neighbours read as separate.
|
|
41368
|
+
const LABEL_GAP_PIXELS = 16;
|
|
41369
|
+
const LABEL_ANCHOR_COLOR = "rgba(255,255,255,0.85)";
|
|
41370
|
+
// 8 rather than 5: below this a cell reads as texture rather than as a boundary, and it also
|
|
41371
|
+
// bounds the worst case, since a full screen of 8 px cells fits inside MAX_GRID_CELLS.
|
|
41372
|
+
const DEFAULT_MIN_GRID_CELL_PIXELS = 8;
|
|
41373
|
+
const DEFAULT_GRID_WIDTH_PIXELS = 1;
|
|
41374
|
+
// Enough for a full screen at the minimum legible cell size, with headroom for an oblique view
|
|
41375
|
+
// whose texel window is larger than the screen. The budget is shared across tiles, so sizing it
|
|
41376
|
+
// too tightly made the grid depend on which tile happened to be walked first.
|
|
41377
|
+
const MAX_GRID_CELLS = 40000;
|
|
41378
|
+
// Probes per side when hunting a labellable cell inside one lattice block.
|
|
41379
|
+
const MAX_LABEL_PROBES_PER_SIDE = 8;
|
|
41380
|
+
const TILE_OUTLINE_ALPHA = 0.45;
|
|
41381
|
+
// Cells per grid square at the coarsest. Beyond this a tile is outlined instead.
|
|
41382
|
+
const MAX_GRID_STRIDE = 256;
|
|
41383
|
+
const LINE_HEIGHT = 13;
|
|
41384
|
+
/**
|
|
41385
|
+
* A clip outline from a projected ring, or null when the ring cannot be drawn.
|
|
41386
|
+
* @param projected one entry per ring vertex, null where the vertex has no window coordinate
|
|
41387
|
+
*/
|
|
41388
|
+
function ClipOutline(projected) {
|
|
41389
|
+
if (projected.length < 3) {
|
|
41390
|
+
return null;
|
|
41391
|
+
}
|
|
41392
|
+
const out = [];
|
|
41393
|
+
for (const at of projected) {
|
|
41394
|
+
if (!at) {
|
|
41395
|
+
return null;
|
|
41396
|
+
}
|
|
41397
|
+
out.push(at);
|
|
41398
|
+
}
|
|
41399
|
+
return out;
|
|
41400
|
+
}
|
|
41401
|
+
TextureValueLabels.ClipOutline = ClipOutline;
|
|
41402
|
+
/**
|
|
41403
|
+
* How many cells apart labels should sit, as a power of two.
|
|
41404
|
+
* @param options cellPixels is one cell's size on screen, held is the stride already in use
|
|
41405
|
+
*/
|
|
41406
|
+
function ChooseLabelStride(options) {
|
|
41407
|
+
const { cellPixels, visibleCols, visibleRows, maxLabels, minSpacingPixels } = options;
|
|
41408
|
+
const fits = (s, slack) => cellPixels * s >= minSpacingPixels / slack
|
|
41409
|
+
&& Math.ceil(visibleCols / s) * Math.ceil(visibleRows / s) <= maxLabels * slack;
|
|
41410
|
+
let stride = 1;
|
|
41411
|
+
while (stride < MAX_STRIDE && !fits(stride, 1)) {
|
|
41412
|
+
stride *= 2;
|
|
41413
|
+
}
|
|
41414
|
+
const held = options.held;
|
|
41415
|
+
if (held && fits(held, STRIDE_HYSTERESIS)) {
|
|
41416
|
+
if (held === stride / 2 && cellPixels * held >= minSpacingPixels / STRIDE_HYSTERESIS) {
|
|
41417
|
+
return held;
|
|
41418
|
+
}
|
|
41419
|
+
if (held === stride * 2 && cellPixels * stride < minSpacingPixels * STRIDE_HYSTERESIS) {
|
|
41420
|
+
return held;
|
|
41421
|
+
}
|
|
41422
|
+
}
|
|
41423
|
+
return stride;
|
|
41424
|
+
}
|
|
41425
|
+
TextureValueLabels.ChooseLabelStride = ChooseLabelStride;
|
|
41426
|
+
class Labels {
|
|
41427
|
+
constructor(options) {
|
|
41428
|
+
this.source = null;
|
|
41429
|
+
this.painted = null;
|
|
41430
|
+
this.floor = null;
|
|
41431
|
+
this.ceiling = null;
|
|
41432
|
+
this.disposed = false;
|
|
41433
|
+
this.drawn = 0;
|
|
41434
|
+
this.stride = 0;
|
|
41435
|
+
this.viewer = options.viewer;
|
|
41436
|
+
this.extent = options.extent;
|
|
41437
|
+
this.settings = options.settings || {};
|
|
41438
|
+
this.metadata = options.metadata;
|
|
41439
|
+
this.tiles = options.tiles;
|
|
41440
|
+
this.clipRing = options.clipRing;
|
|
41441
|
+
this.canvas = document.createElement("canvas");
|
|
41442
|
+
this.canvas.style.position = "absolute";
|
|
41443
|
+
this.canvas.style.inset = "0";
|
|
41444
|
+
this.canvas.style.pointerEvents = "none";
|
|
41445
|
+
const parent = this.viewer.canvas.parentElement;
|
|
41446
|
+
if (parent) {
|
|
41447
|
+
parent.appendChild(this.canvas);
|
|
41448
|
+
}
|
|
41449
|
+
this.ctx = this.canvas.getContext("2d");
|
|
41450
|
+
}
|
|
41451
|
+
SetExtent(extent) {
|
|
41452
|
+
this.extent = extent;
|
|
41453
|
+
}
|
|
41454
|
+
/**
|
|
41455
|
+
* The current frame's decoded values, as the animator's value canvas already provides them.
|
|
41456
|
+
* @param source value pixels, one texel per cell
|
|
41457
|
+
*/
|
|
41458
|
+
SetSource(source) {
|
|
41459
|
+
this.source = source;
|
|
41460
|
+
}
|
|
41461
|
+
/**
|
|
41462
|
+
* The tinted pixels of the same frame, so the grid can skip cells the ramp painted nothing in.
|
|
41463
|
+
* @param painted RGBA of the presented frame, or null to fall back to coverage alone
|
|
41464
|
+
*/
|
|
41465
|
+
SetPainted(painted) {
|
|
41466
|
+
this.painted = painted;
|
|
41467
|
+
}
|
|
41468
|
+
/**
|
|
41469
|
+
* The archive's per-cell extremes, when it publishes them. Without these a label can still
|
|
41470
|
+
* print the current reading, just not the range behind it.
|
|
41471
|
+
*/
|
|
41472
|
+
SetExtremes(floor, ceiling) {
|
|
41473
|
+
this.floor = floor;
|
|
41474
|
+
this.ceiling = ceiling;
|
|
41475
|
+
}
|
|
41476
|
+
GetDrawnCount() {
|
|
41477
|
+
return this.drawn;
|
|
41478
|
+
}
|
|
41479
|
+
Clear() {
|
|
41480
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
41481
|
+
this.drawn = 0;
|
|
41482
|
+
}
|
|
41483
|
+
Dispose() {
|
|
41484
|
+
this.disposed = true;
|
|
41485
|
+
if (this.canvas.parentElement) {
|
|
41486
|
+
this.canvas.parentElement.removeChild(this.canvas);
|
|
41487
|
+
}
|
|
41488
|
+
}
|
|
41489
|
+
/**
|
|
41490
|
+
* Redraws every visible label. Cheap enough to call per rendered frame, because the cell
|
|
41491
|
+
* count it considers is bounded by the view rather than by the texture.
|
|
41492
|
+
*/
|
|
41493
|
+
Render() {
|
|
41494
|
+
var _a, _b, _c;
|
|
41495
|
+
if (this.disposed || !this.source) {
|
|
41496
|
+
return;
|
|
41497
|
+
}
|
|
41498
|
+
const view = this.viewer.canvas;
|
|
41499
|
+
if (this.canvas.width !== view.clientWidth || this.canvas.height !== view.clientHeight) {
|
|
41500
|
+
this.canvas.width = view.clientWidth;
|
|
41501
|
+
this.canvas.height = view.clientHeight;
|
|
41502
|
+
}
|
|
41503
|
+
this.Clear();
|
|
41504
|
+
const cols = this.source.width;
|
|
41505
|
+
const rows = this.source.height;
|
|
41506
|
+
const window = this.visibleTexels(cols, rows);
|
|
41507
|
+
if (!window) {
|
|
41508
|
+
return;
|
|
41509
|
+
}
|
|
41510
|
+
const midCol = (window.col0 + window.col1) / 2;
|
|
41511
|
+
const midRow = (window.row0 + window.row1) / 2;
|
|
41512
|
+
const a = this.project(this.lonAt(midCol, cols), this.latAt(midRow, rows));
|
|
41513
|
+
const b = this.project(this.lonAt(midCol + 1, cols), this.latAt(midRow + 1, rows));
|
|
41514
|
+
if (!a || !b) {
|
|
41515
|
+
return;
|
|
41516
|
+
}
|
|
41517
|
+
const cellPixels = Math.min(Math.abs(b.x - a.x), Math.abs(b.y - a.y));
|
|
41518
|
+
// Ahead of the label threshold on purpose: a grid still reads at a cell size where a
|
|
41519
|
+
// number no longer fits.
|
|
41520
|
+
this.drawGrid(window, cols, rows, cellPixels);
|
|
41521
|
+
if (cellPixels < ((_a = this.settings.minCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_CELL_PIXELS)) {
|
|
41522
|
+
return;
|
|
41523
|
+
}
|
|
41524
|
+
const visibleWidth = cellPixels * (window.col1 - window.col0 + 1);
|
|
41525
|
+
const visibleHeight = cellPixels * (window.row1 - window.row0 + 1);
|
|
41526
|
+
if (Math.max(visibleWidth, visibleHeight) < MIN_VISIBLE_PIXELS) {
|
|
41527
|
+
return;
|
|
41528
|
+
}
|
|
41529
|
+
const parts = this.settings.parts || ["value", "floor", "ceiling"];
|
|
41530
|
+
this.ctx.font = LABEL_FONT;
|
|
41531
|
+
this.ctx.textAlign = "center";
|
|
41532
|
+
const maxLabels = (_b = this.settings.maxLabels) !== null && _b !== void 0 ? _b : DEFAULT_MAX_LABELS;
|
|
41533
|
+
this.stride = ChooseLabelStride({
|
|
41534
|
+
cellPixels,
|
|
41535
|
+
visibleCols: window.col1 - window.col0 + 1,
|
|
41536
|
+
visibleRows: window.row1 - window.row0 + 1,
|
|
41537
|
+
maxLabels,
|
|
41538
|
+
// Measured, not assumed: the spacing that keeps labels apart is whatever the widest
|
|
41539
|
+
// one is, so changing what a label says cannot quietly start overlapping them.
|
|
41540
|
+
minSpacingPixels: Math.max((_c = this.settings.minSpacingPixels) !== null && _c !== void 0 ? _c : DEFAULT_MIN_SPACING_PIXELS, this.widestLabel(parts)),
|
|
41541
|
+
held: this.stride
|
|
41542
|
+
});
|
|
41543
|
+
const stride = this.stride;
|
|
41544
|
+
const firstRow = Math.floor(window.row0 / stride) * stride;
|
|
41545
|
+
const firstCol = Math.floor(window.col0 / stride) * stride;
|
|
41546
|
+
for (let row = firstRow; row <= window.row1 && this.drawn < maxLabels; row += stride) {
|
|
41547
|
+
for (let col = firstCol; col <= window.col1 && this.drawn < maxLabels; col += stride) {
|
|
41548
|
+
const found = this.readNear(col, row, cols, rows, stride);
|
|
41549
|
+
if (!found) {
|
|
41550
|
+
continue;
|
|
41551
|
+
}
|
|
41552
|
+
const point = this.project(this.lonAt(found.col, cols), this.latAt(found.row, rows));
|
|
41553
|
+
if (!point || point.x < 0 || point.y < 0
|
|
41554
|
+
|| point.x > this.canvas.width || point.y > this.canvas.height) {
|
|
41555
|
+
continue;
|
|
41556
|
+
}
|
|
41557
|
+
this.drawLabel(point, found.reading, parts, this.cellOutline(found.col, found.row, cols, rows));
|
|
41558
|
+
}
|
|
41559
|
+
}
|
|
41560
|
+
}
|
|
41561
|
+
/*
|
|
41562
|
+
* Outlines each covered cell, from a projected lattice of its corners.
|
|
41563
|
+
*/
|
|
41564
|
+
drawGrid(window, cols, rows, cellPixels) {
|
|
41565
|
+
var _a, _b;
|
|
41566
|
+
const color = this.settings.gridColor;
|
|
41567
|
+
if (!color) {
|
|
41568
|
+
return;
|
|
41569
|
+
}
|
|
41570
|
+
if (this.tiles && this.tiles.length) {
|
|
41571
|
+
this.ctx.save();
|
|
41572
|
+
this.applyRingClip();
|
|
41573
|
+
this.drawTileGrid(color);
|
|
41574
|
+
this.ctx.restore();
|
|
41575
|
+
return;
|
|
41576
|
+
}
|
|
41577
|
+
if (cellPixels < ((_a = this.settings.minGridCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_GRID_CELL_PIXELS)) {
|
|
41578
|
+
return;
|
|
41579
|
+
}
|
|
41580
|
+
// Corners are shared by four cells, so projecting the lattice once rather than per cell
|
|
41581
|
+
// cuts the work by about four and keeps neighbouring outlines exactly coincident.
|
|
41582
|
+
const wide = window.col1 - window.col0 + 1;
|
|
41583
|
+
const high = window.row1 - window.row0 + 1;
|
|
41584
|
+
if (wide * high > MAX_GRID_CELLS) {
|
|
41585
|
+
return;
|
|
41586
|
+
}
|
|
41587
|
+
const lattice = new Array((wide + 1) * (high + 1));
|
|
41588
|
+
for (let r = 0; r <= high; r++) {
|
|
41589
|
+
for (let c = 0; c <= wide; c++) {
|
|
41590
|
+
// Corners sit half a texel out from the centres lonAt/latAt return.
|
|
41591
|
+
const lon = this.lonAt(window.col0 + c - 0.5, cols);
|
|
41592
|
+
const lat = this.latAt(window.row0 + r - 0.5, rows);
|
|
41593
|
+
lattice[r * (wide + 1) + c] = this.project(lon, lat);
|
|
41594
|
+
}
|
|
41595
|
+
}
|
|
41596
|
+
const path = new Path2D();
|
|
41597
|
+
let drew = false;
|
|
41598
|
+
for (let r = 0; r < high; r++) {
|
|
41599
|
+
for (let c = 0; c < wide; c++) {
|
|
41600
|
+
if (!this.isCovered(window.col0 + c, window.row0 + r, cols)) {
|
|
41601
|
+
continue;
|
|
41602
|
+
}
|
|
41603
|
+
const tl = lattice[r * (wide + 1) + c];
|
|
41604
|
+
const tr = lattice[r * (wide + 1) + c + 1];
|
|
41605
|
+
const bl = lattice[(r + 1) * (wide + 1) + c];
|
|
41606
|
+
const br = lattice[(r + 1) * (wide + 1) + c + 1];
|
|
41607
|
+
if (!tl || !tr || !bl || !br) {
|
|
41608
|
+
continue;
|
|
41609
|
+
}
|
|
41610
|
+
path.moveTo(tl.x, tl.y);
|
|
41611
|
+
path.lineTo(tr.x, tr.y);
|
|
41612
|
+
path.lineTo(br.x, br.y);
|
|
41613
|
+
path.lineTo(bl.x, bl.y);
|
|
41614
|
+
path.closePath();
|
|
41615
|
+
drew = true;
|
|
41616
|
+
}
|
|
41617
|
+
}
|
|
41618
|
+
if (!drew) {
|
|
41619
|
+
return;
|
|
41620
|
+
}
|
|
41621
|
+
this.ctx.strokeStyle = color;
|
|
41622
|
+
this.ctx.lineWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
|
|
41623
|
+
this.ctx.stroke(path);
|
|
41624
|
+
+" alpha" + this.ctx.globalAlpha;
|
|
41625
|
+
}
|
|
41626
|
+
/*
|
|
41627
|
+
* Restricts drawing to the polygon, so nothing is ruled over ground it does not cover.
|
|
41628
|
+
*/
|
|
41629
|
+
applyRingClip() {
|
|
41630
|
+
const ring = this.clipRing;
|
|
41631
|
+
if (!ring || ring.length < 3) {
|
|
41632
|
+
return;
|
|
41633
|
+
}
|
|
41634
|
+
const outline = ClipOutline(ring.map(point => this.project(point.lon, point.lat)));
|
|
41635
|
+
if (!outline) {
|
|
41636
|
+
return;
|
|
41637
|
+
}
|
|
41638
|
+
const path = new Path2D();
|
|
41639
|
+
path.moveTo(outline[0].x, outline[0].y);
|
|
41640
|
+
for (let i = 1; i < outline.length; i++) {
|
|
41641
|
+
path.lineTo(outline[i].x, outline[i].y);
|
|
41642
|
+
}
|
|
41643
|
+
path.closePath();
|
|
41644
|
+
this.ctx.clip(path);
|
|
41645
|
+
}
|
|
41646
|
+
/*
|
|
41647
|
+
* The cell of whichever tile covers a point, as screen corners.
|
|
41648
|
+
*/
|
|
41649
|
+
tileCellOutline(lon, lat) {
|
|
41650
|
+
for (const tile of this.tiles) {
|
|
41651
|
+
if (lon < tile.West || lon > tile.East || lat < tile.South || lat > tile.North) {
|
|
41652
|
+
continue;
|
|
41653
|
+
}
|
|
41654
|
+
const c = (Math.min(tile.ResolutionX - 1, Math.max(0, Math.floor((lon - tile.West) / (tile.East - tile.West) * tile.ResolutionX))));
|
|
41655
|
+
const r = (Math.min(tile.ResolutionY - 1, Math.max(0, Math.floor((tile.North - lat) / (tile.North - tile.South) * tile.ResolutionY))));
|
|
41656
|
+
const w0 = tile.West + (c / tile.ResolutionX) * (tile.East - tile.West);
|
|
41657
|
+
const w1 = tile.West + ((c + 1) / tile.ResolutionX) * (tile.East - tile.West);
|
|
41658
|
+
const n0 = tile.North - (r / tile.ResolutionY) * (tile.North - tile.South);
|
|
41659
|
+
const n1 = tile.North - ((r + 1) / tile.ResolutionY) * (tile.North - tile.South);
|
|
41660
|
+
const corners = [
|
|
41661
|
+
this.project(w0, n0), this.project(w1, n0),
|
|
41662
|
+
this.project(w1, n1), this.project(w0, n1)
|
|
41663
|
+
];
|
|
41664
|
+
return corners.every((q) => q) ? corners : null;
|
|
41665
|
+
}
|
|
41666
|
+
return null;
|
|
41667
|
+
}
|
|
41668
|
+
/*
|
|
41669
|
+
* Outlines each tile's own cells, so cell size still shows where the mesh is refined.
|
|
41670
|
+
*/
|
|
41671
|
+
drawTileGrid(color) {
|
|
41672
|
+
var _a, _b, _c;
|
|
41673
|
+
const view = this.viewer.camera.computeViewRectangle(Cesium.Ellipsoid.WGS84);
|
|
41674
|
+
if (!view) {
|
|
41675
|
+
return;
|
|
41676
|
+
}
|
|
41677
|
+
const west = Cesium.Math.toDegrees(view.west);
|
|
41678
|
+
const east = Cesium.Math.toDegrees(view.east);
|
|
41679
|
+
const south = Cesium.Math.toDegrees(view.south);
|
|
41680
|
+
const north = Cesium.Math.toDegrees(view.north);
|
|
41681
|
+
const minCell = (_a = this.settings.minGridCellPixels) !== null && _a !== void 0 ? _a : DEFAULT_MIN_GRID_CELL_PIXELS;
|
|
41682
|
+
const focus = this.groundAtScreenCentre();
|
|
41683
|
+
const path = new Path2D();
|
|
41684
|
+
// A tile too fine to rule, or one the budget cannot afford, contributes its own outline
|
|
41685
|
+
// instead of vanishing. Dropping such tiles entirely is what made whole regions blink out.
|
|
41686
|
+
const coarse = new Path2D();
|
|
41687
|
+
let drew = false;
|
|
41688
|
+
let drewCoarse = false;
|
|
41689
|
+
const outlineTile = (tile) => {
|
|
41690
|
+
const tl = this.project(tile.West, tile.North);
|
|
41691
|
+
const tr = this.project(tile.East, tile.North);
|
|
41692
|
+
const br = this.project(tile.East, tile.South);
|
|
41693
|
+
const bl = this.project(tile.West, tile.South);
|
|
41694
|
+
if (!tl || !tr || !br || !bl) {
|
|
41695
|
+
return;
|
|
41696
|
+
}
|
|
41697
|
+
coarse.moveTo(tl.x, tl.y);
|
|
41698
|
+
coarse.lineTo(tr.x, tr.y);
|
|
41699
|
+
coarse.lineTo(br.x, br.y);
|
|
41700
|
+
coarse.lineTo(bl.x, bl.y);
|
|
41701
|
+
coarse.closePath();
|
|
41702
|
+
drewCoarse = true;
|
|
41703
|
+
};
|
|
41704
|
+
// Planned for every visible tile BEFORE any is drawn.
|
|
41705
|
+
const planned = [];
|
|
41706
|
+
for (const tile of this.tiles) {
|
|
41707
|
+
if (tile.East < west || tile.West > east || tile.North < south || tile.South > north) {
|
|
41708
|
+
continue;
|
|
41709
|
+
}
|
|
41710
|
+
const c0 = Math.max(0, Math.floor((Math.max(west, tile.West) - tile.West)
|
|
41711
|
+
/ (tile.East - tile.West) * tile.ResolutionX));
|
|
41712
|
+
const c1 = Math.min(tile.ResolutionX, Math.ceil((Math.min(east, tile.East) - tile.West)
|
|
41713
|
+
/ (tile.East - tile.West) * tile.ResolutionX));
|
|
41714
|
+
const r0 = Math.max(0, Math.floor((tile.North - Math.min(north, tile.North))
|
|
41715
|
+
/ (tile.North - tile.South) * tile.ResolutionY));
|
|
41716
|
+
const r1 = Math.min(tile.ResolutionY, Math.ceil((tile.North - Math.max(south, tile.South))
|
|
41717
|
+
/ (tile.North - tile.South) * tile.ResolutionY));
|
|
41718
|
+
if (c1 <= c0 || r1 <= r0) {
|
|
41719
|
+
continue;
|
|
41720
|
+
}
|
|
41721
|
+
const focusC = focus
|
|
41722
|
+
? Math.round((focus.lon - tile.West) / (tile.East - tile.West) * tile.ResolutionX)
|
|
41723
|
+
: Math.floor((c0 + c1) / 2);
|
|
41724
|
+
const focusR = focus
|
|
41725
|
+
? Math.round((tile.North - focus.lat) / (tile.North - tile.South) * tile.ResolutionY)
|
|
41726
|
+
: Math.floor((r0 + r1) / 2);
|
|
41727
|
+
const midC = Math.min(c1 - 1, Math.max(c0, focusC));
|
|
41728
|
+
const midR = Math.min(r1 - 1, Math.max(r0, focusR));
|
|
41729
|
+
const cellLon = (tile.East - tile.West) / tile.ResolutionX;
|
|
41730
|
+
const cellLat = (tile.North - tile.South) / tile.ResolutionY;
|
|
41731
|
+
const at = this.project(tile.West + midC * cellLon, tile.North - midR * cellLat);
|
|
41732
|
+
const next = (this.project(tile.West + (midC + 1) * cellLon, tile.North - (midR + 1) * cellLat));
|
|
41733
|
+
if (!at || !next) {
|
|
41734
|
+
outlineTile(tile);
|
|
41735
|
+
continue;
|
|
41736
|
+
}
|
|
41737
|
+
const onScreen = Math.min(Math.abs(next.x - at.x), Math.abs(next.y - at.y));
|
|
41738
|
+
let step = 1;
|
|
41739
|
+
while (step < MAX_GRID_STRIDE && onScreen * step < minCell) {
|
|
41740
|
+
step *= 2;
|
|
41741
|
+
}
|
|
41742
|
+
if (onScreen * step < minCell) {
|
|
41743
|
+
outlineTile(tile);
|
|
41744
|
+
continue;
|
|
41745
|
+
}
|
|
41746
|
+
planned.push({ tile, c0, c1, r0, r1, step });
|
|
41747
|
+
}
|
|
41748
|
+
// Coarsened together rather than dropped one by one, so a busy view thins out evenly.
|
|
41749
|
+
const blocksOf = (p) => Math.ceil((p.c1 - p.c0) / p.step) * Math.ceil((p.r1 - p.r0) / p.step);
|
|
41750
|
+
let total = planned.reduce((sum, p) => sum + blocksOf(p), 0);
|
|
41751
|
+
while (total > MAX_GRID_CELLS && planned.some((p) => p.step < MAX_GRID_STRIDE)) {
|
|
41752
|
+
for (const p of planned) {
|
|
41753
|
+
p.step = Math.min(MAX_GRID_STRIDE, p.step * 2);
|
|
41754
|
+
}
|
|
41755
|
+
total = planned.reduce((sum, p) => sum + blocksOf(p), 0);
|
|
41756
|
+
}
|
|
41757
|
+
for (const { tile, c0, c1, r0, r1, step } of planned) {
|
|
41758
|
+
// Anchored to the tile's own origin so lines stay on the same boundaries as the camera
|
|
41759
|
+
// moves, and so each step up is a subset of the one below it.
|
|
41760
|
+
const firstR = Math.floor(r0 / step) * step;
|
|
41761
|
+
const firstC = Math.floor(c0 / step) * step;
|
|
41762
|
+
for (let r = firstR; r < r1; r += step) {
|
|
41763
|
+
const n0 = tile.North - (r / tile.ResolutionY) * (tile.North - tile.South);
|
|
41764
|
+
const n1 = tile.North
|
|
41765
|
+
- (Math.min(r + step, tile.ResolutionY) / tile.ResolutionY) * (tile.North - tile.South);
|
|
41766
|
+
for (let c = firstC; c < c1; c += step) {
|
|
41767
|
+
const w0 = tile.West + (c / tile.ResolutionX) * (tile.East - tile.West);
|
|
41768
|
+
const w1 = tile.West
|
|
41769
|
+
+ (Math.min(c + step, tile.ResolutionX) / tile.ResolutionX) * (tile.East - tile.West);
|
|
41770
|
+
if (!this.coveredAt((w0 + w1) / 2, (n0 + n1) / 2)) {
|
|
41771
|
+
continue;
|
|
41772
|
+
}
|
|
41773
|
+
const tl = this.project(w0, n0);
|
|
41774
|
+
const tr = this.project(w1, n0);
|
|
41775
|
+
const br = this.project(w1, n1);
|
|
41776
|
+
const bl = this.project(w0, n1);
|
|
41777
|
+
if (!tl || !tr || !br || !bl) {
|
|
41778
|
+
continue;
|
|
41779
|
+
}
|
|
41780
|
+
path.moveTo(tl.x, tl.y);
|
|
41781
|
+
path.lineTo(tr.x, tr.y);
|
|
41782
|
+
path.lineTo(br.x, br.y);
|
|
41783
|
+
path.lineTo(bl.x, bl.y);
|
|
41784
|
+
path.closePath();
|
|
41785
|
+
drew = true;
|
|
41786
|
+
}
|
|
41787
|
+
}
|
|
41788
|
+
}
|
|
41789
|
+
const gridWidth = (_b = this.settings.gridWidthPixels) !== null && _b !== void 0 ? _b : DEFAULT_GRID_WIDTH_PIXELS;
|
|
41790
|
+
if (drewCoarse) {
|
|
41791
|
+
this.ctx.save();
|
|
41792
|
+
// Dimmer than a cell edge, so a tile boundary reads as "there is finer data here"
|
|
41793
|
+
// rather than as a cell in its own right.
|
|
41794
|
+
this.ctx.globalAlpha = TILE_OUTLINE_ALPHA;
|
|
41795
|
+
this.ctx.strokeStyle = color;
|
|
41796
|
+
this.ctx.lineWidth = gridWidth;
|
|
41797
|
+
this.ctx.stroke(coarse);
|
|
41798
|
+
this.ctx.restore();
|
|
41799
|
+
}
|
|
41800
|
+
if (!drew) {
|
|
41801
|
+
return;
|
|
41802
|
+
}
|
|
41803
|
+
this.ctx.strokeStyle = color;
|
|
41804
|
+
this.ctx.lineWidth = (_c = this.settings.gridWidthPixels) !== null && _c !== void 0 ? _c : DEFAULT_GRID_WIDTH_PIXELS;
|
|
41805
|
+
this.ctx.stroke(path);
|
|
41806
|
+
}
|
|
41807
|
+
/*
|
|
41808
|
+
* Lon/lat of the ground at the centre of the view, or null when the centre misses the globe.
|
|
41809
|
+
*/
|
|
41810
|
+
groundAtScreenCentre() {
|
|
41811
|
+
const scene = this.viewer.scene;
|
|
41812
|
+
const centre = new Cesium.Cartesian2(this.viewer.canvas.clientWidth / 2, this.viewer.canvas.clientHeight / 2);
|
|
41813
|
+
const hit = scene.camera.pickEllipsoid(centre, Cesium.Ellipsoid.WGS84);
|
|
41814
|
+
if (!hit) {
|
|
41815
|
+
return null;
|
|
41816
|
+
}
|
|
41817
|
+
const carto = Cesium.Cartographic.fromCartesian(hit);
|
|
41818
|
+
return {
|
|
41819
|
+
lon: Cesium.Math.toDegrees(carto.longitude),
|
|
41820
|
+
lat: Cesium.Math.toDegrees(carto.latitude)
|
|
41821
|
+
};
|
|
41822
|
+
}
|
|
41823
|
+
/*
|
|
41824
|
+
* Whether the composited raster holds data at a lon/lat.
|
|
41825
|
+
*/
|
|
41826
|
+
coveredAt(lon, lat) {
|
|
41827
|
+
if (!this.source) {
|
|
41828
|
+
return false;
|
|
41829
|
+
}
|
|
41830
|
+
const cols = this.source.width;
|
|
41831
|
+
const rows = this.source.height;
|
|
41832
|
+
const col = Math.floor((lon - this.extent.West) / (this.extent.East - this.extent.West) * cols);
|
|
41833
|
+
const row = Math.floor((this.extent.North - lat) / (this.extent.North - this.extent.South) * rows);
|
|
41834
|
+
if (col < 0 || row < 0 || col >= cols || row >= rows) {
|
|
41835
|
+
return false;
|
|
41836
|
+
}
|
|
41837
|
+
const at = (row * cols + col) * 4 + 3;
|
|
41838
|
+
// Painted, not merely covered: a dry cell holds a reading the ramp deliberately hides, and
|
|
41839
|
+
// ruling a grid over it draws detail where the picture shows none.
|
|
41840
|
+
if (this.painted && this.painted.length === cols * rows * 4) {
|
|
41841
|
+
return this.painted[at] > 0;
|
|
41842
|
+
}
|
|
41843
|
+
return this.source.data[at] >= 128;
|
|
41844
|
+
}
|
|
41845
|
+
isCovered(col, row, cols) {
|
|
41846
|
+
return Boolean(this.source && this.source.data[(row * cols + col) * 4 + 3] >= 128);
|
|
41847
|
+
}
|
|
41848
|
+
/*
|
|
41849
|
+
* Width of the widest label this overlay could draw, in pixels.
|
|
41850
|
+
*/
|
|
41851
|
+
widestLabel(parts) {
|
|
41852
|
+
const sample = [];
|
|
41853
|
+
if (parts.indexOf("value") >= 0) {
|
|
41854
|
+
sample.push("-000.00");
|
|
41855
|
+
}
|
|
41856
|
+
const range = [];
|
|
41857
|
+
if (parts.indexOf("floor") >= 0) {
|
|
41858
|
+
range.push("min: -000.00");
|
|
41859
|
+
}
|
|
41860
|
+
if (parts.indexOf("ceiling") >= 0) {
|
|
41861
|
+
range.push("max: -000.00");
|
|
41862
|
+
}
|
|
41863
|
+
if (range.length) {
|
|
41864
|
+
sample.push(range.join(" - "));
|
|
41865
|
+
}
|
|
41866
|
+
if (!sample.length) {
|
|
41867
|
+
return 0;
|
|
41868
|
+
}
|
|
41869
|
+
const widest = Math.max(...sample.map((line) => this.ctx.measureText(line).width));
|
|
41870
|
+
return widest + LABEL_GAP_PIXELS;
|
|
41871
|
+
}
|
|
41872
|
+
/*
|
|
41873
|
+
* The four screen corners of one cell, or null if any of them will not project.
|
|
41874
|
+
*/
|
|
41875
|
+
cellOutline(col, row, cols, rows) {
|
|
41876
|
+
// On a pyramid the composited texel is not a cell. Outlining it would draw a second,
|
|
41877
|
+
// uniform lattice over the tile grid, misaligned with it everywhere.
|
|
41878
|
+
if (this.tiles && this.tiles.length) {
|
|
41879
|
+
return this.tileCellOutline(this.lonAt(col, cols), this.latAt(row, rows));
|
|
41880
|
+
}
|
|
41881
|
+
const corners = [
|
|
41882
|
+
this.project(this.lonAt(col - 0.5, cols), this.latAt(row - 0.5, rows)),
|
|
41883
|
+
this.project(this.lonAt(col + 0.5, cols), this.latAt(row - 0.5, rows)),
|
|
41884
|
+
this.project(this.lonAt(col + 0.5, cols), this.latAt(row + 0.5, rows)),
|
|
41885
|
+
this.project(this.lonAt(col - 0.5, cols), this.latAt(row + 0.5, rows))
|
|
41886
|
+
];
|
|
41887
|
+
return corners.every((c) => c) ? corners : null;
|
|
41888
|
+
}
|
|
41889
|
+
drawLabel(point, reading, parts, outline) {
|
|
41890
|
+
var _a;
|
|
41891
|
+
const dp = (_a = this.settings.decimals) !== null && _a !== void 0 ? _a : (Math.abs(reading.value) < 10 ? 2 : 1);
|
|
41892
|
+
const lines = [];
|
|
41893
|
+
if (parts.indexOf("value") >= 0) {
|
|
41894
|
+
lines.push(reading.value.toFixed(dp));
|
|
41895
|
+
}
|
|
41896
|
+
// Named rather than a bare pair: two numbers under a third say nothing about which is
|
|
41897
|
+
// the series low and which is its high.
|
|
41898
|
+
const range = [];
|
|
41899
|
+
if (parts.indexOf("floor") >= 0 && reading.floor !== undefined) {
|
|
41900
|
+
range.push(`min: ${reading.floor.toFixed(dp)}`);
|
|
41901
|
+
}
|
|
41902
|
+
if (parts.indexOf("ceiling") >= 0 && reading.ceiling !== undefined) {
|
|
41903
|
+
range.push(`max: ${reading.ceiling.toFixed(dp)}`);
|
|
41904
|
+
}
|
|
41905
|
+
if (range.length) {
|
|
41906
|
+
lines.push(range.join(" - "));
|
|
41907
|
+
}
|
|
41908
|
+
if (!lines.length) {
|
|
41909
|
+
return;
|
|
41910
|
+
}
|
|
41911
|
+
if (outline) {
|
|
41912
|
+
this.ctx.strokeStyle = LABEL_ANCHOR_COLOR;
|
|
41913
|
+
this.ctx.lineWidth = 1.5;
|
|
41914
|
+
this.ctx.beginPath();
|
|
41915
|
+
this.ctx.moveTo(outline[0].x, outline[0].y);
|
|
41916
|
+
for (let i = 1; i < outline.length; i++) {
|
|
41917
|
+
this.ctx.lineTo(outline[i].x, outline[i].y);
|
|
41918
|
+
}
|
|
41919
|
+
this.ctx.closePath();
|
|
41920
|
+
this.ctx.stroke();
|
|
41921
|
+
}
|
|
41922
|
+
const width = Math.max(...lines.map((l) => this.ctx.measureText(l).width)) + 10;
|
|
41923
|
+
const height = lines.length * LINE_HEIGHT + 6;
|
|
41924
|
+
const top = point.y - height / 2;
|
|
41925
|
+
this.ctx.fillStyle = "rgba(16,16,14,0.72)";
|
|
41926
|
+
this.ctx.beginPath();
|
|
41927
|
+
this.ctx.roundRect(point.x - width / 2, top, width, height, LABEL_RADIUS);
|
|
41928
|
+
this.ctx.fill();
|
|
41929
|
+
lines.forEach((line, i) => {
|
|
41930
|
+
this.ctx.fillStyle = i === 0 ? "#ffffff" : "#a8a89f";
|
|
41931
|
+
this.ctx.fillText(line, point.x, top + LINE_HEIGHT * (i + 1) - 1);
|
|
41932
|
+
});
|
|
41933
|
+
this.drawn++;
|
|
41934
|
+
}
|
|
41935
|
+
/*
|
|
41936
|
+
* The first cell with a reading at or near a lattice point, searched within its own block.
|
|
41937
|
+
*/
|
|
41938
|
+
readNear(col, row, cols, rows, stride) {
|
|
41939
|
+
const direct = this.readAt(col, row, cols);
|
|
41940
|
+
if (direct && this.paintedAt(col, row, cols)) {
|
|
41941
|
+
return { col, row, reading: direct };
|
|
41942
|
+
}
|
|
41943
|
+
if (stride <= 1) {
|
|
41944
|
+
return null;
|
|
41945
|
+
}
|
|
41946
|
+
// Bounded, and stepped rather than exhaustive, so a large stride cannot make this the
|
|
41947
|
+
// expensive part of a frame.
|
|
41948
|
+
const step = Math.max(1, Math.floor(stride / MAX_LABEL_PROBES_PER_SIDE));
|
|
41949
|
+
for (let r = row; r < Math.min(rows, row + stride); r += step) {
|
|
41950
|
+
for (let c = col; c < Math.min(cols, col + stride); c += step) {
|
|
41951
|
+
const reading = this.readAt(c, r, cols);
|
|
41952
|
+
if (reading && this.paintedAt(c, r, cols)) {
|
|
41953
|
+
return { col: c, row: r, reading };
|
|
41954
|
+
}
|
|
41955
|
+
}
|
|
41956
|
+
}
|
|
41957
|
+
return null;
|
|
41958
|
+
}
|
|
41959
|
+
paintedAt(col, row, cols) {
|
|
41960
|
+
if (!this.painted) {
|
|
41961
|
+
return true;
|
|
41962
|
+
}
|
|
41963
|
+
const at = (row * cols + col) * 4 + 3;
|
|
41964
|
+
return this.painted[at] > 0;
|
|
41965
|
+
}
|
|
41966
|
+
// Decodes one cell, skipping anything the texture marks as having no data.
|
|
41967
|
+
readAt(col, row, cols) {
|
|
41968
|
+
var _a, _b;
|
|
41969
|
+
const i = (row * cols + col) * 4;
|
|
41970
|
+
if (!this.source || this.source.data[i + 3] < 128) {
|
|
41971
|
+
return null;
|
|
41972
|
+
}
|
|
41973
|
+
const lo = this.metadata && typeof this.metadata.ValueMin === "number" ? this.metadata.ValueMin : 0;
|
|
41974
|
+
const hi = this.metadata && typeof this.metadata.ValueMax === "number" ? this.metadata.ValueMax : 1;
|
|
41975
|
+
const value = lo + (this.source.data[i] / 255) * (hi - lo);
|
|
41976
|
+
const reading = { value };
|
|
41977
|
+
const eLo = (_a = this.metadata) === null || _a === void 0 ? void 0 : _a.ExtremesValueMin;
|
|
41978
|
+
const eHi = (_b = this.metadata) === null || _b === void 0 ? void 0 : _b.ExtremesValueMax;
|
|
41979
|
+
if (this.floor && typeof eLo === "number" && typeof eHi === "number") {
|
|
41980
|
+
reading.floor = eLo + decode16(this.floor.data, i) * (eHi - eLo);
|
|
41981
|
+
// An anomaly archive's frames are already floor-relative, so the reading a user
|
|
41982
|
+
// wants is the absolute one. This is the whole reason the floor is published.
|
|
41983
|
+
if (TextureFrameSeriesAnimator.IsAnomalyMeasure(this.metadata)) {
|
|
41984
|
+
reading.value += reading.floor;
|
|
41985
|
+
}
|
|
41986
|
+
}
|
|
41987
|
+
if (this.ceiling && typeof eLo === "number" && typeof eHi === "number") {
|
|
41988
|
+
reading.ceiling = eLo + decode16(this.ceiling.data, i) * (eHi - eLo);
|
|
41989
|
+
}
|
|
41990
|
+
return reading;
|
|
41991
|
+
}
|
|
41992
|
+
lonAt(col, cols) {
|
|
41993
|
+
return this.extent.West + ((col + 0.5) / cols) * (this.extent.East - this.extent.West);
|
|
41994
|
+
}
|
|
41995
|
+
latAt(row, rows) {
|
|
41996
|
+
return this.extent.North - ((row + 0.5) / rows) * (this.extent.North - this.extent.South);
|
|
41997
|
+
}
|
|
41998
|
+
// The texel range overlapping the camera's view, so an off-screen texture costs nothing.
|
|
41999
|
+
visibleTexels(cols, rows) {
|
|
42000
|
+
const view = this.viewer.camera.computeViewRectangle(Cesium.Ellipsoid.WGS84);
|
|
42001
|
+
if (!view) {
|
|
42002
|
+
return null;
|
|
42003
|
+
}
|
|
42004
|
+
const west = Math.max(this.extent.West, Cesium.Math.toDegrees(view.west));
|
|
42005
|
+
const east = Math.min(this.extent.East, Cesium.Math.toDegrees(view.east));
|
|
42006
|
+
const south = Math.max(this.extent.South, Cesium.Math.toDegrees(view.south));
|
|
42007
|
+
const north = Math.min(this.extent.North, Cesium.Math.toDegrees(view.north));
|
|
42008
|
+
if (west >= east || south >= north) {
|
|
42009
|
+
return null;
|
|
42010
|
+
}
|
|
42011
|
+
const spanLon = this.extent.East - this.extent.West;
|
|
42012
|
+
const spanLat = this.extent.North - this.extent.South;
|
|
42013
|
+
return {
|
|
42014
|
+
col0: Math.max(0, Math.floor((west - this.extent.West) / spanLon * cols) - 1),
|
|
42015
|
+
col1: Math.min(cols - 1, Math.ceil((east - this.extent.West) / spanLon * cols) + 1),
|
|
42016
|
+
row0: Math.max(0, Math.floor((this.extent.North - north) / spanLat * rows) - 1),
|
|
42017
|
+
row1: Math.min(rows - 1, Math.ceil((this.extent.North - south) / spanLat * rows) + 1)
|
|
42018
|
+
};
|
|
42019
|
+
}
|
|
42020
|
+
project(lon, lat) {
|
|
42021
|
+
const world = Cesium.Cartesian3.fromDegrees(lon, lat, 0);
|
|
42022
|
+
const transforms = Cesium.SceneTransforms;
|
|
42023
|
+
// Renamed in newer Cesium, and this library runs on whatever the host app supplies.
|
|
42024
|
+
const fn = transforms.worldToWindowCoordinates || transforms.wgs84ToWindowCoordinates;
|
|
42025
|
+
return fn ? fn.call(transforms, this.viewer.scene, world) : null;
|
|
42026
|
+
}
|
|
42027
|
+
}
|
|
42028
|
+
TextureValueLabels.Labels = Labels;
|
|
42029
|
+
function decode16(data, i) {
|
|
42030
|
+
return (data[i] * 256 + data[i + 1]) / 65535;
|
|
42031
|
+
}
|
|
42032
|
+
})(exports.TextureValueLabels || (exports.TextureValueLabels = {}));
|
|
42033
|
+
|
|
40363
42034
|
(function (DisplacedSurfacePrimitive) {
|
|
40364
42035
|
// Grid densities a tile can draw at, coarsest first.
|
|
40365
42036
|
const TIERS = [32, 64, 128, 192];
|
|
@@ -40377,6 +42048,9 @@
|
|
|
40377
42048
|
const COVERAGE_CUTOFF = 0.35;
|
|
40378
42049
|
// With no exaggeration stated, the full value range is drawn as this fraction of the extent's shorter side.
|
|
40379
42050
|
const AUTO_RELIEF_FRACTION = 0.02;
|
|
42051
|
+
// Ceiling on the derived relief. Sizing purely off the extent is fine for a parcel and absurd for
|
|
42052
|
+
// a whole estuary: 4 m of water over an 82 km extent came out 1.6 km tall.
|
|
42053
|
+
const AUTO_MAX_RELIEF_METRES = 250.0;
|
|
40380
42054
|
// Terrain is sampled on this grid and interpolated between samples.
|
|
40381
42055
|
// Per-texel sampling would be hundreds of thousands of lookups for a surface whose own data is far coarser than that.
|
|
40382
42056
|
const GROUND_SAMPLES_PER_SIDE = 64;
|
|
@@ -40455,8 +42129,7 @@ void main() {
|
|
|
40455
42129
|
`;
|
|
40456
42130
|
const FRAGMENT_SHADER_GLSL300 = `
|
|
40457
42131
|
uniform sampler2D u_valueTexture;
|
|
40458
|
-
uniform
|
|
40459
|
-
uniform vec4 u_highColor;
|
|
42132
|
+
uniform sampler2D u_rampTexture;
|
|
40460
42133
|
uniform float u_coverageCutoff;
|
|
40461
42134
|
uniform vec2 u_texelSize;
|
|
40462
42135
|
uniform float u_valueRange;
|
|
@@ -40471,7 +42144,13 @@ void main() {
|
|
|
40471
42144
|
if (v_coverage < u_coverageCutoff) {
|
|
40472
42145
|
discard;
|
|
40473
42146
|
}
|
|
40474
|
-
|
|
42147
|
+
// The ramp is baked to a lookup rather than lerped between two colours, so a banded style with a
|
|
42148
|
+
// hidden floor reads the same here as it does on the flat drape.
|
|
42149
|
+
// Without it this surface painted water the style hides, and the cell grid then correctly refused to outline any of it.
|
|
42150
|
+
vec4 color = texture(u_rampTexture, vec2(clamp(v_value, 0.0, 1.0), 0.5));
|
|
42151
|
+
if (color.a <= 0.0) {
|
|
42152
|
+
discard;
|
|
42153
|
+
}
|
|
40475
42154
|
|
|
40476
42155
|
// Relief shading from the value gradient, so the displacement reads at a distance without
|
|
40477
42156
|
// recomputing vertex normals every time the texture changes.
|
|
@@ -40670,6 +42349,7 @@ void main() {
|
|
|
40670
42349
|
this.show = true;
|
|
40671
42350
|
this.valueSource = null;
|
|
40672
42351
|
this.lastPresentVersion = -1;
|
|
42352
|
+
this.rampTexture = null;
|
|
40673
42353
|
this.texture = null;
|
|
40674
42354
|
this.textureDirty = true;
|
|
40675
42355
|
this.shaderProgram = null;
|
|
@@ -40691,8 +42371,7 @@ void main() {
|
|
|
40691
42371
|
this.baseHeight = (_d = options.baseHeight) !== null && _d !== void 0 ? _d : 0;
|
|
40692
42372
|
this.pixelPlacement = options.pixelPlacement;
|
|
40693
42373
|
this.tileSkirts = Boolean(options.tileSkirts);
|
|
40694
|
-
this.
|
|
40695
|
-
this.highColor = toCesiumColor(options.highColor || DEFAULT_HIGH_COLOR);
|
|
42374
|
+
this.rampPixels = RampLookup(options.rampStops || null, options.lowColor || DEFAULT_LOW_COLOR, options.highColor || DEFAULT_HIGH_COLOR);
|
|
40696
42375
|
this.tiles = buildTiles(hasCoverage);
|
|
40697
42376
|
}
|
|
40698
42377
|
GetFollowsGround() {
|
|
@@ -40751,6 +42430,7 @@ void main() {
|
|
|
40751
42430
|
return;
|
|
40752
42431
|
}
|
|
40753
42432
|
this.syncTexture(context);
|
|
42433
|
+
this.syncRampTexture(context);
|
|
40754
42434
|
this.requestGround();
|
|
40755
42435
|
this.syncGroundTexture(context);
|
|
40756
42436
|
// A tile needs one mesh before it has a bounding sphere to measure against.
|
|
@@ -40789,6 +42469,10 @@ void main() {
|
|
|
40789
42469
|
this.texture.destroy();
|
|
40790
42470
|
this.texture = null;
|
|
40791
42471
|
}
|
|
42472
|
+
if (this.rampTexture) {
|
|
42473
|
+
this.rampTexture.destroy();
|
|
42474
|
+
this.rampTexture = null;
|
|
42475
|
+
}
|
|
40792
42476
|
if (this.groundTexture) {
|
|
40793
42477
|
this.groundTexture.destroy();
|
|
40794
42478
|
this.groundTexture = null;
|
|
@@ -40882,6 +42566,25 @@ void main() {
|
|
|
40882
42566
|
const width = Cesium.Cartesian3.distance(southWest, southEast);
|
|
40883
42567
|
return Math.max(1, width / Math.max(1, this.source ? this.source.width : 1));
|
|
40884
42568
|
}
|
|
42569
|
+
syncRampTexture(context) {
|
|
42570
|
+
if (this.rampTexture) {
|
|
42571
|
+
return;
|
|
42572
|
+
}
|
|
42573
|
+
this.rampTexture = new Cesium.Texture({
|
|
42574
|
+
context,
|
|
42575
|
+
pixelFormat: Cesium.PixelFormat.RGBA,
|
|
42576
|
+
source: { width: this.rampPixels.length / 4, height: 1,
|
|
42577
|
+
arrayBufferView: this.rampPixels },
|
|
42578
|
+
flipY: false,
|
|
42579
|
+
sampler: new Cesium.Sampler({
|
|
42580
|
+
wrapS: Cesium.TextureWrap.CLAMP_TO_EDGE,
|
|
42581
|
+
wrapT: Cesium.TextureWrap.CLAMP_TO_EDGE,
|
|
42582
|
+
// Linear across the ramp, so a gradient between stops stays smooth.
|
|
42583
|
+
minificationFilter: Cesium.TextureMinificationFilter.LINEAR,
|
|
42584
|
+
magnificationFilter: Cesium.TextureMagnificationFilter.LINEAR
|
|
42585
|
+
})
|
|
42586
|
+
});
|
|
42587
|
+
}
|
|
40885
42588
|
syncTexture(context) {
|
|
40886
42589
|
const sizeChanged = this.texture
|
|
40887
42590
|
&& (this.texture.width !== this.source.width || this.texture.height !== this.source.height);
|
|
@@ -41012,8 +42715,7 @@ void main() {
|
|
|
41012
42715
|
? Math.max(2, Math.abs(self.valueMax - self.valueMin) * self.exaggeration * SKIRT_FRACTION)
|
|
41013
42716
|
: 0,
|
|
41014
42717
|
u_coverageCutoff: () => COVERAGE_CUTOFF,
|
|
41015
|
-
|
|
41016
|
-
u_highColor: () => self.highColor,
|
|
42718
|
+
u_rampTexture: () => self.rampTexture,
|
|
41017
42719
|
u_texelSize: () => new Cesium.Cartesian2(1 / self.source.width, 1 / self.source.height)
|
|
41018
42720
|
}
|
|
41019
42721
|
});
|
|
@@ -41074,12 +42776,10 @@ void main() {
|
|
|
41074
42776
|
if (!(shorterSide > 0)) {
|
|
41075
42777
|
return 1;
|
|
41076
42778
|
}
|
|
41077
|
-
|
|
42779
|
+
const relief = Math.min(shorterSide * AUTO_RELIEF_FRACTION, AUTO_MAX_RELIEF_METRES);
|
|
42780
|
+
return relief / range;
|
|
41078
42781
|
}
|
|
41079
42782
|
DisplacedSurfacePrimitive.autoExaggeration = autoExaggeration;
|
|
41080
|
-
function toCesiumColor(color) {
|
|
41081
|
-
return new Cesium.Color(color.red / 255, color.green / 255, color.blue / 255, color.alpha);
|
|
41082
|
-
}
|
|
41083
42783
|
function now() {
|
|
41084
42784
|
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
41085
42785
|
}
|
|
@@ -41087,10 +42787,11 @@ void main() {
|
|
|
41087
42787
|
|
|
41088
42788
|
const TEXTURE_FRAME_SERIES_ANIMATOR_KEY = "TextureFrameSeriesAnimator.Animator";
|
|
41089
42789
|
const EXTRUSION_ANIMATOR_KEY = "TextureFrameSeriesAnimator.ExtrusionAnimator";
|
|
42790
|
+
const TEXTURE_VALUE_LABELS_KEY = "TextureValueLabels.Overlay";
|
|
41090
42791
|
const DISPLACED_SURFACE_KEY = "DisplacedSurfacePrimitive.Surface";
|
|
41091
42792
|
(function (EntityRenderEnginePolygon) {
|
|
41092
42793
|
async function Render(params) {
|
|
41093
|
-
var _a, _b, _c, _d;
|
|
42794
|
+
var _a, _b, _c, _d, _e;
|
|
41094
42795
|
const entity = params.entity;
|
|
41095
42796
|
const style = params.style;
|
|
41096
42797
|
const pRings = BModels.Entity.GetValue({
|
|
@@ -41141,18 +42842,22 @@ void main() {
|
|
|
41141
42842
|
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);
|
|
41142
42843
|
}
|
|
41143
42844
|
}
|
|
42845
|
+
const retained = params.offline
|
|
42846
|
+
? (_c = params.rendered) === null || _c === void 0 ? void 0 : _c[TEXTURE_FRAME_SERIES_ANIMATOR_KEY]
|
|
42847
|
+
: null;
|
|
42848
|
+
const keepsAnimator = Boolean(retained && !retained.IsDisposed());
|
|
41144
42849
|
const drawsDisplacedSurface = Boolean(extrusionArchive);
|
|
41145
42850
|
// The displaced surface IS the entity's fill, drawn as its own primitive at the value's height.
|
|
41146
42851
|
// Leaving the flat polygon filled as well shows a second copy of the parcel on the ground under it.
|
|
41147
42852
|
const hasFill = (drawsDisplacedSurface ? true
|
|
41148
42853
|
: fillType === BModels.Style.EPolygonFillType.Texture
|
|
41149
|
-
? Boolean(textureDataUri || frameArchive || cFillColor.alpha > 0)
|
|
42854
|
+
? Boolean(textureDataUri || frameArchive || keepsAnimator || cFillColor.alpha > 0)
|
|
41150
42855
|
: cFillColor.alpha > 0);
|
|
41151
42856
|
const fillMaterial = drawsDisplacedSurface
|
|
41152
42857
|
? Cesium.Color.WHITE.withAlpha(0)
|
|
41153
42858
|
: textureDataUri
|
|
41154
42859
|
? new Cesium.ImageMaterialProperty({ image: textureDataUri, transparent: true })
|
|
41155
|
-
: frameArchive
|
|
42860
|
+
: (frameArchive || keepsAnimator)
|
|
41156
42861
|
? Cesium.Color.WHITE.withAlpha(0)
|
|
41157
42862
|
: cFillColor;
|
|
41158
42863
|
const lineColorTrace = style.lineColor ? BModels.Calculator.TraceGetColor(style.lineColor, entity, params.tags) : { value: null, effective: null };
|
|
@@ -41322,7 +43027,7 @@ void main() {
|
|
|
41322
43027
|
}
|
|
41323
43028
|
// Must run before any material is baked below: the Animator takes over polygon.material, so a bake
|
|
41324
43029
|
// has to know whether it is baking over a live animation or over a disposed one's restored material.
|
|
41325
|
-
const animator = syncTextureFrameArchive(cEntity, drawsDisplacedSurface ? null : frameArchive, params.viewer, style.textureColorMask, style.maskTextureBaseline);
|
|
43030
|
+
const animator = syncTextureFrameArchive(cEntity, drawsDisplacedSurface ? null : frameArchive, params.viewer, style.textureColorMask, style.maskTextureBaseline, undefined, undefined, wantsLabels(style), params.offline, polygonBbox(posses));
|
|
41326
43031
|
syncDisplacedSurface({
|
|
41327
43032
|
cEntity,
|
|
41328
43033
|
extrusionArchive,
|
|
@@ -41330,7 +43035,30 @@ void main() {
|
|
|
41330
43035
|
style,
|
|
41331
43036
|
entity,
|
|
41332
43037
|
heightRef,
|
|
41333
|
-
outerRingPosses: posses
|
|
43038
|
+
outerRingPosses: posses,
|
|
43039
|
+
offline: params.offline
|
|
43040
|
+
});
|
|
43041
|
+
const drapeExtent = polygonBbox(posses);
|
|
43042
|
+
const labelArchive = drawsDisplacedSurface ? extrusionArchive : frameArchive;
|
|
43043
|
+
syncTextureValueLabels({
|
|
43044
|
+
cEntity,
|
|
43045
|
+
viewer: params.viewer,
|
|
43046
|
+
settings: overlaySettings(style, entity, params.tags),
|
|
43047
|
+
extent: labelArchive
|
|
43048
|
+
? (drawsDisplacedSurface ? archiveExtent(labelArchive.metadata, posses) : drapeExtent)
|
|
43049
|
+
: null,
|
|
43050
|
+
animator: drawsDisplacedSurface
|
|
43051
|
+
? cEntity[EXTRUSION_ANIMATOR_KEY]
|
|
43052
|
+
: animator,
|
|
43053
|
+
metadata: labelArchive ? labelArchive.metadata : undefined,
|
|
43054
|
+
offline: params.offline,
|
|
43055
|
+
clipRing: posses.map((pos) => {
|
|
43056
|
+
const carto = Cesium.Cartographic.fromCartesian(pos);
|
|
43057
|
+
return {
|
|
43058
|
+
lon: Cesium.Math.toDegrees(carto.longitude),
|
|
43059
|
+
lat: Cesium.Math.toDegrees(carto.latitude)
|
|
43060
|
+
};
|
|
43061
|
+
})
|
|
41334
43062
|
});
|
|
41335
43063
|
if (animator) {
|
|
41336
43064
|
exports.CesiumEntityStyler.SetDefaultTextureImage({
|
|
@@ -41391,7 +43119,7 @@ void main() {
|
|
|
41391
43119
|
}
|
|
41392
43120
|
}
|
|
41393
43121
|
let borderPosses = posses.map(x => x.clone ? x.clone() : { ...x });
|
|
41394
|
-
let cEntityBorder = (
|
|
43122
|
+
let cEntityBorder = (_e = (_d = params.rendered) === null || _d === void 0 ? void 0 : _d._siblingGraphics) === null || _e === void 0 ? void 0 : _e[0];
|
|
41395
43123
|
cEntity._siblingGraphics = [];
|
|
41396
43124
|
if (!cEntityBorder || ((!cEntityBorder.polyline && units == "px") ||
|
|
41397
43125
|
(!cEntityBorder.corridor && units == "m"))) {
|
|
@@ -41642,6 +43370,11 @@ void main() {
|
|
|
41642
43370
|
* Disposes a cEntity's TextureFrameSeriesAnimator.Animator (if any).
|
|
41643
43371
|
*/
|
|
41644
43372
|
function DisposeTextureFrameSeriesAnimator(cEntity, viewer) {
|
|
43373
|
+
const labelOverlay = cEntity === null || cEntity === void 0 ? void 0 : cEntity[TEXTURE_VALUE_LABELS_KEY];
|
|
43374
|
+
if (labelOverlay) {
|
|
43375
|
+
labelOverlay.remove();
|
|
43376
|
+
cEntity[TEXTURE_VALUE_LABELS_KEY] = null;
|
|
43377
|
+
}
|
|
41645
43378
|
const existing = cEntity === null || cEntity === void 0 ? void 0 : cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY];
|
|
41646
43379
|
if (existing && !existing.IsDisposed()) {
|
|
41647
43380
|
existing.Dispose();
|
|
@@ -41767,10 +43500,13 @@ void main() {
|
|
|
41767
43500
|
* @param params
|
|
41768
43501
|
*/
|
|
41769
43502
|
function syncDisplacedSurface(params) {
|
|
41770
|
-
var _a;
|
|
41771
43503
|
const { cEntity, extrusionArchive, viewer, style, entity, heightRef, outerRingPosses } = params;
|
|
41772
43504
|
const existingAnimator = cEntity[EXTRUSION_ANIMATOR_KEY];
|
|
41773
43505
|
const existingSurface = cEntity[DISPLACED_SURFACE_KEY];
|
|
43506
|
+
// Same reason as the fill animator: an unresolvable archive is not a removed one.
|
|
43507
|
+
if (params.offline && existingSurface && !existingSurface.isDestroyed()) {
|
|
43508
|
+
return;
|
|
43509
|
+
}
|
|
41774
43510
|
// Nothing to draw and nothing left over, which is every polygon that does not use this feature.
|
|
41775
43511
|
if (!extrusionArchive && !existingAnimator && !existingSurface) {
|
|
41776
43512
|
return;
|
|
@@ -41786,7 +43522,7 @@ void main() {
|
|
|
41786
43522
|
// to rebuild rather than update in place.
|
|
41787
43523
|
if (reusable && existingSurface.GetFollowsGround() === placement.followGround) {
|
|
41788
43524
|
existingSurface.SetBaseHeight(placement.baseHeight);
|
|
41789
|
-
existingSurface.SetExaggeration((
|
|
43525
|
+
existingSurface.SetExaggeration(exaggerationFor(style, extrusionArchive.metadata, existingSurface.GetExtent()));
|
|
41790
43526
|
return;
|
|
41791
43527
|
}
|
|
41792
43528
|
disposeDisplacedSurface(cEntity, viewer);
|
|
@@ -41808,6 +43544,8 @@ void main() {
|
|
|
41808
43544
|
textureColorMask: style.textureColorMask,
|
|
41809
43545
|
driveMaterial: false,
|
|
41810
43546
|
produceValueCanvas: true,
|
|
43547
|
+
metadata: extrusionArchive.metadata,
|
|
43548
|
+
cellTexels: 1,
|
|
41811
43549
|
baselineMask: extrusionArchive.metadata.BaselineMask,
|
|
41812
43550
|
// Off-type for the same reason as the fill path: the pinned bruce-models predates the field.
|
|
41813
43551
|
maskBaseline: style.maskTextureBaseline
|
|
@@ -41824,9 +43562,15 @@ void main() {
|
|
|
41824
43562
|
terrainProvider: viewer && viewer.terrainProvider,
|
|
41825
43563
|
valueMin: extrusionArchive.metadata.ValueMin,
|
|
41826
43564
|
valueMax: extrusionArchive.metadata.ValueMax,
|
|
41827
|
-
exaggeration: style.
|
|
43565
|
+
exaggeration: exaggerationFor(style, extrusionArchive.metadata, extent),
|
|
41828
43566
|
lowColor: mask ? BModels.Color.ColorFromStr(mask.minColor) : null,
|
|
41829
|
-
highColor: mask ? BModels.Color.ColorFromStr(mask.maxColor) : null
|
|
43567
|
+
highColor: mask ? BModels.Color.ColorFromStr(mask.maxColor) : null,
|
|
43568
|
+
// Normalised the same way the animator normalises them, so the surface and the flat drape
|
|
43569
|
+
// agree about which values the style hides.
|
|
43570
|
+
rampStops: (mask && mask.points || []).map((stop) => ({
|
|
43571
|
+
position: TextureFrameSeriesAnimator.NormalisePosition(extrusionArchive.metadata, stop.position),
|
|
43572
|
+
color: BModels.Color.ColorFromStr(stop.color)
|
|
43573
|
+
})).filter((stop) => Boolean(stop.color))
|
|
41830
43574
|
});
|
|
41831
43575
|
surface.BindAnimator(animator);
|
|
41832
43576
|
viewer.scene.primitives.add(surface);
|
|
@@ -41859,6 +43603,40 @@ void main() {
|
|
|
41859
43603
|
* @param metadata
|
|
41860
43604
|
* @param posses
|
|
41861
43605
|
*/
|
|
43606
|
+
/*
|
|
43607
|
+
* The factor a displaced surface should use, honouring an explicit style setting first.
|
|
43608
|
+
*
|
|
43609
|
+
* An elevation is a real height against a datum, so scaling it is not a display choice: it lifts the
|
|
43610
|
+
* surface off the datum it belongs to. Only a thickness is safe to exaggerate, which is why an
|
|
43611
|
+
* elevation archive defaults to 1 rather than to a derived factor.
|
|
43612
|
+
*/
|
|
43613
|
+
function exaggerationFor(style, metadata, extent) {
|
|
43614
|
+
if (style.extrusionExaggeration != null) {
|
|
43615
|
+
return style.extrusionExaggeration;
|
|
43616
|
+
}
|
|
43617
|
+
if (metadata.Quantity === TextureFrameSeriesAnimator.QUANTITY_ELEVATION) {
|
|
43618
|
+
return 1;
|
|
43619
|
+
}
|
|
43620
|
+
if (!extent) {
|
|
43621
|
+
return undefined;
|
|
43622
|
+
}
|
|
43623
|
+
return exports.DisplacedSurfacePrimitive.autoExaggeration(extent, metadata.ValueMin, metadata.ValueMax);
|
|
43624
|
+
}
|
|
43625
|
+
/*
|
|
43626
|
+
* The lon/lat bounding rectangle of a ring, which is the frame Cesium drapes an image material in.
|
|
43627
|
+
*/
|
|
43628
|
+
function polygonBbox(posses) {
|
|
43629
|
+
if (!posses || posses.length === 0) {
|
|
43630
|
+
return undefined;
|
|
43631
|
+
}
|
|
43632
|
+
const rectangle = Cesium.Rectangle.fromCartesianArray(posses);
|
|
43633
|
+
return {
|
|
43634
|
+
West: Cesium.Math.toDegrees(rectangle.west),
|
|
43635
|
+
East: Cesium.Math.toDegrees(rectangle.east),
|
|
43636
|
+
South: Cesium.Math.toDegrees(rectangle.south),
|
|
43637
|
+
North: Cesium.Math.toDegrees(rectangle.north)
|
|
43638
|
+
};
|
|
43639
|
+
}
|
|
41862
43640
|
function archiveExtent(metadata, posses) {
|
|
41863
43641
|
if (metadata.West != null && metadata.East != null && metadata.South != null && metadata.North != null) {
|
|
41864
43642
|
return { West: metadata.West, East: metadata.East, South: metadata.South, North: metadata.North };
|
|
@@ -41909,9 +43687,22 @@ void main() {
|
|
|
41909
43687
|
* @param viewer
|
|
41910
43688
|
* @param textureColorMask
|
|
41911
43689
|
*/
|
|
41912
|
-
function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask, maskTextureBaseline) {
|
|
43690
|
+
function syncTextureFrameArchive(cEntity, frameArchive, viewer, textureColorMask, maskTextureBaseline, cellBorder, cellTexels, produceValueCanvas, offline, drapeExtent) {
|
|
41913
43691
|
const existing = cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY];
|
|
41914
|
-
|
|
43692
|
+
// Offline says nothing about whether the archive changed, only that it could not be asked.
|
|
43693
|
+
if (offline && existing && !existing.IsDisposed()) {
|
|
43694
|
+
return existing;
|
|
43695
|
+
}
|
|
43696
|
+
// Appearance is part of the reuse test, not just the archive URL. Recolouring a texture-driven
|
|
43697
|
+
// polygon keeps the same archive, so matching on the URL alone kept the old Animator and the
|
|
43698
|
+
// style edit appeared to do nothing at all.
|
|
43699
|
+
const appearance = TextureFrameSeriesAnimator.AppearanceSignature({
|
|
43700
|
+
textureColorMask, cellBorder, cellTexels, maskBaseline: maskTextureBaseline,
|
|
43701
|
+
valueCanvas: produceValueCanvas, drape: drapeExtent
|
|
43702
|
+
});
|
|
43703
|
+
if (frameArchive && existing && !existing.IsDisposed()
|
|
43704
|
+
&& existing.GetArchiveUrl() === frameArchive.url
|
|
43705
|
+
&& existing.GetAppearanceSignature() === appearance) {
|
|
41915
43706
|
return existing;
|
|
41916
43707
|
}
|
|
41917
43708
|
if (existing && !existing.IsDisposed()) {
|
|
@@ -41927,12 +43718,118 @@ void main() {
|
|
|
41927
43718
|
archiveUrl: frameArchive.url,
|
|
41928
43719
|
frames: frameArchive.metadata.Frames,
|
|
41929
43720
|
textureColorMask,
|
|
43721
|
+
metadata: frameArchive.metadata,
|
|
43722
|
+
cellBorder,
|
|
43723
|
+
cellTexels,
|
|
41930
43724
|
baselineMask: frameArchive.metadata.BaselineMask,
|
|
41931
|
-
maskBaseline: maskTextureBaseline
|
|
43725
|
+
maskBaseline: maskTextureBaseline,
|
|
43726
|
+
produceValueCanvas,
|
|
43727
|
+
drapeExtent
|
|
41932
43728
|
});
|
|
41933
43729
|
cEntity[TEXTURE_FRAME_SERIES_ANIMATOR_KEY] = animator;
|
|
41934
43730
|
return animator;
|
|
41935
43731
|
}
|
|
43732
|
+
/**
|
|
43733
|
+
* Folds the cell-border and value-label style into the single overlay that draws both.
|
|
43734
|
+
*
|
|
43735
|
+
* Returns undefined when neither is asked for, which is what keeps the overlay off entirely.
|
|
43736
|
+
*/
|
|
43737
|
+
function wantsLabels(style) {
|
|
43738
|
+
const texture = style.texture;
|
|
43739
|
+
return Boolean(texture && !Array.isArray(texture) && texture.label);
|
|
43740
|
+
}
|
|
43741
|
+
/*
|
|
43742
|
+
* How the cell grid and its values are drawn, which the style does not get a say in.
|
|
43743
|
+
*
|
|
43744
|
+
* A white hairline reads over every ramp colour without competing with it, and the spacing and cap
|
|
43745
|
+
* are what a parcel-sized archive needs to stay readable while the camera moves. Exposing them
|
|
43746
|
+
* bought nothing except thresholds an author could get wrong.
|
|
43747
|
+
*/
|
|
43748
|
+
const LABEL_GRID_COLOR = "rgba(255,255,255,0.55)";
|
|
43749
|
+
const LABEL_GRID_WIDTH_PIXELS = 1;
|
|
43750
|
+
const LABEL_MIN_SPACING_PIXELS = 78;
|
|
43751
|
+
const LABEL_MAX_LABELS = 240;
|
|
43752
|
+
function overlaySettings(style, entity, tags) {
|
|
43753
|
+
if (!wantsLabels(style)) {
|
|
43754
|
+
return undefined;
|
|
43755
|
+
}
|
|
43756
|
+
return {
|
|
43757
|
+
gridColor: LABEL_GRID_COLOR,
|
|
43758
|
+
gridWidthPixels: LABEL_GRID_WIDTH_PIXELS,
|
|
43759
|
+
minSpacingPixels: LABEL_MIN_SPACING_PIXELS,
|
|
43760
|
+
maxLabels: LABEL_MAX_LABELS
|
|
43761
|
+
};
|
|
43762
|
+
}
|
|
43763
|
+
/**
|
|
43764
|
+
* Keeps a value-label overlay in step with whichever animator is driving the polygon.
|
|
43765
|
+
*
|
|
43766
|
+
* Redrawn on preRender rather than on frame changes: labels are screen-space, so panning and zooming
|
|
43767
|
+
* move them even when the data has not advanced at all.
|
|
43768
|
+
*/
|
|
43769
|
+
function syncTextureValueLabels(params) {
|
|
43770
|
+
const { cEntity, viewer, settings, extent, animator, metadata } = params;
|
|
43771
|
+
const existing = cEntity[TEXTURE_VALUE_LABELS_KEY];
|
|
43772
|
+
// Offline has no extent to rebuild the overlay from, so an existing one is left running.
|
|
43773
|
+
if (params.offline && existing) {
|
|
43774
|
+
return;
|
|
43775
|
+
}
|
|
43776
|
+
if (existing) {
|
|
43777
|
+
existing.remove();
|
|
43778
|
+
cEntity[TEXTURE_VALUE_LABELS_KEY] = null;
|
|
43779
|
+
}
|
|
43780
|
+
if (!settings || !extent || !animator) {
|
|
43781
|
+
return;
|
|
43782
|
+
}
|
|
43783
|
+
const labels = new exports.TextureValueLabels.Labels({
|
|
43784
|
+
viewer, extent, settings, metadata,
|
|
43785
|
+
tiles: TextureFrameSeriesAnimator.IsTiledLayout(metadata)
|
|
43786
|
+
? metadata.Tiles.map((tile) => ({
|
|
43787
|
+
West: tile.West, East: tile.East, South: tile.South, North: tile.North,
|
|
43788
|
+
ResolutionX: tile.ResolutionX, ResolutionY: tile.ResolutionY
|
|
43789
|
+
}))
|
|
43790
|
+
: undefined,
|
|
43791
|
+
clipRing: params.clipRing
|
|
43792
|
+
});
|
|
43793
|
+
let lastVersion = -1;
|
|
43794
|
+
let appliedFloor = null;
|
|
43795
|
+
let appliedCeiling = null;
|
|
43796
|
+
// The extremes are what turn an anomaly frame back into a reading, so they are worth two extra
|
|
43797
|
+
// Range GETs here even though nothing else in the render path wants them.
|
|
43798
|
+
animator.EnsureExtremes();
|
|
43799
|
+
const remove = viewer.scene.preRender.addEventListener(() => {
|
|
43800
|
+
if (animator.IsDisposed()) {
|
|
43801
|
+
return;
|
|
43802
|
+
}
|
|
43803
|
+
const canvas = animator.GetValueCanvas();
|
|
43804
|
+
if (!canvas) {
|
|
43805
|
+
return;
|
|
43806
|
+
}
|
|
43807
|
+
const version = animator.GetPresentVersion();
|
|
43808
|
+
if (version !== lastVersion) {
|
|
43809
|
+
lastVersion = version;
|
|
43810
|
+
const ctx = canvas.getContext("2d");
|
|
43811
|
+
labels.SetSource(ctx.getImageData(0, 0, canvas.width, canvas.height));
|
|
43812
|
+
labels.SetPainted(animator.GetDisplayedPixels());
|
|
43813
|
+
}
|
|
43814
|
+
// Tracked by reference rather than latched on the first one to arrive: a tiled archive
|
|
43815
|
+
// composites the floor and the ceiling separately, so latching once would leave every label
|
|
43816
|
+
// showing a minimum and no maximum.
|
|
43817
|
+
const extremes = animator.GetExtremes();
|
|
43818
|
+
if (extremes.floor !== appliedFloor || extremes.ceiling !== appliedCeiling) {
|
|
43819
|
+
appliedFloor = extremes.floor;
|
|
43820
|
+
appliedCeiling = extremes.ceiling;
|
|
43821
|
+
labels.SetExtremes(appliedFloor, appliedCeiling);
|
|
43822
|
+
}
|
|
43823
|
+
labels.Render();
|
|
43824
|
+
});
|
|
43825
|
+
cEntity[TEXTURE_VALUE_LABELS_KEY] = {
|
|
43826
|
+
labels,
|
|
43827
|
+
remove: () => {
|
|
43828
|
+
remove();
|
|
43829
|
+
labels.Dispose();
|
|
43830
|
+
}
|
|
43831
|
+
};
|
|
43832
|
+
}
|
|
41936
43833
|
/**
|
|
41937
43834
|
* Derives a min/max/label time-range segment from a resolved frame archive's Frames metadata,
|
|
41938
43835
|
* for reporting up to whoever owns the menu item this entity belongs to (see IParams.onSeriesDiscovered).
|
|
@@ -43525,7 +45422,7 @@ void main() {
|
|
|
43525
45422
|
StyleUtils.ApplyTypeStyle = ApplyTypeStyle;
|
|
43526
45423
|
})(exports.StyleUtils || (exports.StyleUtils = {}));
|
|
43527
45424
|
|
|
43528
|
-
const VERSION = "7.2.
|
|
45425
|
+
const VERSION = "7.2.2";
|
|
43529
45426
|
/**
|
|
43530
45427
|
* Updates the environment instance used by bruce-cesium to one specified.
|
|
43531
45428
|
* This can be used to ensure that the instance a parent is referencing is shared between bruce-cesium, bruce-models, and the parent app.
|