原文链接及内容

: 由于个人的Cesium Ion中没有官方示例中的数据,因此仅在官方提供的在线编辑器中进行了学习、修改。

实现效果如下视频所示:

NGA:National Geospatial-Intelligence Agency,美国国家地理空间情报局。
GPM:Global Precipitation Measurement,全球降水测量

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<style>
@import url(../templates/bucket.css);

#toolbar {
background: rgba(42, 42, 42, 0.8);
padding: 4px;
border-radius: 4px;
}

#toolbar input {
vertical-align: middle;
padding-top: 2px;
padding-bottom: 2px;
}

ul {
margin-left: -25px;
}

.cesium-performanceDisplay-defaultContainer {
top: 10px;
}
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay">
<h1>Loading...</h1>
</div>
<div id="toolbar">
<table>
<tbody>
<tr>
<td colspan="3">
<h3>Cesium GPM Visualization-Cesium全球降水测量可视化</h3>
<div>GPM,Global Precipitation Measurement</div>
</td>
</tr>
<tr>
<td>数据集</td>
<td>
<select data-bind="options: dataSetNames, value: dataSetName"></select>
</td>
<td style="text-align: right">
<button type="button" data-bind="click: zoomToCurrentTileset">缩放至当前Tileset</button>
</td>
</tr>
<tr>
<td>锚点椭球体</td>
<td colspan="2">
<input type="range" min="1.0" max="500" step="1"
data-bind="value: anchorPointEllipsoidScaling, valueUpdate: 'input'">
<input type="text" size="5" data-bind="value: anchorPointEllipsoidScaling">
<label><input type="checkbox" data-bind="checked: anchorPointLabelsVisible">显示标签</label>
</td>
</tr>
<tr>
<td>着色器模式</td>
<td colspan="2">
<select data-bind="options: shaderModes, value: shaderMode"></select>
</td>
</tr>
<tr id="texture-threshold-control" class="hidden">
<td>纹理阈值(m)</td>
<td>
<input type="range" min="0.0" max="16.0" step="0.01"
data-bind="value: textureThreshold, valueUpdate: 'input'">
<input type="text" size="5" data-bind="value: textureThreshold">
</td>
</tr>
<tr>
<td colspan="3">
<ul>
<li>不确定性信息以 GPM 元数据的形式存储在瓦片中。</li>
<li><b>锚点</b>锚点提供了关于低频误差的信息。这里将这个误差可视化为椭球。</li>
<li>高频误差表示为<b>每点误差</b>纹理。</li>
<li>此错误可以通过自定义着色器进行可视化。</li>
<li>“不确定性”着色器通过颜色刻度来可视化水平或垂直的不确定性。</li>
<li>“阈值”着色器会在图像中标记或强调那些错误超过预定标准的区域。</li>
<li>当你在瓦片集上拾取一个具体的点时,系统会显示一个展示该点对应的<br>精确错误值的标签。</li>
</ul>
</td>
</tr>
</tbody>
</table>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
const viewer = new Cesium.Viewer("cesiumContainer", {
geocoder: false,
sceneModePicker: false,
homeButton: false,
navigationHelpButton: false,
baseLayerPicker: false,
navigationInstructionsInitiallyVisible: false,
animation: false,
timeline: false,
fullscreenButton: false,
selectionIndicator: false,
skyBox: false,
shouldAnimate: true,
depthPlaneEllipsoidOffset: 10000, //调整 DepthPlane 以解决椭球零高程以下的渲染伪影,默认值为0.0
});

//viewer.cesiumWidget.creditContainer.style.display = "none";//隐藏版权信息
viewer.scene.debugShowFramesPerSecond = true; //添加帧速显示

const maxarCredit = new Cesium.Credit(
"由 Maxar 提供,用于 GPM v1.2 评估目的",
true
);
viewer.creditDisplay.addStaticCredit(maxarCredit);

// 将传递给所有 Cesium3DTileset.fromXxx系列函数的选项
const defaultTilesetOptions = {
maximumScreenSpaceError: 4,
cacheBytes: 536870912 * 4,
};

// 用户界面中可用的数据集列表
const dataSetOptions = [
{
name: "卡纳维拉尔港",
assetId: 2772481,
initialCameraConfiguration: {
destination: new Cesium.Cartesian3(
918010.2634368961,
-5538948.372542206,
3016855.1270222967
),
orientation: new Cesium.HeadingPitchRoll(
4.316957618964909,
-0.36573164980955686,
6.283174173076164
),
},
},
{
name: "拉杰斯",
assetId: 2772479,
initialCameraConfiguration: {
destination: new Cesium.Cartesian3(
4436474.736810458,
-2279634.446362909,
3962861.396769178
),
orientation: new Cesium.HeadingPitchRoll(
5.214554743456651,
-0.5265190154471187,
0.000008735845947960286
),
},
},
];

/**
* 从给定的数据集选项创建瓦片集
*
* @param {object} dataSetOption 数据集选项
* @returns A promise to wait for
*/
async function createTileset(dataSetOption) {
const assetId = dataSetOption.assetId;
return viewer.scene.primitives.add(
await Cesium.Cesium3DTileset.fromIonAssetId(assetId, defaultTilesetOptions)
);
}

//============================================================================
// Application state 应用程序状态

class App {
constructor() {
// 当前数据集的名称,即 dataSetOptions[i].name
this._currentDataSetName = undefined;

// 当前瓦片集
this._currentTileset = undefined;

// 当前自定义着色器
this._currentCustomShader = undefined;

// 当前u_textureThreshold uniform变量的值决定currentCustomShader中是否启用片元高亮渲染
this._textureThreshold = 3.0;

// 锚点椭球体的缩放因子
this.anchorPointEllipsoidScaling = 100.0;

// 每当 `currentTileset` 发生改变时,都会调用一组函数,
// 并接收 oldCurrentTileset 和 newCurrentTileset
this._currentTilesetChangedListeners = [];
}

async selectCurrentDataSet(dataSetName) {
if (this._currentDataSetName === dataSetName) {
return;
}
for (const dataSetOption of dataSetOptions) {
if (dataSetOption.name === dataSetName) {
this._currentDataSetName = dataSetName;
await this.createCurrentTileset(dataSetOption);
}
}
}

get currentTileset() {
return this._currentTileset;
}
set currentTileset(value) {
const oldCurrentTileset = this._currentTileset;
this._currentTileset = value;
this.updateCustomShaderInTileset();
this.notifyCurrentTilesetChanged(oldCurrentTileset, this.currentTileset);
}

/**
* 添加给定的函数,以便在当前图块集更改时调用,接收“旧”和“新”的图块集。
*
* @param {object} listener The listener
*/
addCurrentTilesetChangedListener(listener) {
this._currentTilesetChangedListeners.push(listener);
}

/**
* 通知所有已注册的监听器当前的图块集已发生改变
*
* @param {Cesium3DTileset} oldCurrentTileset
* @param {Cesium3DTileset} newCurrentTileset
*
* @private
*/
notifyCurrentTilesetChanged(oldCurrentTileset, newCurrentTileset) {
for (const listener of this._currentTilesetChangedListeners) {
listener(oldCurrentTileset, newCurrentTileset);
}
}

get currentCustomShader() {
return this._currentCustomShader;
}
set currentCustomShader(value) {
this._currentCustomShader = value;
this.updateTextureThresholdInShader();
this.updateCustomShaderInTileset();
}

/**
* 如果定义了 currentTileset,则将 currentCustomShader 分配给它。
*
* @private
*/
updateCustomShaderInTileset() {
if (!Cesium.defined(this.currentTileset)) {
return;
}
this.currentTileset.customShader = this.currentCustomShader;
}

get textureThreshold() {
return this._textureThreshold;
}

set textureThreshold(value) {
this._textureThreshold = value;
this.updateTextureThresholdInShader();
}

/**
* 如果 currentCustomShader 已定义并且拥有 u_textureThreshold uniform,
* 则将其值设置为当前的 textureThreshold。
*
* @private
*/
updateTextureThresholdInShader() {
if (!Cesium.defined(this.currentCustomShader)) {
return;
}
const hasTextureThreshold = Object.keys(
this.currentCustomShader.uniforms
).includes("u_textureThreshold");
if (!hasTextureThreshold) {
return;
}
this.currentCustomShader.setUniform(
"u_textureThreshold",
this.textureThreshold
);
}

/**
* 加载在给定的数据集选项中描述的瓦片集,并将其设置为当前的瓦片集。
*
* @param {object} dataSetOption The data set option
* @returns A promise to wait for...
*/
async createCurrentTileset(dataSetOption) {
if (Cesium.defined(this.currentTileset)) {
viewer.scene.primitives.remove(this.currentTileset);
this.currentTileset = undefined;
}

this.currentTileset = await createTileset(dataSetOption);
if (Cesium.defined(dataSetOption.initialCameraConfiguration)) {
viewer.scene.camera.setView(dataSetOption.initialCameraConfiguration);
} else {
this.zoomToCurrentTileset();
}
}

/**
* 缩放到当前的瓦片集,并带有微小且不确定的偏移量...
*/
zoomToCurrentTileset() {
if (!Cesium.defined(this.currentTileset)) {
return;
}
const offset = new Cesium.HeadingPitchRange(
Cesium.Math.toRadians(0.0),
Cesium.Math.toRadians(-22.5),
10000.0
);
viewer.zoomTo(this.currentTileset, offset);
}
}

const app = new App();

//============================================================================

/**
* 从给定窗口位置的纹理中拾取(pick)局部不确定性信息。
* 这对基础数据集中包含的元数据的结构进行了假设。
* 它假定数据包含两个 PPE(每点误差)纹理,这些纹理在内部被转换为结构元数据属性纹理属性。
* 第一个 PPE 纹理使用了 SIGZ 特性(z 方向的 sigma/标准差,单位为米)。这将是结果的`y`分量。
* 第二个 PPE 纹理使用了 SIGR 特性(径向标准偏差,单位为米)。这将是结果的 x 分量。
* (两个值均在 [0...16] 范围内,其中 16 为 noData 值)
*
* @param {Cartesian2} windowPosition 浏览器窗口的位置
* @returns {Cartesian2} 局部不确定性,其中 `x` 为径向不确定性,`y` 为垂直不确定性
*/
function pickUncertaintyFromTexture(windowPosition) {
const schemaId = undefined;
const classNameX = "ppeTexture_1";
const propertyNameX = "SIGR";
const classNameY = "ppeTexture_0";
const propertyNameY = "SIGZ";
const result = new Cesium.Cartesian2();

let metadataValueX = viewer.scene.pickMetadata(
windowPosition,
schemaId,
classNameX,
propertyNameX
);
let metadataValueY = viewer.scene.pickMetadata(
windowPosition,
schemaId,
classNameY,
propertyNameY
);

if (!Cesium.defined(metadataValueX)) {
metadataValueX = 0;
}
if (!Cesium.defined(metadataValueY)) {
metadataValueY = 0;
}
result.x = metadataValueX;
result.y = metadataValueY;
return result;
}

//============================================================================
// Property texture shaders 属性纹理着色器

class PropertyTextureShaders {
/**
* 为指定属性创建自定义(片元)着色器。
*
* 它使用给定的属性名称访问元数据值,并根据给定的源范围将其归一化到[0,1]的值。
* 如果得到的结果小于 1.0,则将该值用作片元的亮度。
*
* @param {string} propertyName 属性名称
* @param {number} sourceMin 最小的source值
* @param {number} sourceMax 最大source值
* @returns 自定义着色器
* @private
*/
static createShader1D(propertyName, sourceMin, sourceMax) {
const shader = new Cesium.CustomShader({
fragmentShaderText: `
void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
{
float value = float(fsInput.metadata.${propertyName});
float range = float(${sourceMax}) - float(${sourceMin});
float brightness = (value - float(${sourceMin})) / range;

if (value < float(${sourceMax})) {
material.diffuse = vec3(brightness);
}
}
`,
});
return shader;
}

/**
* 为指定的属性创建自定义(片元)着色器。
*
* 它使用给定的属性名称访问元数据值,并根据给定的源范围将它们归一化到[0,1]的值。
* 如果结果值都小于 1.0,则将这些值用作片元的红绿分量。
*
* @param {string} propertyName0 The property name 0
* @param {number} sourceMin0 The minimum source value 0
* @param {number} sourceMax0 The maximum source value 0
* @param {string} propertyName1 The property name 1
* @param {number} sourceMin1 The minimum source value 1
* @param {number} sourceMax1 The maximum source value 1
* @returns The `CustomShader`
* @private
*/
static createShader2D(
propertyName0,
sourceMin0,
sourceMax0,
propertyName1,
sourceMin1,
sourceMax1
) {
const shader = new Cesium.CustomShader({
fragmentShaderText: `
void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
{
float value0 = float(fsInput.metadata.${propertyName0});
float range0 = float(${sourceMax0}) - float(${sourceMin0});
float brightness0 = (value0 - float(${sourceMin0})) / range0;

float value1 = float(fsInput.metadata.${propertyName1});
float range1 = float(${sourceMax1}) - float(${sourceMin1});
float brightness1 = (value1 - float(${sourceMin1})) / range1;

if (value0 < float(${sourceMax0}) && value1 < float(${sourceMax1})) {
material.diffuse = vec3(brightness0, brightness1, 0.0);
}
}
`,
});
return shader;
}

/**
* 为指定属性创建自定义(片元)着色器以实现阈值可视化。
*
* 该函数定义了u_textureThreshold浮点型uniform变量作为阈值参数。
* 通过访问指定属性名称对应的元数据值,当计算结果处于阈值与1.0之间时,
* 片元将使用高亮颜色渲染;否则将以降低饱和度与亮度的原始色彩显示。
*
* @param {string} propertyName The property name
* @param {number} sourceMin The minimum source value
* @param {number} sourceMax The maximum source value
* @returns The `CustomShader`
* @private
*/
static createThresholdShader1D(propertyName, sourceMin, sourceMax) {
const shader = new Cesium.CustomShader({
uniforms: {
// 纹理阈值参数:当采样值超过该阈值时,片元将进行高亮渲染
u_textureThreshold: {
type: Cesium.UniformType.FLOAT,
value: 3.0,
},
},
fragmentShaderText: `
void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
{
float value = float(fsInput.metadata.${propertyName});
float range = float(${sourceMax}) - float(${sourceMin});
float brightness = (value - float(${sourceMin})) / range;

if (value < float(${sourceMax}) && value > u_textureThreshold) {
material.diffuse = vec3(1.0, 1.0, 0.0);
} else {
vec3 diffuseHsl = czm_RGBToHSL(material.diffuse);
diffuseHsl.y *= 0.25;
diffuseHsl.z *= 0.25;
material.diffuse = czm_HSLToRGB(diffuseHsl);
}
}
`,
});
return shader;
}

/**
* 为指定属性创建自定义(片元)着色器以实现阈值可视化。
*
* 该函数定义了一个浮点型u_textureThreshold uniform变量作为阈值参数,
* 通过访问指定属性名称对应的元数据值进行判定。
* 当数值处于阈值与1.0之间时,片元将以高亮颜色渲染;否则将采用较低的饱和度与亮度原始色彩呈现。
*
* @param {string} propertyName0 The property name 0
* @param {number} sourceMin0 The minimum source value 0
* @param {number} sourceMax0 The maximum source value 0
* @param {string} propertyName1 The property name 1
* @param {number} sourceMin1 The minimum source value 1
* @param {number} sourceMax1 The maximum source value 1
* @returns The `CustomShader`
* @private
*/
static createThresholdShader2D(
propertyName0,
sourceMin0,
sourceMax0,
propertyName1,
sourceMin1,
sourceMax1
) {
const shader = new Cesium.CustomShader({
uniforms: {
// 纹理的阈值:超过此值时,片元将高亮显示。
u_textureThreshold: {
type: Cesium.UniformType.FLOAT,
value: 3.0,
},
},
fragmentShaderText: `
void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
{
float value0 = float(fsInput.metadata.${propertyName0});
float range0 = float(${sourceMax0}) - float(${sourceMin0});
float brightness0 = (value0 - float(${sourceMin0})) / range0;

float value1 = float(fsInput.metadata.${propertyName1});
float range1 = float(${sourceMax1}) - float(${sourceMin1});
float brightness1 = (value1 - float(${sourceMin1})) / range1;

vec3 diffuseHsl = czm_RGBToHSL(material.diffuse);
diffuseHsl.y *= 0.25;
diffuseHsl.z *= 0.25;
vec3 diffuseResult = czm_HSLToRGB(diffuseHsl);

if (value0 < float(${sourceMax0}) && value0 > u_textureThreshold) {
diffuseResult.x = 1.0;
diffuseResult.y = 1.0;
}
if (value1 < float(${sourceMax1}) && value1 > u_textureThreshold) {
diffuseResult.x = 1.0;
diffuseResult.z = 1.0;
}
material.diffuse = diffuseResult;
}
`,
});
return shader;
}

/**
* 创建一个对象,该对象可用作 Sandcastle 工具栏菜单选项中的一个条目。
*
* 根据定义的属性名称创建一个选择默认着色、1D 着色器或 2D 着色器的选项
*
* @param {string} title The title to be displayed in the combo box
* @param {string|undefined} propertyName0 The property name 0
* @param {number} sourceMin0 The minimum source value 0
* @param {number} sourceMax0 The maximum source value 0
* @param {string|undefined} propertyName1 The property name 1
* @param {number} sourceMin1 The minimum source value 1
* @param {number} sourceMax1 The maximum source value 1
* @returns The shader option
* @private
*/
static createShaderOption(
title,
propertyName0,
sourceMin0,
sourceMax0,
propertyName1,
sourceMin1,
sourceMax1
) {
return {
text: title,
onselect: function () {
if (Cesium.defined(propertyName0) && Cesium.defined(propertyName1)) {
app.currentCustomShader = PropertyTextureShaders.createShader2D(
propertyName0,
sourceMin0,
sourceMax0,
propertyName1,
sourceMin1,
sourceMax1
);
} else if (Cesium.defined(propertyName0)) {
app.currentCustomShader = PropertyTextureShaders.createShader1D(
propertyName0,
sourceMin0,
sourceMax0
);
} else {
app.currentCustomShader = undefined;
}

document.getElementById("texture-threshold-control").className =
"hidden";
},
};
}

/**
* 创建一个对象,该对象可用作 Sandcastle 工具栏菜单选项中的一个条目。
*
* 它将允许为指定属性选择“阈值着色器”。
*
* @param {string} title The title to be displayed in the combo box
* @param {string|undefined} propertyName0 The property name 0
* @param {number} sourceMin0 The minimum source value 0
* @param {number} sourceMax0 The maximum source value 0
* @param {string|undefined} propertyName1 The property name 1
* @param {number} sourceMin1 The minimum source value 1
* @param {number} sourceMax1 The maximum source value 1
* @returns The shader option
* @private
*/
static createThresholdShaderOption(
title,
propertyName0,
sourceMin0,
sourceMax0,
propertyName1,
sourceMin1,
sourceMax1
) {
return {
text: title,
onselect: function () {
if (Cesium.defined(propertyName0) && Cesium.defined(propertyName1)) {
app.currentCustomShader =
PropertyTextureShaders.createThresholdShader2D(
propertyName0,
sourceMin0,
sourceMax0,
propertyName1,
sourceMin1,
sourceMax1
);
} else if (Cesium.defined(propertyName0)) {
app.currentCustomShader =
PropertyTextureShaders.createThresholdShader1D(
propertyName0,
sourceMin0,
sourceMax0
);
} else {
app.currentCustomShader = undefined;
}

document.getElementById("texture-threshold-control").className = "";
},
};
}

/**
* 为 Sandcastle 工具栏菜单创建着色器选项
*/
static createShaderOptions() {
// Note: The value of `16.0` that appears here corresponds
// to the "ppeTextures[i].traits.max" value that is found
// in the NGA_gpm_local extension object that is contained
// in the meshPrimitive objects

const shaderOptions = [
PropertyTextureShaders.createShaderOption(
"默认着色",
undefined,
0.0,
16.0,
undefined,
0.0,
16.0
),
PropertyTextureShaders.createShaderOption(
"局部垂直不确定性",
"SIGZ",
0.0,
16.0,
undefined,
0.0,
16.0
),
PropertyTextureShaders.createShaderOption(
"局部径向不确定性",
"SIGR",
0.0,
16.0,
undefined,
0.0,
16.0
),
PropertyTextureShaders.createShaderOption(
"局部组合不确定性",
"SIGZ",
0.0,
16.0,
"SIGR",
0.0,
16.0
),
PropertyTextureShaders.createThresholdShaderOption(
"局部垂直阈值",
"SIGZ",
0.0,
16.0,
undefined,
0.0,
16.0
),
PropertyTextureShaders.createThresholdShaderOption(
"局部径向阈值",
"SIGR",
0.0,
16.0,
undefined,
0.0,
16.0
),
PropertyTextureShaders.createThresholdShaderOption(
"局部组合阈值",
"SIGZ",
0.0,
16.0,
"SIGR",
0.0,
16.0
),
];
return shaderOptions;
}
}

//============================================================================
// 纹理拾取:拾取位置处的局部不确定性标签和椭球

class TexturePickingHandler {
constructor() {
// 当前是否启用纹理拾取
this.enabled = false;

// 当前椭球体基元展示不确定性半径
this.errorEllipsoidPrimitive = undefined;

const errorLabels = viewer.scene.primitives.add(
new Cesium.LabelCollection()
);
// 标签显示的不确定性值
this.errorLabel = errorLabels.add({
position: Cesium.Cartesian3.fromDegrees(0, 0),
show: false,
text: "",
font: "20px sans-serif",
showBackground: true,
disableDepthTestDistance: 1e15,
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
pixelOffset: new Cesium.Cartesian2(0.0, 5.0),
pixelOffsetScaleByDistance: new Cesium.NearFarScalar(50, 0.0, 2000, -2.0),
});

// 安装处理程序,从纹理中挑选不确定值,并更新错误椭圆体和标签
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
const that = this;
handler.setInputAction(function (movement) {
if (!that.enabled) {
return;
}
const worldPosition = viewer.scene.pickPosition(movement.position);
if (!Cesium.defined(worldPosition)) {
that.clear();
} else {
const uncertainty = pickUncertaintyFromTexture(movement.position);
that.update(worldPosition, uncertainty.x, uncertainty.y);
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
handler.setInputAction(function () {
if (!that.enabled) {
return;
}
that.clear();
}, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
}

/**
* 设置当前是否启用纹理不确定性拾取
*
* @param {boolean} enabled The state
*/
setEnabled(enabled) {
this.enabled = enabled;
if (!enabled) {
this.clear();
}
}

/**
* 隐藏错误标签,并移除错误椭圆体
*/
clear() {
viewer.scene.primitives.remove(this.errorEllipsoidPrimitive);
this.errorEllipsoidPrimitive = undefined;
this.errorLabel.show = false;
}

/**
* 更新给定参数的错误标签和椭球基元。
*
* @param {Cartesian3} worldPosition The position for the label
* and ellipsoid
* @param {number} sizeX The radial size (uncertainty)
* @param {number} sizeY The vertical size (uncertainty)
* @private
*/
update(center, sizeX, sizeY) {
this.updateErrorLabel(center, sizeX, sizeY);
this.updateErrorEllipsoidPrimitive(center, sizeX, sizeY);
}

/**
* 更新给定参数的错误标签。
*
* @param {Cartesian3} worldPosition The position for the label
* @param {number} sizeX The radial size (uncertainty)
* @param {number} sizeY The vertical size (uncertainty)
* @private
*/
updateErrorLabel(center, sizeX, sizeY) {
const cartographic = Cesium.Cartographic.fromCartesian(center);
cartographic.height += sizeY + 1.0;
const labelPosition = Cesium.Cartographic.toCartesian(cartographic);
this.errorLabel.position = labelPosition;
this.errorLabel.show = true;
const radialString = sizeX === 16.0 ? "(unknown)" : `${sizeX.toFixed(4)}m`;
const verticalString =
sizeY === 16.0 ? "(unknown)" : `${sizeY.toFixed(4)}m`;
this.errorLabel.text =
`Local Radial Uncertainty: ${radialString}\n` +
`Local Vertical Uncertainty: ${verticalString}`;
}

/**
* 更新给定参数的错误椭球体基元。
*
* @param {Cartesian3} worldPosition The position for the ellipsoid
* @param {number} sizeX The radial size (uncertainty)
* @param {number} sizeY The vertical size (uncertainty)
* @private
*/
updateErrorEllipsoidPrimitive(center, sizeX, sizeY) {
viewer.scene.primitives.remove(this.errorEllipsoidPrimitive);
const modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(center);
const ellipsoidGeometry = new Cesium.EllipsoidGeometry({
radii: new Cesium.Cartesian3(sizeX, sizeX, sizeY),
vertexFormat: Cesium.PerInstanceColorAppearance.VERTEX_FORMAT,
});
const ellipsoid = new Cesium.GeometryInstance({
geometry: ellipsoidGeometry,
modelMatrix: modelMatrix,
attributes: {
color: Cesium.ColorGeometryInstanceAttribute.fromColor(
Cesium.Color.BLUE.withAlpha(0.6)
),
depthFailColor: Cesium.ColorGeometryInstanceAttribute.fromColor(
Cesium.Color.BLUE.withAlpha(0.2)
),
},
});
this.errorEllipsoidPrimitive = new Cesium.Primitive({
geometryInstances: [ellipsoid],
appearance: new Cesium.PerInstanceColorAppearance({
closed: true,
translucent: true,
}),
depthFailAppearance: new Cesium.PerInstanceColorAppearance({
closed: true,
translucent: true,
}),
asynchronous: false,
});
viewer.scene.primitives.add(this.errorEllipsoidPrimitive);
}
}

const texturePickingHandler = new TexturePickingHandler();
app.addCurrentTilesetChangedListener(function () {
texturePickingHandler.clear();
});

//============================================================================
// Anchor point visualization 锚点可视化

class AnchorPointVisualizer {
constructor() {
// 从锚点 JSON 对象到其实体的映射
this.anchorPointEntities = new Map();

// 显示锚点不确定性值的标签
this.anchorPointLabelCollection = viewer.scene.primitives.add(
new Cesium.LabelCollection()
);

//锚点处的标签(包含标准差)是否可见
this.anchorPointLabelsVisible = false;

// 从锚点 JSON 对象到其标签的映射
this.anchorPointLabels = new Map();

// 锚点 JSON 对象的集合,其实体和标签应添加到下一帧中的 anchorPointEntities 中
this.anchorPointsForNextFrame = new Set();

// 对于每个可见的文件,获取其锚点并将它们添加到 anchorPointsForNextFrame 集合中
const that = this;
this.tileVisibleListener = function (tile) {
const anchorPointsIndirect = that.obtainAnchorPoints(tile);
if (Cesium.defined(anchorPointsIndirect)) {
for (const anchorPoint of anchorPointsIndirect) {
that.anchorPointsForNextFrame.add(anchorPoint);
}
}
};

this.initialize();
}

/**
* 通过将所需的侦听器附加到场景和tileset来初始化锚点可视化效果
*
* @private
*/
initialize() {
const that = this;

// 在渲染前,清除下一帧应显示的锚点集合。在瓦片集的渲染过程中,
// 将使用附加到瓦片集上的 tileVisibleListener 填充应渲染的锚点集合。
viewer.scene.preRender.addEventListener(function () {
that.anchorPointsForNextFrame.clear();
});

// 触发更新以将 tileVisibleListener 绑定到当前图块集(如果已定义)
this.updateForCurrentTilesetChanged(undefined, app.currentTileset);

// 渲染后,根据需要从 anchorPointEntities 中添加/移除元素来更新锚点可视化
viewer.scene.postRender.addEventListener(function () {
// 对于所有应在下一帧中可见但尚未可见的锚点:在查看器中创建一个实体,并将其添加到 anchorPointEntities 中,
// 同时创建一个标签并将其添加到 anchorPointLabelCollection 和 anchorPointLabels 中
for (const anchorPoint of that.anchorPointsForNextFrame) {
if (
!that.anchorPointEntities.has(anchorPoint) ||
!that.anchorPointLabels.has(anchorPoint)
) {
const anchorPointVisualizationData =
that.createAnchorPointVisualizationData(anchorPoint);
if (!that.anchorPointEntities.has(anchorPoint)) {
const anchorPointEntity = that.createAnchorPointEntity(
anchorPointVisualizationData
);
that.anchorPointEntities.set(anchorPoint, anchorPointEntity);
}

if (!that.anchorPointLabels.has(anchorPoint)) {
const anchorPointLabel = that.createAnchorPointLabel(
anchorPointVisualizationData
);
that.anchorPointLabels.set(anchorPoint, anchorPointLabel);
}
}
}

// 对于当前可见但不应在下一帧中显示的所有锚点:从查看器和 anchorPointEntities 中移除实体,
// 并从 anchorPointLabelCollection 和 anchorPointLabels 中移除标签
for (const anchorPoint of that.anchorPointEntities.keys()) {
if (!that.anchorPointsForNextFrame.has(anchorPoint)) {
const anchorPointEntity = that.anchorPointEntities.get(anchorPoint);
viewer.entities.remove(anchorPointEntity);
that.anchorPointEntities.delete(anchorPoint);
}
}
for (const anchorPoint of that.anchorPointLabels.keys()) {
if (!that.anchorPointsForNextFrame.has(anchorPoint)) {
const anchorPointLabel = that.anchorPointLabels.get(anchorPoint);
that.anchorPointLabelCollection.remove(anchorPointLabel);
that.anchorPointLabels.delete(anchorPoint);
}
}
});
}

/**
* 设置在锚点处显示标准差的标签当前是否可见。
*
* @param {boolean} anchorPointLabelsVisible
*/
setAnchorPointLabelsVisible(anchorPointLabelsVisible) {
if (this.anchorPointLabelsVisible === anchorPointLabelsVisible) {
return;
}
this.anchorPointLabelsVisible = anchorPointLabelsVisible;
// 显然,切换`show`标志的开/关有时会导致背景不再显示。这可能是 CesiumJS 的问题,需要进一步调查。
// 目前,只需在标志更改时移除所有标签,这将导致它们在下一帧中重新创建...
const labels = this.anchorPointLabels.values();
for (const label of labels) {
this.anchorPointLabelCollection.remove(label);
}
this.anchorPointLabels.clear();
}

/**
* 创建给定锚点可视化的数据。
*
* 结果将是一个具有以下属性的对象:
* - anchorPoint: 给定的anchorPoint
* - position: 一个Cartesian3坐标形式的位置
* - orientation: 表示椭圆体方向的四元数
* - standardDeviations: 包含标准差的 Cartesian3,作为椭球半径的基础
*
* @param {AnchorPointIndirect} anchorPoint 锚点,位于 `NGA_gpm_local` `anchorPointsIndirect` 数组中
* @returns 可视化数据
*/
createAnchorPointVisualizationData(anchorPoint) {
const position = anchorPoint.position;

// 计算协方差矩阵的特征分解
const covarianceMatrix = anchorPoint.covarianceMatrix;
const eigenDecomposition = {
unitary: new Cesium.Matrix3(),
diagonal: new Cesium.Matrix3(),
};
Cesium.Matrix3.computeEigenDecomposition(
covarianceMatrix,
eigenDecomposition
);

// 特征值是对角矩阵对角线上的元素
const eigenValue0 = eigenDecomposition.diagonal[0];
const eigenValue1 = eigenDecomposition.diagonal[4];
const eigenValue2 = eigenDecomposition.diagonal[8];

// 特征向量是单位矩阵的列。方向直接从这个矩阵中计算得出。
const orientation = Cesium.Quaternion.fromRotationMatrix(
eigenDecomposition.unitary,
new Cesium.Quaternion()
);

// 特征值包含方差。其平方根是以米为单位的标准差,
// 用于锚点椭球的半径(乘以 anchorPointEllipsoidScaling)。
const standardDeviations = new Cesium.Cartesian3(
Math.sqrt(eigenValue0),
Math.sqrt(eigenValue1),
Math.sqrt(eigenValue2)
);

return {
anchorPoint: anchorPoint,
position: position,
orientation: orientation,
standardDeviations: standardDeviations,
};
}

/**
* 在锚点位置创建一个标签,显示给定可视化数据的标准差。
*
* @param {object} anchorPointVisualizationData 锚点可视化数据(请参阅 createAnchorPointVisualizationData)
* @returns 标签
*/
createAnchorPointLabel(anchorPointVisualizationData) {
const standardDeviations = anchorPointVisualizationData.standardDeviations;
const x = standardDeviations.x;
const y = standardDeviations.y;
const z = standardDeviations.z;
const text = `${x.toFixed(3)}m, ${y.toFixed(3)}m, ${z.toFixed(3)}m`;

const labelPosition = anchorPointVisualizationData.position;
const anchorPointLabel = this.anchorPointLabelCollection.add({
position: labelPosition,
show: this.anchorPointLabelsVisible,
text: text,
font: "12px sans-serif",
showBackground: true,
disableDepthTestDistance: 1e15,
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
pixelOffset: new Cesium.Cartesian2(0.0, 5.0),
pixelOffsetScaleByDistance: new Cesium.NearFarScalar(50, 0.0, 2000, -2.0),
});
return anchorPointLabel;
}

/**
* 为给定的锚点创建 CesiumJS 实体
*
* @param {object} anchorPointVisualizationData 锚点可视化数据(请参阅 createAnchorPointVisualizationData)
* @returns CesiumJS 实体
* @private
*/
createAnchorPointEntity(anchorPointVisualizationData) {
const position = anchorPointVisualizationData.position;
const orientation = anchorPointVisualizationData.orientation;
const standardDeviations = anchorPointVisualizationData.standardDeviations;

const radii = new Cesium.Cartesian3();
const radiiProperty = new Cesium.CallbackProperty(function () {
const baseRadius = app.anchorPointEllipsoidScaling;
Cesium.Cartesian3.multiplyByScalar(standardDeviations, baseRadius, radii);
return radii;
}, false);

const anchorPointEntity = viewer.entities.add({
position: position,
orientation: orientation,
ellipsoid: {
radii: radiiProperty,
material: Cesium.Color.BLUE.withAlpha(0.5),
outline: true,
outlineColor: Cesium.Color.BLACK,
},
});
return anchorPointEntity;
}

/**
* 从给定的标题中获取内容,并从该内容中获取`NGA_gpm_local`扩展对象,
* 然后返回`anchorPointsIndirect`,如果任何元素为`undefined`,则返回`undefined`。
*
* @param {Tile} tile The tile
* @returns The anchor points, or undefined
*/
obtainAnchorPoints(tile) {
const content = tile?.content;
const modelContent =
content instanceof Cesium.Model3DTileContent ? content : undefined;
if (!Cesium.defined(modelContent)) {
return undefined;
}

const extensionObject = modelContent.getExtension("NGA_gpm_local");
if (!Cesium.defined(extensionObject)) {
return undefined;
}
return extensionObject.anchorPointsIndirect;
}

/**
* 每当所选Tileset发生变化时,将通过 “App” currentTilesetChangedListener 调用。
*
* @param {Cesium3DTileset|undefined} oldCurrentTileset The old tileset
* @param {Cesium3DTileset|undefined} newCurrentTileset The new tileset
*/
updateForCurrentTilesetChanged(oldCurrentTileset, newCurrentTileset) {
if (Cesium.defined(oldCurrentTileset)) {
oldCurrentTileset.tileVisible.removeEventListener(
this.tileVisibleListener
);
}
if (Cesium.defined(newCurrentTileset)) {
newCurrentTileset.tileVisible.addEventListener(this.tileVisibleListener);
}
}
}

const anchorPointVisualizer = new AnchorPointVisualizer();
app.addCurrentTilesetChangedListener(function (
oldCurrentTileset,
newCurrentTileset
) {
anchorPointVisualizer.updateForCurrentTilesetChanged(
oldCurrentTileset,
newCurrentTileset
);
});

//============================================================================
// ViewModel for Sandcastle UI ----------Sandcastle UI 的 ViewModel

const interactionOptions = [
{
text: "View",
onselect: function () {
texturePickingHandler.setEnabled(false);
},
},
{
text: "Pick",
onselect: function () {
texturePickingHandler.setEnabled(true);
},
},
];

const shaderOptions = PropertyTextureShaders.createShaderOptions();

const viewModel = {
textureThreshold: 3.0,
anchorPointEllipsoidScaling: app.anchorPointEllipsoidScaling,
anchorPointLabelsVisible: app.anchorPointLabelsVisible,
dataSetNames: dataSetOptions.map((e) => e.name),
dataSetName: "卡纳维拉尔港",
zoomToCurrentTileset: function () {
app.zoomToCurrentTileset();
},
interactionModes: interactionOptions.map((e) => e.text),
interactionMode: "Pick",
shaderModes: shaderOptions.map((e) => e.text),
shaderMode: "Default Shading",
};

// 默认启用纹理拾取模式
texturePickingHandler.setEnabled(true);

async function updateModelFromView() {
app.anchorPointEllipsoidScaling = Number(
viewModel.anchorPointEllipsoidScaling
);
const anchorPointLabelsVisible = Boolean(viewModel.anchorPointLabelsVisible);
anchorPointVisualizer.setAnchorPointLabelsVisible(anchorPointLabelsVisible);
app.textureThreshold = Number(viewModel.textureThreshold);
await app.selectCurrentDataSet(viewModel.dataSetName);
for (const interactionOption of interactionOptions) {
if (interactionOption.text === viewModel.interactionMode) {
interactionOption.onselect();
}
}
for (const shaderOption of shaderOptions) {
if (shaderOption.text === viewModel.shaderMode) {
shaderOption.onselect();
}
}
}

Cesium.knockout.track(viewModel);
const toolbar = document.getElementById("toolbar");
Cesium.knockout.applyBindings(viewModel, toolbar);
for (const name in viewModel) {
if (viewModel.hasOwnProperty(name)) {
Cesium.knockout
.getObservable(viewModel, name)
.subscribe(updateModelFromView);
}
}

await app.selectCurrentDataSet(dataSetOptions[0].name);