cesium-xs-sdk 1.0.0 → 1.0.1
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/cesium-xs-sdk.cjs.js +1271 -16
- package/dist/cesium-xs-sdk.cjs.js.map +1 -1
- package/dist/cesium-xs-sdk.esm.js +1271 -16
- package/dist/cesium-xs-sdk.esm.js.map +1 -1
- package/dist/cesium-xs-sdk.umd.js +1 -1
- package/dist/cesium-xs-sdk.umd.js.map +1 -1
- package/package.json +9 -3
- package/src/controls/BaseControl.js +264 -0
- package/src/controls/Compass.js +312 -0
- package/src/controls/DistanceLegend.js +130 -0
- package/src/controls/LocationBar.js +182 -0
- package/src/controls/Zoom.js +128 -0
- package/src/controls/index.js +5 -0
- package/src/core/ModuleManager.js +28 -0
- package/src/index.d.ts +67 -1
- package/src/index.js +15 -0
- package/src/modules/ControlModule.js +200 -0
- package/src/modules/tools/DeleteTool.js +2 -2
- package/src/modules/tools/EditTool.js +3 -3
- package/src/modules/tools/SelectTool.js +15 -5
- package/src/utils/PlotGeometryUtil.js +10 -6
|
@@ -1005,12 +1005,13 @@ class PlotGeometryUtil {
|
|
|
1005
1005
|
let angleDiff = endAngle - startAngle;
|
|
1006
1006
|
angleDiff = angleDiff < 0 ? angleDiff + Math.PI * 2 : angleDiff;
|
|
1007
1007
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1008
|
+
// 所有圆弧点必须严格在 radiusMeters 为半径的圆上,不能直接使用输入控制点,
|
|
1009
|
+
// 否则鼠标位置到圆心的距离可能与半径不一致,导致扇形闭合边出现三角形或穿入内部。
|
|
1010
|
+
const points = [];
|
|
1011
|
+
for (let i = 0; i <= segments; i++) {
|
|
1010
1012
|
const angle = startAngle + (angleDiff * i) / segments;
|
|
1011
1013
|
points.push(this._getPointAtBearingAndDistance(center, radiusMeters, angle));
|
|
1012
1014
|
}
|
|
1013
|
-
points.push(endPoint);
|
|
1014
1015
|
return points;
|
|
1015
1016
|
}
|
|
1016
1017
|
|
|
@@ -1450,11 +1451,14 @@ class PlotGeometryUtil {
|
|
|
1450
1451
|
// 使用球面距离作为半径,与圆弧插值函数保持一致
|
|
1451
1452
|
const radius = this._calcSphericalDistance(pnt2, center);
|
|
1452
1453
|
|
|
1453
|
-
//
|
|
1454
|
+
// 弧线精确经过两个控制点方向,半径统一为圆心到起始控制点的球面距离,
|
|
1455
|
+
// 避免结束控制点(鼠标位置)到圆心距离与半径不一致导致扇形畸变。
|
|
1454
1456
|
const arcPoints = this._getGeoArcPointsBetween(center, pnt2, pnt3, radius);
|
|
1455
|
-
arcPoints.push(center);
|
|
1456
1457
|
|
|
1457
|
-
|
|
1458
|
+
// 扇形轮廓:圆心 -> 圆弧起点 -> 圆弧终点 -> 回到圆心(Cesium polygon 自动闭合)
|
|
1459
|
+
const sectorPoints = [center, ...arcPoints];
|
|
1460
|
+
|
|
1461
|
+
return this.toPositions(sectorPoints, height);
|
|
1458
1462
|
}
|
|
1459
1463
|
|
|
1460
1464
|
/**
|
|
@@ -4387,7 +4391,7 @@ class EditTool extends BaseTool {
|
|
|
4387
4391
|
const screenPos = movement.position || (movement && movement.endPosition);
|
|
4388
4392
|
if (!screenPos) return;
|
|
4389
4393
|
|
|
4390
|
-
const picked = this._viewer.scene.pick(
|
|
4394
|
+
const picked = this._viewer.scene.pick(screenPos);
|
|
4391
4395
|
console.log('EditTool _onClick:', { picked, screenPos });
|
|
4392
4396
|
|
|
4393
4397
|
if (Cesium.defined(picked) && Cesium.defined(picked.id)) {
|
|
@@ -4425,7 +4429,7 @@ class EditTool extends BaseTool {
|
|
|
4425
4429
|
|
|
4426
4430
|
// 如果已经选中了图元,检查是否点击了控制点
|
|
4427
4431
|
if (this._selectedEntity && !this._isDragging) {
|
|
4428
|
-
const picked = this._viewer.scene.pick(
|
|
4432
|
+
const picked = this._viewer.scene.pick(screenPos);
|
|
4429
4433
|
if (Cesium.defined(picked) && Cesium.defined(picked.id)) {
|
|
4430
4434
|
const entityId = picked.id.id;
|
|
4431
4435
|
if (entityId && (entityId.startsWith('__edit_handle_') || entityId.includes('__edit_handle_'))) {
|
|
@@ -4478,7 +4482,7 @@ class EditTool extends BaseTool {
|
|
|
4478
4482
|
this._lastMoveTime = now;
|
|
4479
4483
|
|
|
4480
4484
|
// 检测是否悬停在控制点上
|
|
4481
|
-
const picked = this._viewer.scene.pick(
|
|
4485
|
+
const picked = this._viewer.scene.pick(screenPos);
|
|
4482
4486
|
const isOverHandle = Cesium.defined(picked) && Cesium.defined(picked.id) &&
|
|
4483
4487
|
picked.id.id && picked.id.id.startsWith('__edit_handle_');
|
|
4484
4488
|
this._setCursorStyle(isOverHandle ? 'move' : 'default');
|
|
@@ -5970,7 +5974,7 @@ class SelectTool extends BaseTool {
|
|
|
5970
5974
|
const screenPos = movement.position || (movement && movement.endPosition);
|
|
5971
5975
|
if (!screenPos) return;
|
|
5972
5976
|
|
|
5973
|
-
const picked = this._viewer.scene.pick(
|
|
5977
|
+
const picked = this._viewer.scene.pick(screenPos);
|
|
5974
5978
|
|
|
5975
5979
|
if (Cesium.defined(picked) && Cesium.defined(picked.id)) {
|
|
5976
5980
|
const entityId = picked.id.id;
|
|
@@ -6024,6 +6028,7 @@ class SelectTool extends BaseTool {
|
|
|
6024
6028
|
}
|
|
6025
6029
|
|
|
6026
6030
|
_updateSelectionRectPreview(currentPos) {
|
|
6031
|
+
this._currentEndPos = currentPos;
|
|
6027
6032
|
if (this._selectType === SelectType.RECTANGLE) {
|
|
6028
6033
|
this._updateRectPreview(currentPos);
|
|
6029
6034
|
} else if (this._selectType === SelectType.CIRCLE) {
|
|
@@ -6041,7 +6046,10 @@ class SelectTool extends BaseTool {
|
|
|
6041
6046
|
this._selectionRect = this.createTempEntity(this.generateId('sel_rect'), {
|
|
6042
6047
|
polyline: {
|
|
6043
6048
|
positions: new Cesium.CallbackProperty(() => {
|
|
6044
|
-
const
|
|
6049
|
+
const c1 = this._startPos;
|
|
6050
|
+
const c2 = this._currentEndPos;
|
|
6051
|
+
if (!c1 || !c2) return [];
|
|
6052
|
+
const pos = this._getRectPositions(c1, c2);
|
|
6045
6053
|
return Cesium.Cartesian3.fromDegreesArrayHeights(pos.flatMap(c => [c[0], c[1], 0]));
|
|
6046
6054
|
}, false),
|
|
6047
6055
|
material: Cesium.Color.CYAN,
|
|
@@ -6049,7 +6057,10 @@ class SelectTool extends BaseTool {
|
|
|
6049
6057
|
},
|
|
6050
6058
|
polygon: {
|
|
6051
6059
|
hierarchy: new Cesium.CallbackProperty(() => {
|
|
6052
|
-
const
|
|
6060
|
+
const c1 = this._startPos;
|
|
6061
|
+
const c2 = this._currentEndPos;
|
|
6062
|
+
if (!c1 || !c2) return new Cesium.PolygonHierarchy([]);
|
|
6063
|
+
const pos = this._getRectPositions(c1, c2);
|
|
6053
6064
|
return new Cesium.PolygonHierarchy(
|
|
6054
6065
|
Cesium.Cartesian3.fromDegreesArrayHeights(pos.flatMap(c => [c[0], c[1], 0]))
|
|
6055
6066
|
);
|
|
@@ -6072,8 +6083,11 @@ class SelectTool extends BaseTool {
|
|
|
6072
6083
|
this._selectionRect = this.createTempEntity(this.generateId('sel_circle'), {
|
|
6073
6084
|
polygon: {
|
|
6074
6085
|
hierarchy: new Cesium.CallbackProperty(() => {
|
|
6075
|
-
const
|
|
6076
|
-
const
|
|
6086
|
+
const c = this._startPos;
|
|
6087
|
+
const edge = this._currentEndPos;
|
|
6088
|
+
if (!c || !edge) return new Cesium.PolygonHierarchy([]);
|
|
6089
|
+
const radius = this._calcDistance(c, edge);
|
|
6090
|
+
const positions = this._generateCirclePositions(c, radius, 36);
|
|
6077
6091
|
return new Cesium.PolygonHierarchy(
|
|
6078
6092
|
Cesium.Cartesian3.fromDegreesArrayHeights(
|
|
6079
6093
|
positions.flatMap(c => [c[0], c[1], 0])
|
|
@@ -6459,7 +6473,7 @@ class DeleteTool extends BaseTool {
|
|
|
6459
6473
|
const screenPos = movement.position || (movement && movement.endPosition);
|
|
6460
6474
|
if (!screenPos) return;
|
|
6461
6475
|
|
|
6462
|
-
const picked = this._viewer.scene.pick(
|
|
6476
|
+
const picked = this._viewer.scene.pick(screenPos);
|
|
6463
6477
|
|
|
6464
6478
|
if (Cesium.defined(picked) && Cesium.defined(picked.id)) {
|
|
6465
6479
|
const entityId = picked.id.id;
|
|
@@ -6481,7 +6495,7 @@ class DeleteTool extends BaseTool {
|
|
|
6481
6495
|
const screenPos = data.position || data.endPosition;
|
|
6482
6496
|
if (!screenPos) return;
|
|
6483
6497
|
|
|
6484
|
-
const picked = this._viewer.scene.pick(
|
|
6498
|
+
const picked = this._viewer.scene.pick(screenPos);
|
|
6485
6499
|
|
|
6486
6500
|
let hoveredId = null;
|
|
6487
6501
|
|
|
@@ -7022,6 +7036,1213 @@ class EditorModule {
|
|
|
7022
7036
|
}
|
|
7023
7037
|
}
|
|
7024
7038
|
|
|
7039
|
+
/**
|
|
7040
|
+
* 控件基类
|
|
7041
|
+
* 所有地图控件的抽象基类,提供 DOM 容器管理、挂载/移除、事件绑定等通用能力。
|
|
7042
|
+
* 参考 Mars3D BaseControl 设计,但保持轻量,不依赖 jQuery。
|
|
7043
|
+
*/
|
|
7044
|
+
class BaseControl {
|
|
7045
|
+
/**
|
|
7046
|
+
* @param {object} options 控件配置
|
|
7047
|
+
* @param {string} options.type 控件类型标识
|
|
7048
|
+
* @param {string} [options.className] 容器额外 CSS 类名
|
|
7049
|
+
* @param {string|HTMLElement} [options.container] 自定义父容器,默认挂到 viewer.container
|
|
7050
|
+
* @param {number} [options.bottom=0] 底部偏移(px)
|
|
7051
|
+
* @param {number} [options.left] 左侧偏移(px)
|
|
7052
|
+
* @param {number} [options.right] 右侧偏移(px)
|
|
7053
|
+
* @param {number} [options.top] 顶部偏移(px)
|
|
7054
|
+
* @param {boolean} [options.visible=true] 初始是否可见
|
|
7055
|
+
*/
|
|
7056
|
+
constructor(options = {}) {
|
|
7057
|
+
this._type = options.type || 'base';
|
|
7058
|
+
this._options = options;
|
|
7059
|
+
this._viewer = null;
|
|
7060
|
+
this._container = null;
|
|
7061
|
+
this._parentContainer = null;
|
|
7062
|
+
this._isAdded = false;
|
|
7063
|
+
this._handlers = [];
|
|
7064
|
+
}
|
|
7065
|
+
|
|
7066
|
+
/**
|
|
7067
|
+
* 控件类型
|
|
7068
|
+
*/
|
|
7069
|
+
get type() {
|
|
7070
|
+
return this._type;
|
|
7071
|
+
}
|
|
7072
|
+
|
|
7073
|
+
/**
|
|
7074
|
+
* 获取控件 DOM 容器
|
|
7075
|
+
*/
|
|
7076
|
+
get container() {
|
|
7077
|
+
return this._container;
|
|
7078
|
+
}
|
|
7079
|
+
|
|
7080
|
+
/**
|
|
7081
|
+
* 创建控件 DOM 容器
|
|
7082
|
+
* 子类可重写以自定义容器结构
|
|
7083
|
+
*/
|
|
7084
|
+
_createContainer() {
|
|
7085
|
+
const div = document.createElement('div');
|
|
7086
|
+
div.className = `xs3d-control xs3d-control-${this._type}`;
|
|
7087
|
+
if (this._options.className) {
|
|
7088
|
+
div.className += ` ${this._options.className}`;
|
|
7089
|
+
}
|
|
7090
|
+
div.style.position = 'absolute';
|
|
7091
|
+
div.style.zIndex = '1000';
|
|
7092
|
+
div.style.pointerEvents = 'auto';
|
|
7093
|
+
|
|
7094
|
+
const positions = ['left', 'right', 'top', 'bottom'];
|
|
7095
|
+
positions.forEach(key => {
|
|
7096
|
+
if (this._options[key] !== undefined) {
|
|
7097
|
+
div.style[key] = typeof this._options[key] === 'number'
|
|
7098
|
+
? `${this._options[key]}px`
|
|
7099
|
+
: this._options[key];
|
|
7100
|
+
}
|
|
7101
|
+
});
|
|
7102
|
+
|
|
7103
|
+
if (this._options.visible === false) {
|
|
7104
|
+
div.style.display = 'none';
|
|
7105
|
+
}
|
|
7106
|
+
|
|
7107
|
+
return div;
|
|
7108
|
+
}
|
|
7109
|
+
|
|
7110
|
+
/**
|
|
7111
|
+
* 获取父容器
|
|
7112
|
+
* 子类可重写,例如挂到 Cesium 工具栏
|
|
7113
|
+
*/
|
|
7114
|
+
_getParentContainer() {
|
|
7115
|
+
if (this._options.container) {
|
|
7116
|
+
if (typeof this._options.container === 'string') {
|
|
7117
|
+
return document.querySelector(this._options.container);
|
|
7118
|
+
}
|
|
7119
|
+
return this._options.container;
|
|
7120
|
+
}
|
|
7121
|
+
return this._viewer?.container;
|
|
7122
|
+
}
|
|
7123
|
+
|
|
7124
|
+
/**
|
|
7125
|
+
* 获取或创建 SDK 统一工具栏(xs3d-viewer-toolbar)
|
|
7126
|
+
* 供内置控件共享一个右上角的垂直工具栏容器。
|
|
7127
|
+
*/
|
|
7128
|
+
_getOrCreateToolbar() {
|
|
7129
|
+
const viewerContainer = this._viewer?.container;
|
|
7130
|
+
if (!viewerContainer) return null;
|
|
7131
|
+
|
|
7132
|
+
let toolbar = viewerContainer.querySelector('.xs3d-viewer-toolbar');
|
|
7133
|
+
if (!toolbar) {
|
|
7134
|
+
toolbar = document.createElement('div');
|
|
7135
|
+
toolbar.className = 'xs3d-viewer-toolbar';
|
|
7136
|
+
toolbar.style.position = 'absolute';
|
|
7137
|
+
toolbar.style.top = '5px';
|
|
7138
|
+
toolbar.style.right = '5px';
|
|
7139
|
+
toolbar.style.display = 'flex';
|
|
7140
|
+
toolbar.style.flexDirection = 'column';
|
|
7141
|
+
toolbar.style.alignItems = 'center';
|
|
7142
|
+
toolbar.style.gap = '4px';
|
|
7143
|
+
toolbar.style.padding = '4px';
|
|
7144
|
+
toolbar.style.background = 'rgba(63, 72, 84, 0.95)';
|
|
7145
|
+
toolbar.style.borderRadius = '4px';
|
|
7146
|
+
toolbar.style.boxShadow = '0 1px 4px rgba(0,0,0,0.35)';
|
|
7147
|
+
toolbar.style.zIndex = '1001';
|
|
7148
|
+
viewerContainer.appendChild(toolbar);
|
|
7149
|
+
}
|
|
7150
|
+
return toolbar;
|
|
7151
|
+
}
|
|
7152
|
+
|
|
7153
|
+
/**
|
|
7154
|
+
* 若当前父容器是统一工具栏,清除子容器的 absolute 定位
|
|
7155
|
+
*/
|
|
7156
|
+
_clearAbsolutePositionIfInToolbar() {
|
|
7157
|
+
if (this._parentContainer?.classList?.contains('xs3d-viewer-toolbar') ||
|
|
7158
|
+
this._parentContainer?.classList?.contains('cesium-viewer-toolbar')) {
|
|
7159
|
+
this._container.style.position = 'static';
|
|
7160
|
+
this._container.style.top = '';
|
|
7161
|
+
this._container.style.right = '';
|
|
7162
|
+
this._container.style.bottom = '';
|
|
7163
|
+
this._container.style.left = '';
|
|
7164
|
+
}
|
|
7165
|
+
}
|
|
7166
|
+
|
|
7167
|
+
/**
|
|
7168
|
+
* 挂载到 Cesium Viewer
|
|
7169
|
+
* @param {Cesium.Viewer} viewer
|
|
7170
|
+
*/
|
|
7171
|
+
addTo(viewer) {
|
|
7172
|
+
if (this._isAdded || !viewer) return this;
|
|
7173
|
+
|
|
7174
|
+
this._viewer = viewer;
|
|
7175
|
+
this._parentContainer = this._getParentContainer();
|
|
7176
|
+
this._container = this._createContainer();
|
|
7177
|
+
|
|
7178
|
+
// 若父容器是统一工具栏,子容器取消 absolute 定位,避免被自身 top/right 撑出工具栏
|
|
7179
|
+
if (this._parentContainer?.classList?.contains('xs3d-viewer-toolbar') ||
|
|
7180
|
+
this._parentContainer?.classList?.contains('cesium-viewer-toolbar')) {
|
|
7181
|
+
this._container.style.position = 'static';
|
|
7182
|
+
this._container.style.top = '';
|
|
7183
|
+
this._container.style.right = '';
|
|
7184
|
+
this._container.style.bottom = '';
|
|
7185
|
+
this._container.style.left = '';
|
|
7186
|
+
}
|
|
7187
|
+
|
|
7188
|
+
// 确保父容器有定位,否则 absolute 子元素会相对于 body 定位
|
|
7189
|
+
if (this._parentContainer) {
|
|
7190
|
+
const style = window.getComputedStyle(this._parentContainer);
|
|
7191
|
+
if (style.position === 'static') {
|
|
7192
|
+
this._parentContainer.style.position = 'relative';
|
|
7193
|
+
}
|
|
7194
|
+
}
|
|
7195
|
+
|
|
7196
|
+
// 子类挂载 DOM 内容
|
|
7197
|
+
this._mount();
|
|
7198
|
+
|
|
7199
|
+
if (this._parentContainer) {
|
|
7200
|
+
this._parentContainer.appendChild(this._container);
|
|
7201
|
+
}
|
|
7202
|
+
|
|
7203
|
+
// 绑定地图事件
|
|
7204
|
+
this._bindEvents();
|
|
7205
|
+
|
|
7206
|
+
this._isAdded = true;
|
|
7207
|
+
return this;
|
|
7208
|
+
}
|
|
7209
|
+
|
|
7210
|
+
/**
|
|
7211
|
+
* 从地图移除
|
|
7212
|
+
* @param {boolean} destroy 是否销毁控件
|
|
7213
|
+
*/
|
|
7214
|
+
remove(destroy = true) {
|
|
7215
|
+
if (!this._isAdded) return this;
|
|
7216
|
+
|
|
7217
|
+
this._unbindEvents();
|
|
7218
|
+
|
|
7219
|
+
if (this._container && this._container.parentNode) {
|
|
7220
|
+
this._container.parentNode.removeChild(this._container);
|
|
7221
|
+
}
|
|
7222
|
+
|
|
7223
|
+
if (destroy) {
|
|
7224
|
+
this._destroy();
|
|
7225
|
+
}
|
|
7226
|
+
|
|
7227
|
+
this._container = null;
|
|
7228
|
+
this._parentContainer = null;
|
|
7229
|
+
this._viewer = null;
|
|
7230
|
+
this._isAdded = false;
|
|
7231
|
+
return this;
|
|
7232
|
+
}
|
|
7233
|
+
|
|
7234
|
+
/**
|
|
7235
|
+
* 显示控件
|
|
7236
|
+
*/
|
|
7237
|
+
show() {
|
|
7238
|
+
if (this._container) {
|
|
7239
|
+
this._container.style.display = '';
|
|
7240
|
+
}
|
|
7241
|
+
return this;
|
|
7242
|
+
}
|
|
7243
|
+
|
|
7244
|
+
/**
|
|
7245
|
+
* 隐藏控件
|
|
7246
|
+
*/
|
|
7247
|
+
hide() {
|
|
7248
|
+
if (this._container) {
|
|
7249
|
+
this._container.style.display = 'none';
|
|
7250
|
+
}
|
|
7251
|
+
return this;
|
|
7252
|
+
}
|
|
7253
|
+
|
|
7254
|
+
/**
|
|
7255
|
+
* 注册 Viewer/Cesium 事件处理器
|
|
7256
|
+
* @param {Cesium.Event} event Cesium 事件对象
|
|
7257
|
+
* @param {Function} callback 回调
|
|
7258
|
+
*/
|
|
7259
|
+
_addCesiumHandler(event, callback) {
|
|
7260
|
+
const handler = event.addEventListener(callback);
|
|
7261
|
+
this._handlers.push({ type: 'cesium', event, handler });
|
|
7262
|
+
return handler;
|
|
7263
|
+
}
|
|
7264
|
+
|
|
7265
|
+
/**
|
|
7266
|
+
* 注册原生 DOM 事件
|
|
7267
|
+
*/
|
|
7268
|
+
_addDomEvent(element, eventType, callback, options = false) {
|
|
7269
|
+
element.addEventListener(eventType, callback, options);
|
|
7270
|
+
this._handlers.push({ type: 'dom', element, eventType, callback, options });
|
|
7271
|
+
return this;
|
|
7272
|
+
}
|
|
7273
|
+
|
|
7274
|
+
/**
|
|
7275
|
+
* 解除所有事件绑定
|
|
7276
|
+
*/
|
|
7277
|
+
_unbindEvents() {
|
|
7278
|
+
this._handlers.forEach(h => {
|
|
7279
|
+
if (h.type === 'cesium' && h.event && typeof h.handler === 'function') {
|
|
7280
|
+
h.handler();
|
|
7281
|
+
} else if (h.type === 'dom' && h.element) {
|
|
7282
|
+
h.element.removeEventListener(h.eventType, h.callback, h.options);
|
|
7283
|
+
}
|
|
7284
|
+
});
|
|
7285
|
+
this._handlers = [];
|
|
7286
|
+
}
|
|
7287
|
+
|
|
7288
|
+
/**
|
|
7289
|
+
* 子类重写:创建控件内部 DOM
|
|
7290
|
+
*/
|
|
7291
|
+
_mount() { }
|
|
7292
|
+
|
|
7293
|
+
/**
|
|
7294
|
+
* 子类重写:绑定地图事件
|
|
7295
|
+
*/
|
|
7296
|
+
_bindEvents() { }
|
|
7297
|
+
|
|
7298
|
+
/**
|
|
7299
|
+
* 子类重写:销毁内部资源
|
|
7300
|
+
*/
|
|
7301
|
+
_destroy() { }
|
|
7302
|
+
}
|
|
7303
|
+
|
|
7304
|
+
/**
|
|
7305
|
+
* 状态栏控件
|
|
7306
|
+
* 显示鼠标位置经纬度、海拔、相机方向/俯仰角/视高、地图层级、FPS 等信息。
|
|
7307
|
+
* 支持自定义模板和显示字段。
|
|
7308
|
+
*/
|
|
7309
|
+
class LocationBar extends BaseControl {
|
|
7310
|
+
/**
|
|
7311
|
+
* @param {object} options
|
|
7312
|
+
* @param {string} [options.template='经度:{lng} 纬度:{lat} 海拔:{alt}米 方向:{heading}° 俯仰:{pitch}° 视高:{cameraHeight}米']
|
|
7313
|
+
* @param {number} [options.latDecimal=6] 纬度小数位
|
|
7314
|
+
* @param {number} [options.lngDecimal=6] 经度小数位
|
|
7315
|
+
* @param {boolean} [options.showFPS=false] 是否显示 FPS
|
|
7316
|
+
* @param {number} [options.bottom=0] 底部偏移
|
|
7317
|
+
* @param {number} [options.left=0] 左侧偏移
|
|
7318
|
+
* @param {number} [options.right] 右侧偏移
|
|
7319
|
+
*/
|
|
7320
|
+
constructor(options = {}) {
|
|
7321
|
+
super({
|
|
7322
|
+
type: 'locationBar',
|
|
7323
|
+
bottom: 0,
|
|
7324
|
+
right: 0,
|
|
7325
|
+
...options
|
|
7326
|
+
});
|
|
7327
|
+
|
|
7328
|
+
this._template = options.template || '经度:{lng} 纬度:{lat} 海拔:{alt}米 方向:{heading}° 俯仰:{pitch}° 视高:{cameraHeight}米 层级:{level}';
|
|
7329
|
+
this._latDecimal = options.latDecimal ?? 6;
|
|
7330
|
+
this._lngDecimal = options.lngDecimal ?? 6;
|
|
7331
|
+
this._showFPS = options.showFPS ?? false;
|
|
7332
|
+
this._lastCameraData = {};
|
|
7333
|
+
this._lastMouseData = {};
|
|
7334
|
+
this._fps = 0;
|
|
7335
|
+
}
|
|
7336
|
+
|
|
7337
|
+
_createContainer() {
|
|
7338
|
+
const div = super._createContainer();
|
|
7339
|
+
div.className += ' xs3d-locationbar';
|
|
7340
|
+
div.style.display = 'flex';
|
|
7341
|
+
div.style.alignItems = 'center';
|
|
7342
|
+
div.style.gap = '12px';
|
|
7343
|
+
div.style.padding = '6px 12px';
|
|
7344
|
+
div.style.background = 'rgba(0, 0, 0, 0.55)';
|
|
7345
|
+
div.style.color = '#fff';
|
|
7346
|
+
div.style.fontSize = '12px';
|
|
7347
|
+
div.style.fontFamily = 'Microsoft YaHei, sans-serif';
|
|
7348
|
+
div.style.pointerEvents = 'none';
|
|
7349
|
+
div.style.userSelect = 'none';
|
|
7350
|
+
div.style.whiteSpace = 'nowrap';
|
|
7351
|
+
return div;
|
|
7352
|
+
}
|
|
7353
|
+
|
|
7354
|
+
_mount() {
|
|
7355
|
+
this._updateContent();
|
|
7356
|
+
}
|
|
7357
|
+
|
|
7358
|
+
_bindEvents() {
|
|
7359
|
+
// 鼠标移动:更新鼠标位置信息
|
|
7360
|
+
const canvas = this._viewer.scene.canvas;
|
|
7361
|
+
this._addDomEvent(canvas, 'mousemove', this._onMouseMove.bind(this));
|
|
7362
|
+
this._addDomEvent(canvas, 'mouseleave', this._onMouseLeave.bind(this));
|
|
7363
|
+
|
|
7364
|
+
// 相机变化:更新方向/俯仰/视高/层级
|
|
7365
|
+
this._addCesiumHandler(this._viewer.camera.changed, this._onCameraChanged.bind(this));
|
|
7366
|
+
|
|
7367
|
+
// FPS
|
|
7368
|
+
if (this._showFPS) {
|
|
7369
|
+
this._addCesiumHandler(this._viewer.scene.postRender, this._onPostRender.bind(this));
|
|
7370
|
+
this._fpsFrameCount = 0;
|
|
7371
|
+
this._fpsLastTime = performance.now();
|
|
7372
|
+
}
|
|
7373
|
+
|
|
7374
|
+
// 初始更新一次
|
|
7375
|
+
this._onCameraChanged();
|
|
7376
|
+
}
|
|
7377
|
+
|
|
7378
|
+
_onMouseMove(e) {
|
|
7379
|
+
const rect = this._viewer.scene.canvas.getBoundingClientRect();
|
|
7380
|
+
const x = e.clientX - rect.left;
|
|
7381
|
+
const y = e.clientY - rect.top;
|
|
7382
|
+
const screenPos = new Cesium.Cartesian2(x, y);
|
|
7383
|
+
|
|
7384
|
+
let cartesian = null;
|
|
7385
|
+
|
|
7386
|
+
// 优先使用 globe.pick 获取真实地表(含地形)高度
|
|
7387
|
+
const ray = this._viewer.camera.getPickRay(screenPos);
|
|
7388
|
+
if (ray) {
|
|
7389
|
+
cartesian = this._viewer.scene.globe.pick(ray, this._viewer.scene);
|
|
7390
|
+
}
|
|
7391
|
+
|
|
7392
|
+
// 如果地表不可见,回退到椭球面
|
|
7393
|
+
if (!cartesian) {
|
|
7394
|
+
cartesian = this._viewer.camera.pickEllipsoid(screenPos, this._viewer.scene.globe.ellipsoid);
|
|
7395
|
+
}
|
|
7396
|
+
|
|
7397
|
+
if (cartesian) {
|
|
7398
|
+
const cartographic = Cesium.Cartographic.fromCartesian(cartesian);
|
|
7399
|
+
this._lastMouseData = {
|
|
7400
|
+
lng: Cesium.Math.toDegrees(cartographic.longitude).toFixed(this._lngDecimal),
|
|
7401
|
+
lat: Cesium.Math.toDegrees(cartographic.latitude).toFixed(this._latDecimal),
|
|
7402
|
+
alt: cartographic.height.toFixed(2)
|
|
7403
|
+
};
|
|
7404
|
+
} else {
|
|
7405
|
+
this._lastMouseData = { lng: '-', lat: '-', alt: '-' };
|
|
7406
|
+
}
|
|
7407
|
+
|
|
7408
|
+
this._updateContent();
|
|
7409
|
+
}
|
|
7410
|
+
|
|
7411
|
+
_onMouseLeave() {
|
|
7412
|
+
this._lastMouseData = { lng: '-', lat: '-', alt: '-' };
|
|
7413
|
+
this._updateContent();
|
|
7414
|
+
}
|
|
7415
|
+
|
|
7416
|
+
_onCameraChanged() {
|
|
7417
|
+
const camera = this._viewer.camera;
|
|
7418
|
+
const positionCartographic = camera.positionCartographic;
|
|
7419
|
+
|
|
7420
|
+
this._lastCameraData = {
|
|
7421
|
+
heading: Cesium.Math.toDegrees(camera.heading).toFixed(2),
|
|
7422
|
+
pitch: Cesium.Math.toDegrees(camera.pitch).toFixed(2),
|
|
7423
|
+
cameraHeight: positionCartographic.height.toFixed(2),
|
|
7424
|
+
level: this._getZoomLevel().toFixed(0)
|
|
7425
|
+
};
|
|
7426
|
+
|
|
7427
|
+
this._updateContent();
|
|
7428
|
+
}
|
|
7429
|
+
|
|
7430
|
+
_onPostRender() {
|
|
7431
|
+
this._fpsFrameCount++;
|
|
7432
|
+
const now = performance.now();
|
|
7433
|
+
const elapsed = now - this._fpsLastTime;
|
|
7434
|
+
|
|
7435
|
+
if (elapsed >= 1000) {
|
|
7436
|
+
this._fps = Math.round((this._fpsFrameCount * 1000) / elapsed);
|
|
7437
|
+
this._fpsFrameCount = 0;
|
|
7438
|
+
this._fpsLastTime = now;
|
|
7439
|
+
this._updateContent();
|
|
7440
|
+
}
|
|
7441
|
+
}
|
|
7442
|
+
|
|
7443
|
+
_getZoomLevel() {
|
|
7444
|
+
const camera = this._viewer.camera;
|
|
7445
|
+
const positionCartographic = camera.positionCartographic;
|
|
7446
|
+
|
|
7447
|
+
// 计算相机到正下方地表点的距离(视高),而不是到自己位置的距离
|
|
7448
|
+
const surfaceCartesian = Cesium.Cartesian3.fromRadians(
|
|
7449
|
+
positionCartographic.longitude,
|
|
7450
|
+
positionCartographic.latitude,
|
|
7451
|
+
0
|
|
7452
|
+
);
|
|
7453
|
+
const distance = Cesium.Cartesian3.distance(camera.position, surfaceCartesian);
|
|
7454
|
+
|
|
7455
|
+
if (!distance || distance <= 0 || !Number.isFinite(distance)) return 0;
|
|
7456
|
+
|
|
7457
|
+
// 赤道周长约 40075017 米,映射为类似瓦片层级
|
|
7458
|
+
const equatorCircumference = 6378137 * 2 * Math.PI;
|
|
7459
|
+
return Math.max(0, Math.floor(Math.log2(equatorCircumference / distance)) + 1);
|
|
7460
|
+
}
|
|
7461
|
+
|
|
7462
|
+
_updateContent() {
|
|
7463
|
+
if (!this._container) return;
|
|
7464
|
+
|
|
7465
|
+
const data = {
|
|
7466
|
+
...this._lastMouseData,
|
|
7467
|
+
...this._lastCameraData,
|
|
7468
|
+
fps: this._fps
|
|
7469
|
+
};
|
|
7470
|
+
|
|
7471
|
+
let html = this._template;
|
|
7472
|
+
Object.keys(data).forEach(key => {
|
|
7473
|
+
html = html.replace(new RegExp(`{${key}}`, 'g'), data[key]);
|
|
7474
|
+
});
|
|
7475
|
+
|
|
7476
|
+
// 如果开启 FPS 但模板里没有 {fps},则自动追加到末尾
|
|
7477
|
+
if (this._showFPS && !this._template.includes('{fps}')) {
|
|
7478
|
+
html += ` FPS:${this._fps}`;
|
|
7479
|
+
}
|
|
7480
|
+
|
|
7481
|
+
this._container.innerHTML = html;
|
|
7482
|
+
}
|
|
7483
|
+
}
|
|
7484
|
+
|
|
7485
|
+
/**
|
|
7486
|
+
* 缩放控件
|
|
7487
|
+
* 提供放大/缩小两个按钮,默认挂到 SDK 统一工具栏(xs3d-viewer-toolbar)。
|
|
7488
|
+
*/
|
|
7489
|
+
class Zoom extends BaseControl {
|
|
7490
|
+
/**
|
|
7491
|
+
* @param {object} options
|
|
7492
|
+
* @param {number} [options.duration=0.5] 缩放动画时长(秒)
|
|
7493
|
+
*/
|
|
7494
|
+
constructor(options = {}) {
|
|
7495
|
+
super({
|
|
7496
|
+
type: 'zoom',
|
|
7497
|
+
...options
|
|
7498
|
+
});
|
|
7499
|
+
|
|
7500
|
+
this._duration = options.duration ?? 0.5;
|
|
7501
|
+
}
|
|
7502
|
+
|
|
7503
|
+
_createContainer() {
|
|
7504
|
+
const div = super._createContainer();
|
|
7505
|
+
div.className += ' xs3d-zoom';
|
|
7506
|
+
div.style.display = 'contents';
|
|
7507
|
+
return div;
|
|
7508
|
+
}
|
|
7509
|
+
|
|
7510
|
+
_getParentContainer() {
|
|
7511
|
+
if (this._options.container) {
|
|
7512
|
+
return super._getParentContainer();
|
|
7513
|
+
}
|
|
7514
|
+
return this._getOrCreateToolbar();
|
|
7515
|
+
}
|
|
7516
|
+
|
|
7517
|
+
_mount() {
|
|
7518
|
+
this._clearAbsolutePositionIfInToolbar();
|
|
7519
|
+
|
|
7520
|
+
this._zoomInBtn = this._createButton('+', '放大');
|
|
7521
|
+
this._zoomOutBtn = this._createButton('-', '缩小');
|
|
7522
|
+
|
|
7523
|
+
this._container.appendChild(this._zoomInBtn);
|
|
7524
|
+
this._container.appendChild(this._zoomOutBtn);
|
|
7525
|
+
}
|
|
7526
|
+
|
|
7527
|
+
_createButton(label, title) {
|
|
7528
|
+
const btn = document.createElement('button');
|
|
7529
|
+
btn.type = 'button';
|
|
7530
|
+
btn.title = title;
|
|
7531
|
+
btn.className = 'cesium-button cesium-toolbar-button xs3d-zoom-button';
|
|
7532
|
+
btn.textContent = label;
|
|
7533
|
+
btn.style.width = '32px';
|
|
7534
|
+
btn.style.height = '32px';
|
|
7535
|
+
btn.style.lineHeight = '1';
|
|
7536
|
+
btn.style.fontSize = '18px';
|
|
7537
|
+
btn.style.fontWeight = 'bold';
|
|
7538
|
+
btn.style.display = 'flex';
|
|
7539
|
+
btn.style.alignItems = 'center';
|
|
7540
|
+
btn.style.justifyContent = 'center';
|
|
7541
|
+
btn.style.cursor = 'pointer';
|
|
7542
|
+
// 在统一工具栏内按 order 排列:指南针 -1,放大 1,缩小 2
|
|
7543
|
+
btn.style.order = label === '+' ? '1' : '2';
|
|
7544
|
+
return btn;
|
|
7545
|
+
}
|
|
7546
|
+
|
|
7547
|
+
_bindEvents() {
|
|
7548
|
+
this._addDomEvent(this._zoomInBtn, 'click', (e) => {
|
|
7549
|
+
e.stopPropagation();
|
|
7550
|
+
this._zoomIn();
|
|
7551
|
+
});
|
|
7552
|
+
|
|
7553
|
+
this._addDomEvent(this._zoomOutBtn, 'click', (e) => {
|
|
7554
|
+
e.stopPropagation();
|
|
7555
|
+
this._zoomOut();
|
|
7556
|
+
});
|
|
7557
|
+
}
|
|
7558
|
+
|
|
7559
|
+
_zoomIn() {
|
|
7560
|
+
this._zoomBy(1);
|
|
7561
|
+
}
|
|
7562
|
+
|
|
7563
|
+
_zoomOut() {
|
|
7564
|
+
this._zoomBy(-1);
|
|
7565
|
+
}
|
|
7566
|
+
|
|
7567
|
+
/**
|
|
7568
|
+
* 按地图层级步进缩放
|
|
7569
|
+
* @param {number} levelDelta 层级变化量,+1 放大一级,-1 缩小一级
|
|
7570
|
+
*/
|
|
7571
|
+
_zoomBy(levelDelta) {
|
|
7572
|
+
const camera = this._viewer.camera;
|
|
7573
|
+
const cartographic = camera.positionCartographic;
|
|
7574
|
+
const currentLevel = this._getZoomLevel();
|
|
7575
|
+
const targetLevel = Math.max(0, Math.min(24, currentLevel + levelDelta));
|
|
7576
|
+
const targetHeight = this._getHeightByZoomLevel(targetLevel);
|
|
7577
|
+
|
|
7578
|
+
camera.flyTo({
|
|
7579
|
+
destination: Cesium.Cartesian3.fromRadians(
|
|
7580
|
+
cartographic.longitude,
|
|
7581
|
+
cartographic.latitude,
|
|
7582
|
+
targetHeight
|
|
7583
|
+
),
|
|
7584
|
+
orientation: {
|
|
7585
|
+
heading: camera.heading,
|
|
7586
|
+
pitch: camera.pitch,
|
|
7587
|
+
roll: camera.roll
|
|
7588
|
+
},
|
|
7589
|
+
duration: this._duration
|
|
7590
|
+
});
|
|
7591
|
+
}
|
|
7592
|
+
|
|
7593
|
+
_getZoomLevel() {
|
|
7594
|
+
const positionCartographic = this._viewer.camera.positionCartographic;
|
|
7595
|
+
const surfaceCartesian = Cesium.Cartesian3.fromRadians(
|
|
7596
|
+
positionCartographic.longitude,
|
|
7597
|
+
positionCartographic.latitude,
|
|
7598
|
+
0
|
|
7599
|
+
);
|
|
7600
|
+
const distance = Cesium.Cartesian3.distance(this._viewer.camera.position, surfaceCartesian);
|
|
7601
|
+
if (!distance || distance <= 0 || !Number.isFinite(distance)) return 0;
|
|
7602
|
+
const equatorCircumference = 6378137 * 2 * Math.PI;
|
|
7603
|
+
return Math.max(0, Math.floor(Math.log2(equatorCircumference / distance)) + 1);
|
|
7604
|
+
}
|
|
7605
|
+
|
|
7606
|
+
_getHeightByZoomLevel(level) {
|
|
7607
|
+
const equatorCircumference = 6378137 * 2 * Math.PI;
|
|
7608
|
+
return equatorCircumference / Math.pow(2, Math.max(0, level - 1));
|
|
7609
|
+
}
|
|
7610
|
+
}
|
|
7611
|
+
|
|
7612
|
+
/**
|
|
7613
|
+
* 指南针控件
|
|
7614
|
+
* 参考 Mars3D:外环随相机 heading 旋转,拖拽外环可旋转视角,
|
|
7615
|
+
* 拖拽中心球可调整俯仰角,双击复位到正北。
|
|
7616
|
+
*/
|
|
7617
|
+
class Compass extends BaseControl {
|
|
7618
|
+
/**
|
|
7619
|
+
* @param {object} options
|
|
7620
|
+
* @param {number} [options.size=60] 指南针尺寸(px)
|
|
7621
|
+
* @param {boolean} [options.rotation=true] 是否启用中心拖拽调整俯仰角
|
|
7622
|
+
* @param {number} [options.top=5] 顶部偏移(未挂到工具栏时生效)
|
|
7623
|
+
* @param {number} [options.right=10] 右侧偏移(未挂到工具栏时生效)
|
|
7624
|
+
*/
|
|
7625
|
+
constructor(options = {}) {
|
|
7626
|
+
super({
|
|
7627
|
+
type: 'compass',
|
|
7628
|
+
top: 5,
|
|
7629
|
+
right: 5,
|
|
7630
|
+
...options
|
|
7631
|
+
});
|
|
7632
|
+
|
|
7633
|
+
this._size = options.size ?? 60;
|
|
7634
|
+
this._rotationEnabled = options.rotation !== false;
|
|
7635
|
+
|
|
7636
|
+
// 临时鼠标事件句柄(需要手动清理)
|
|
7637
|
+
this._moveHandle = null;
|
|
7638
|
+
this._upHandle = null;
|
|
7639
|
+
}
|
|
7640
|
+
|
|
7641
|
+
_createContainer() {
|
|
7642
|
+
const div = super._createContainer();
|
|
7643
|
+
div.className += ' xs3d-compass';
|
|
7644
|
+
div.style.width = `${this._size}px`;
|
|
7645
|
+
div.style.height = `${this._size}px`;
|
|
7646
|
+
div.style.borderRadius = '50%';
|
|
7647
|
+
div.style.background = '#3f4854';
|
|
7648
|
+
div.style.boxShadow = '0 1px 4px rgba(0,0,0,0.35)';
|
|
7649
|
+
div.style.cursor = 'pointer';
|
|
7650
|
+
div.style.display = 'flex';
|
|
7651
|
+
div.style.alignItems = 'center';
|
|
7652
|
+
div.style.justifyContent = 'center';
|
|
7653
|
+
div.style.overflow = 'hidden';
|
|
7654
|
+
div.style.userSelect = 'none';
|
|
7655
|
+
return div;
|
|
7656
|
+
}
|
|
7657
|
+
|
|
7658
|
+
_getParentContainer() {
|
|
7659
|
+
if (this._options.container) {
|
|
7660
|
+
return super._getParentContainer();
|
|
7661
|
+
}
|
|
7662
|
+
// 指南针单独定位在地图右上角,不放入工具栏
|
|
7663
|
+
return this._viewer?.container;
|
|
7664
|
+
}
|
|
7665
|
+
|
|
7666
|
+
_mount() {
|
|
7667
|
+
this._clearAbsolutePositionIfInToolbar();
|
|
7668
|
+
|
|
7669
|
+
// 外环:随 heading 旋转
|
|
7670
|
+
this._outerEl = document.createElement('div');
|
|
7671
|
+
this._outerEl.style.position = 'absolute';
|
|
7672
|
+
this._outerEl.style.top = '0';
|
|
7673
|
+
this._outerEl.style.left = '0';
|
|
7674
|
+
this._outerEl.style.width = '100%';
|
|
7675
|
+
this._outerEl.style.height = '100%';
|
|
7676
|
+
this._outerEl.innerHTML = this._getOuterSvg();
|
|
7677
|
+
this._outerEl.style.transition = 'transform 0.1s linear';
|
|
7678
|
+
|
|
7679
|
+
// 内球:固定中心
|
|
7680
|
+
this._innerEl = document.createElement('div');
|
|
7681
|
+
this._innerEl.style.position = 'absolute';
|
|
7682
|
+
this._innerEl.style.top = '50%';
|
|
7683
|
+
this._innerEl.style.left = '50%';
|
|
7684
|
+
this._innerEl.style.width = `${Math.round(this._size * 0.42)}px`;
|
|
7685
|
+
this._innerEl.style.height = `${Math.round(this._size * 0.42)}px`;
|
|
7686
|
+
this._innerEl.style.transform = 'translate(-50%, -50%)';
|
|
7687
|
+
this._innerEl.innerHTML = this._getInnerSvg();
|
|
7688
|
+
|
|
7689
|
+
// 旋转弧:拖拽中心时显示
|
|
7690
|
+
this._arcEl = document.createElement('div');
|
|
7691
|
+
this._arcEl.style.position = 'absolute';
|
|
7692
|
+
this._arcEl.style.top = '0';
|
|
7693
|
+
this._arcEl.style.left = '0';
|
|
7694
|
+
this._arcEl.style.width = '100%';
|
|
7695
|
+
this._arcEl.style.height = '100%';
|
|
7696
|
+
this._arcEl.style.visibility = 'hidden';
|
|
7697
|
+
this._arcEl.innerHTML = this._getArcSvg();
|
|
7698
|
+
|
|
7699
|
+
this._container.appendChild(this._outerEl);
|
|
7700
|
+
this._container.appendChild(this._innerEl);
|
|
7701
|
+
this._container.appendChild(this._arcEl);
|
|
7702
|
+
}
|
|
7703
|
+
|
|
7704
|
+
_getOuterSvg() {
|
|
7705
|
+
return `
|
|
7706
|
+
<svg viewBox="0 0 100 100" width="100%" height="100%">
|
|
7707
|
+
<defs>
|
|
7708
|
+
<linearGradient id="compass-ring" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
7709
|
+
<stop offset="0%" style="stop-color:#4a5563"/>
|
|
7710
|
+
<stop offset="100%" style="stop-color:#2d333b"/>
|
|
7711
|
+
</linearGradient>
|
|
7712
|
+
</defs>
|
|
7713
|
+
<circle cx="50" cy="50" r="48" fill="url(#compass-ring)" stroke="#5a6470" stroke-width="1"/>
|
|
7714
|
+
<g stroke="#ffffff" stroke-width="1" opacity="0.7">
|
|
7715
|
+
<line x1="50" y1="4" x2="50" y2="12"/>
|
|
7716
|
+
<line x1="50" y1="88" x2="50" y2="96"/>
|
|
7717
|
+
<line x1="4" y1="50" x2="12" y2="50"/>
|
|
7718
|
+
<line x1="88" y1="50" x2="96" y2="50"/>
|
|
7719
|
+
</g>
|
|
7720
|
+
<text x="50" y="24" text-anchor="middle" fill="#ff4d4f" font-size="12" font-weight="bold">N</text>
|
|
7721
|
+
<text x="50" y="86" text-anchor="middle" fill="#999" font-size="10">S</text>
|
|
7722
|
+
<text x="86" y="54" text-anchor="middle" fill="#999" font-size="10">E</text>
|
|
7723
|
+
<text x="14" y="54" text-anchor="middle" fill="#999" font-size="10">W</text>
|
|
7724
|
+
</svg>
|
|
7725
|
+
`;
|
|
7726
|
+
}
|
|
7727
|
+
|
|
7728
|
+
_getInnerSvg() {
|
|
7729
|
+
return `
|
|
7730
|
+
<svg viewBox="0 0 100 100" width="100%" height="100%">
|
|
7731
|
+
<circle cx="50" cy="50" r="48" fill="#ffffff"/>
|
|
7732
|
+
<circle cx="50" cy="50" r="38" fill="#68adfe"/>
|
|
7733
|
+
<circle cx="50" cy="50" r="14" fill="#ffffff"/>
|
|
7734
|
+
<path d="M50 30 L56 46 L50 44 L44 46 Z" fill="#3f4854"/>
|
|
7735
|
+
<circle cx="50" cy="50" r="4" fill="#3f4854"/>
|
|
7736
|
+
</svg>
|
|
7737
|
+
`;
|
|
7738
|
+
}
|
|
7739
|
+
|
|
7740
|
+
_getArcSvg() {
|
|
7741
|
+
return `
|
|
7742
|
+
<svg viewBox="0 0 100 100" width="100%" height="100%">
|
|
7743
|
+
<path d="M50 4 A46 46 0 0 1 96 50" fill="none" stroke="#68adfe" stroke-width="4" opacity="0.6"/>
|
|
7744
|
+
</svg>
|
|
7745
|
+
`;
|
|
7746
|
+
}
|
|
7747
|
+
|
|
7748
|
+
_bindEvents() {
|
|
7749
|
+
// 相机方向变化时旋转外环
|
|
7750
|
+
this._addCesiumHandler(this._viewer.camera.changed, this._updateRotation.bind(this));
|
|
7751
|
+
|
|
7752
|
+
// 鼠标交互
|
|
7753
|
+
this._addDomEvent(this._container, 'mousedown', this._onMouseDown.bind(this));
|
|
7754
|
+
this._addDomEvent(this._container, 'dblclick', this._onDoubleClick.bind(this));
|
|
7755
|
+
|
|
7756
|
+
this._updateRotation();
|
|
7757
|
+
}
|
|
7758
|
+
|
|
7759
|
+
_updateRotation() {
|
|
7760
|
+
if (!this._outerEl) return;
|
|
7761
|
+
const heading = Cesium.Math.toDegrees(this._viewer.camera.heading);
|
|
7762
|
+
this._outerEl.style.transform = `rotate(${-heading}deg)`;
|
|
7763
|
+
}
|
|
7764
|
+
|
|
7765
|
+
_onMouseDown(e) {
|
|
7766
|
+
e.preventDefault();
|
|
7767
|
+
e.stopPropagation();
|
|
7768
|
+
|
|
7769
|
+
const rect = this._container.getBoundingClientRect();
|
|
7770
|
+
const centerX = rect.left + rect.width / 2;
|
|
7771
|
+
const centerY = rect.top + rect.height / 2;
|
|
7772
|
+
const dx = e.clientX - centerX;
|
|
7773
|
+
const dy = e.clientY - centerY;
|
|
7774
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
7775
|
+
const radius = rect.width / 2;
|
|
7776
|
+
|
|
7777
|
+
if (this._rotationEnabled && distance < radius * 0.38) {
|
|
7778
|
+
this._startOrbit(e);
|
|
7779
|
+
} else if (distance < radius) {
|
|
7780
|
+
this._startRotate(e);
|
|
7781
|
+
}
|
|
7782
|
+
}
|
|
7783
|
+
|
|
7784
|
+
_getAngle(e) {
|
|
7785
|
+
const rect = this._container.getBoundingClientRect();
|
|
7786
|
+
const centerX = rect.left + rect.width / 2;
|
|
7787
|
+
const centerY = rect.top + rect.height / 2;
|
|
7788
|
+
return Math.atan2(e.clientY - centerY, e.clientX - centerX);
|
|
7789
|
+
}
|
|
7790
|
+
|
|
7791
|
+
_startRotate(e) {
|
|
7792
|
+
this._mode = 'rotate';
|
|
7793
|
+
this._lastAngle = this._getAngle(e);
|
|
7794
|
+
this._rotateFrame = this._getRotateFrame();
|
|
7795
|
+
|
|
7796
|
+
this._moveHandle = (ev) => this._onRotateMove(ev);
|
|
7797
|
+
this._upHandle = () => this._stopRotate();
|
|
7798
|
+
document.addEventListener('mousemove', this._moveHandle, false);
|
|
7799
|
+
document.addEventListener('mouseup', this._upHandle, false);
|
|
7800
|
+
}
|
|
7801
|
+
|
|
7802
|
+
_onRotateMove(e) {
|
|
7803
|
+
if (this._mode !== 'rotate') return;
|
|
7804
|
+
const angle = this._getAngle(e);
|
|
7805
|
+
let delta = angle - this._lastAngle;
|
|
7806
|
+
this._lastAngle = angle;
|
|
7807
|
+
|
|
7808
|
+
// 处理角度跳变(-PI 到 PI)
|
|
7809
|
+
if (delta > Math.PI) delta -= Math.PI * 2;
|
|
7810
|
+
if (delta < -Math.PI) delta += Math.PI * 2;
|
|
7811
|
+
|
|
7812
|
+
const camera = this._viewer.camera;
|
|
7813
|
+
if (this._rotateFrame) {
|
|
7814
|
+
const oldTransform = Cesium.Matrix4.clone(camera.transform);
|
|
7815
|
+
camera.lookAtTransform(this._rotateFrame);
|
|
7816
|
+
camera.rotateRight(delta);
|
|
7817
|
+
camera.lookAtTransform(oldTransform);
|
|
7818
|
+
} else {
|
|
7819
|
+
camera.setView({
|
|
7820
|
+
destination: camera.position,
|
|
7821
|
+
orientation: {
|
|
7822
|
+
heading: camera.heading - delta,
|
|
7823
|
+
pitch: camera.pitch,
|
|
7824
|
+
roll: camera.roll
|
|
7825
|
+
}
|
|
7826
|
+
});
|
|
7827
|
+
}
|
|
7828
|
+
}
|
|
7829
|
+
|
|
7830
|
+
_stopRotate() {
|
|
7831
|
+
this._mode = null;
|
|
7832
|
+
this._rotateFrame = null;
|
|
7833
|
+
document.removeEventListener('mousemove', this._moveHandle, false);
|
|
7834
|
+
document.removeEventListener('mouseup', this._upHandle, false);
|
|
7835
|
+
this._moveHandle = null;
|
|
7836
|
+
this._upHandle = null;
|
|
7837
|
+
}
|
|
7838
|
+
|
|
7839
|
+
_startOrbit(e) {
|
|
7840
|
+
this._mode = 'orbit';
|
|
7841
|
+
this._startY = e.clientY;
|
|
7842
|
+
this._startPitch = this._viewer.camera.pitch;
|
|
7843
|
+
this._startHeading = this._viewer.camera.heading;
|
|
7844
|
+
if (this._arcEl) this._arcEl.style.visibility = 'visible';
|
|
7845
|
+
|
|
7846
|
+
this._moveHandle = (ev) => this._onOrbitMove(ev);
|
|
7847
|
+
this._upHandle = () => this._stopOrbit();
|
|
7848
|
+
document.addEventListener('mousemove', this._moveHandle, false);
|
|
7849
|
+
document.addEventListener('mouseup', this._upHandle, false);
|
|
7850
|
+
}
|
|
7851
|
+
|
|
7852
|
+
_onOrbitMove(e) {
|
|
7853
|
+
if (this._mode !== 'orbit') return;
|
|
7854
|
+
const deltaY = this._startY - e.clientY;
|
|
7855
|
+
const sensitivity = 0.004;
|
|
7856
|
+
const newPitch = Cesium.Math.clamp(
|
|
7857
|
+
this._startPitch + deltaY * sensitivity,
|
|
7858
|
+
-Cesium.Math.PI_OVER_TWO + 0.01,
|
|
7859
|
+
Cesium.Math.PI_OVER_TWO - 0.01
|
|
7860
|
+
);
|
|
7861
|
+
this._viewer.camera.setView({
|
|
7862
|
+
destination: this._viewer.camera.position,
|
|
7863
|
+
orientation: {
|
|
7864
|
+
heading: this._startHeading,
|
|
7865
|
+
pitch: newPitch,
|
|
7866
|
+
roll: this._viewer.camera.roll
|
|
7867
|
+
}
|
|
7868
|
+
});
|
|
7869
|
+
}
|
|
7870
|
+
|
|
7871
|
+
_stopOrbit() {
|
|
7872
|
+
this._mode = null;
|
|
7873
|
+
if (this._arcEl) this._arcEl.style.visibility = 'hidden';
|
|
7874
|
+
document.removeEventListener('mousemove', this._moveHandle, false);
|
|
7875
|
+
document.removeEventListener('mouseup', this._upHandle, false);
|
|
7876
|
+
this._moveHandle = null;
|
|
7877
|
+
this._upHandle = null;
|
|
7878
|
+
}
|
|
7879
|
+
|
|
7880
|
+
_getRotateFrame() {
|
|
7881
|
+
const scene = this._viewer.scene;
|
|
7882
|
+
const camera = this._viewer.camera;
|
|
7883
|
+
if (scene.mode === Cesium.SceneMode.MORPHING) return null;
|
|
7884
|
+
|
|
7885
|
+
let center;
|
|
7886
|
+
if (this._viewer.trackedEntity) {
|
|
7887
|
+
center = this._viewer.trackedEntity.position.getValue(this._viewer.clock.currentTime);
|
|
7888
|
+
} else {
|
|
7889
|
+
const ray = new Cesium.Ray(camera.positionWC, camera.directionWC);
|
|
7890
|
+
center = scene.globe.pick(ray, scene);
|
|
7891
|
+
}
|
|
7892
|
+
if (!center) {
|
|
7893
|
+
center = camera.positionWC;
|
|
7894
|
+
}
|
|
7895
|
+
return Cesium.Transforms.eastNorthUpToFixedFrame(center, scene.globe.ellipsoid);
|
|
7896
|
+
}
|
|
7897
|
+
|
|
7898
|
+
_onDoubleClick() {
|
|
7899
|
+
const camera = this._viewer.camera;
|
|
7900
|
+
const cartographic = camera.positionCartographic;
|
|
7901
|
+
camera.flyTo({
|
|
7902
|
+
destination: Cesium.Cartesian3.fromRadians(
|
|
7903
|
+
cartographic.longitude,
|
|
7904
|
+
cartographic.latitude,
|
|
7905
|
+
cartographic.height
|
|
7906
|
+
),
|
|
7907
|
+
orientation: {
|
|
7908
|
+
heading: 0,
|
|
7909
|
+
pitch: camera.pitch,
|
|
7910
|
+
roll: camera.roll
|
|
7911
|
+
},
|
|
7912
|
+
duration: 0.5
|
|
7913
|
+
});
|
|
7914
|
+
}
|
|
7915
|
+
|
|
7916
|
+
remove(destroy = true) {
|
|
7917
|
+
this._stopRotate();
|
|
7918
|
+
this._stopOrbit();
|
|
7919
|
+
return super.remove(destroy);
|
|
7920
|
+
}
|
|
7921
|
+
}
|
|
7922
|
+
|
|
7923
|
+
/**
|
|
7924
|
+
* 比例尺控件
|
|
7925
|
+
* 参考 Mars3D 实现:在屏幕底部取水平相邻两像素点,计算其地表测地距离,
|
|
7926
|
+
* 从而得到“1 像素对应多少米”,再动态选择合适刻度显示。
|
|
7927
|
+
*/
|
|
7928
|
+
class DistanceLegend extends BaseControl {
|
|
7929
|
+
/**
|
|
7930
|
+
* @param {object} options
|
|
7931
|
+
* @param {number} [options.bottom=20] 底部偏移
|
|
7932
|
+
* @param {number} [options.left=20] 左侧偏移
|
|
7933
|
+
* @param {number} [options.maxWidth=100] 比例尺最大宽度(px)
|
|
7934
|
+
*/
|
|
7935
|
+
constructor(options = {}) {
|
|
7936
|
+
super({
|
|
7937
|
+
type: 'distanceLegend',
|
|
7938
|
+
bottom: 20,
|
|
7939
|
+
left: 20,
|
|
7940
|
+
...options
|
|
7941
|
+
});
|
|
7942
|
+
|
|
7943
|
+
this._maxWidth = options.maxWidth ?? 100;
|
|
7944
|
+
|
|
7945
|
+
// 刻度基数 1,2,3,5 及其 10^n 倍
|
|
7946
|
+
const base = [1, 2, 3, 5];
|
|
7947
|
+
this._distances = [];
|
|
7948
|
+
for (let i = 0; i < 7; i++) {
|
|
7949
|
+
const factor = Math.pow(10, i);
|
|
7950
|
+
base.forEach(b => this._distances.push(b * factor));
|
|
7951
|
+
}
|
|
7952
|
+
|
|
7953
|
+
this._geodesic = new Cesium.EllipsoidGeodesic();
|
|
7954
|
+
this._lastUpdate = 0;
|
|
7955
|
+
}
|
|
7956
|
+
|
|
7957
|
+
_createContainer() {
|
|
7958
|
+
const div = super._createContainer();
|
|
7959
|
+
div.className += ' xs3d-distance-legend';
|
|
7960
|
+
div.style.display = 'flex';
|
|
7961
|
+
div.style.flexDirection = 'column';
|
|
7962
|
+
div.style.alignItems = 'center';
|
|
7963
|
+
div.style.color = '#fff';
|
|
7964
|
+
div.style.fontSize = '11px';
|
|
7965
|
+
div.style.fontFamily = 'Microsoft YaHei, sans-serif';
|
|
7966
|
+
div.style.textShadow = '0 1px 2px rgba(0,0,0,0.5)';
|
|
7967
|
+
div.style.userSelect = 'none';
|
|
7968
|
+
div.style.pointerEvents = 'none';
|
|
7969
|
+
div.style.visibility = 'hidden';
|
|
7970
|
+
return div;
|
|
7971
|
+
}
|
|
7972
|
+
|
|
7973
|
+
_mount() {
|
|
7974
|
+
this._label = document.createElement('div');
|
|
7975
|
+
this._label.style.marginBottom = '2px';
|
|
7976
|
+
|
|
7977
|
+
this._line = document.createElement('div');
|
|
7978
|
+
this._line.style.height = '6px';
|
|
7979
|
+
this._line.style.borderLeft = '2px solid #fff';
|
|
7980
|
+
this._line.style.borderRight = '2px solid #fff';
|
|
7981
|
+
this._line.style.borderBottom = '2px solid #fff';
|
|
7982
|
+
this._line.style.boxSizing = 'border-box';
|
|
7983
|
+
|
|
7984
|
+
this._container.appendChild(this._label);
|
|
7985
|
+
this._container.appendChild(this._line);
|
|
7986
|
+
}
|
|
7987
|
+
|
|
7988
|
+
_bindEvents() {
|
|
7989
|
+
this._addCesiumHandler(this._viewer.scene.postRender, this._update.bind(this));
|
|
7990
|
+
}
|
|
7991
|
+
|
|
7992
|
+
_update(scene, time) {
|
|
7993
|
+
if (!this._viewer) return;
|
|
7994
|
+
|
|
7995
|
+
// 每 250ms 更新一次,避免频繁重绘
|
|
7996
|
+
const now = performance.now();
|
|
7997
|
+
if (now < this._lastUpdate + 250) return;
|
|
7998
|
+
this._lastUpdate = now;
|
|
7999
|
+
|
|
8000
|
+
const camera = this._viewer.camera;
|
|
8001
|
+
const canvas = scene.canvas;
|
|
8002
|
+
const width = canvas.width;
|
|
8003
|
+
const height = canvas.height;
|
|
8004
|
+
|
|
8005
|
+
if (!width || !height) return;
|
|
8006
|
+
|
|
8007
|
+
// 屏幕底部中心相邻两像素
|
|
8008
|
+
const x = Math.floor(width / 2);
|
|
8009
|
+
const y = Math.max(0, height - 1);
|
|
8010
|
+
|
|
8011
|
+
const leftRay = camera.getPickRay(new Cesium.Cartesian2(x, y));
|
|
8012
|
+
const rightRay = camera.getPickRay(new Cesium.Cartesian2(x + 1, y));
|
|
8013
|
+
if (!leftRay || !rightRay) return;
|
|
8014
|
+
|
|
8015
|
+
const leftPosition = scene.globe.pick(leftRay, scene);
|
|
8016
|
+
const rightPosition = scene.globe.pick(rightRay, scene);
|
|
8017
|
+
if (!leftPosition || !rightPosition) {
|
|
8018
|
+
this._container.style.visibility = 'hidden';
|
|
8019
|
+
return;
|
|
8020
|
+
}
|
|
8021
|
+
|
|
8022
|
+
this._geodesic.setEndPoints(
|
|
8023
|
+
scene.globe.ellipsoid.cartesianToCartographic(leftPosition),
|
|
8024
|
+
scene.globe.ellipsoid.cartesianToCartographic(rightPosition)
|
|
8025
|
+
);
|
|
8026
|
+
|
|
8027
|
+
const pixelDistance = this._geodesic.surfaceDistance;
|
|
8028
|
+
if (!pixelDistance || pixelDistance <= 0 || !Number.isFinite(pixelDistance)) {
|
|
8029
|
+
this._container.style.visibility = 'hidden';
|
|
8030
|
+
return;
|
|
8031
|
+
}
|
|
8032
|
+
|
|
8033
|
+
let distance = 0;
|
|
8034
|
+
for (let i = this._distances.length - 1; i >= 0; i--) {
|
|
8035
|
+
if (this._distances[i] / pixelDistance < this._maxWidth) {
|
|
8036
|
+
distance = this._distances[i];
|
|
8037
|
+
break;
|
|
8038
|
+
}
|
|
8039
|
+
}
|
|
8040
|
+
|
|
8041
|
+
if (distance) {
|
|
8042
|
+
const barWidth = Math.floor(distance / pixelDistance);
|
|
8043
|
+
this._label.textContent = distance >= 1000 ? `${distance / 1000} km` : `${distance} m`;
|
|
8044
|
+
this._line.style.width = `${barWidth}px`;
|
|
8045
|
+
this._container.style.visibility = 'visible';
|
|
8046
|
+
} else {
|
|
8047
|
+
this._container.style.visibility = 'hidden';
|
|
8048
|
+
}
|
|
8049
|
+
}
|
|
8050
|
+
}
|
|
8051
|
+
|
|
8052
|
+
/**
|
|
8053
|
+
* 控件管理模块
|
|
8054
|
+
* 统一管理地图控件的添加、移除和访问。
|
|
8055
|
+
*/
|
|
8056
|
+
class ControlModule {
|
|
8057
|
+
constructor(viewer) {
|
|
8058
|
+
this._viewer = viewer;
|
|
8059
|
+
this._controls = new Map();
|
|
8060
|
+
this._toolbar = null;
|
|
8061
|
+
}
|
|
8062
|
+
|
|
8063
|
+
/**
|
|
8064
|
+
* 添加控件
|
|
8065
|
+
* @param {string|BaseControl} type 控件类型或控件实例
|
|
8066
|
+
* @param {object} [options] 控件配置(当 type 为字符串时)
|
|
8067
|
+
* @returns {BaseControl} 控件实例
|
|
8068
|
+
*/
|
|
8069
|
+
addControl(type, options = {}) {
|
|
8070
|
+
// 内置工具栏控件(zoom)共享统一容器,compass 单独定位在工具栏上方
|
|
8071
|
+
if (typeof type === 'string' && !options.container && type === 'zoom') {
|
|
8072
|
+
options = { ...options, container: this._getOrCreateToolbar() };
|
|
8073
|
+
}
|
|
8074
|
+
|
|
8075
|
+
let control;
|
|
8076
|
+
|
|
8077
|
+
if (typeof type === 'string') {
|
|
8078
|
+
control = this._createControl(type, options);
|
|
8079
|
+
} else if (type && typeof type.addTo === 'function') {
|
|
8080
|
+
control = type;
|
|
8081
|
+
} else {
|
|
8082
|
+
console.warn(`ControlModule: 未知控件类型 ${type}`);
|
|
8083
|
+
return null;
|
|
8084
|
+
}
|
|
8085
|
+
|
|
8086
|
+
if (!control) return null;
|
|
8087
|
+
|
|
8088
|
+
// 同类型控件先移除
|
|
8089
|
+
if (this._controls.has(control.type)) {
|
|
8090
|
+
this.removeControl(control.type);
|
|
8091
|
+
}
|
|
8092
|
+
|
|
8093
|
+
control.addTo(this._viewer);
|
|
8094
|
+
this._controls.set(control.type, control);
|
|
8095
|
+
|
|
8096
|
+
return control;
|
|
8097
|
+
}
|
|
8098
|
+
|
|
8099
|
+
/**
|
|
8100
|
+
* 移除控件
|
|
8101
|
+
* @param {string} type 控件类型
|
|
8102
|
+
* @param {boolean} [destroy=true] 是否销毁
|
|
8103
|
+
* @returns {boolean} 是否移除成功
|
|
8104
|
+
*/
|
|
8105
|
+
removeControl(type, destroy = true) {
|
|
8106
|
+
const control = this._controls.get(type);
|
|
8107
|
+
if (!control) return false;
|
|
8108
|
+
|
|
8109
|
+
control.remove(destroy);
|
|
8110
|
+
this._controls.delete(type);
|
|
8111
|
+
return true;
|
|
8112
|
+
}
|
|
8113
|
+
|
|
8114
|
+
/**
|
|
8115
|
+
* 获取控件
|
|
8116
|
+
* @param {string} type 控件类型
|
|
8117
|
+
* @returns {BaseControl|undefined}
|
|
8118
|
+
*/
|
|
8119
|
+
getControl(type) {
|
|
8120
|
+
return this._controls.get(type);
|
|
8121
|
+
}
|
|
8122
|
+
|
|
8123
|
+
/**
|
|
8124
|
+
* 获取所有已添加的控件
|
|
8125
|
+
* @returns {BaseControl[]}
|
|
8126
|
+
*/
|
|
8127
|
+
getAllControls() {
|
|
8128
|
+
return Array.from(this._controls.values());
|
|
8129
|
+
}
|
|
8130
|
+
|
|
8131
|
+
/**
|
|
8132
|
+
* 显示控件
|
|
8133
|
+
* @param {string} type
|
|
8134
|
+
*/
|
|
8135
|
+
showControl(type) {
|
|
8136
|
+
const control = this._controls.get(type);
|
|
8137
|
+
if (control) control.show();
|
|
8138
|
+
return this;
|
|
8139
|
+
}
|
|
8140
|
+
|
|
8141
|
+
/**
|
|
8142
|
+
* 隐藏控件
|
|
8143
|
+
* @param {string} type
|
|
8144
|
+
*/
|
|
8145
|
+
hideControl(type) {
|
|
8146
|
+
const control = this._controls.get(type);
|
|
8147
|
+
if (control) control.hide();
|
|
8148
|
+
return this;
|
|
8149
|
+
}
|
|
8150
|
+
|
|
8151
|
+
/**
|
|
8152
|
+
* 批量添加控件
|
|
8153
|
+
* @param {object} controlsConfig { locationBar: true, zoom: { ... }, ... }
|
|
8154
|
+
*/
|
|
8155
|
+
addControls(controlsConfig = {}) {
|
|
8156
|
+
const keys = Object.keys(controlsConfig);
|
|
8157
|
+
// 让 compass 先于 zoom 添加,确保指南针在工具栏顶部
|
|
8158
|
+
keys.sort((a, b) => {
|
|
8159
|
+
if (a === 'compass') return -1;
|
|
8160
|
+
if (b === 'compass') return 1;
|
|
8161
|
+
return 0;
|
|
8162
|
+
});
|
|
8163
|
+
|
|
8164
|
+
keys.forEach(type => {
|
|
8165
|
+
const config = controlsConfig[type];
|
|
8166
|
+
if (config === true) {
|
|
8167
|
+
this.addControl(type);
|
|
8168
|
+
} else if (config && typeof config === 'object') {
|
|
8169
|
+
this.addControl(type, config);
|
|
8170
|
+
}
|
|
8171
|
+
});
|
|
8172
|
+
return this;
|
|
8173
|
+
}
|
|
8174
|
+
|
|
8175
|
+
/**
|
|
8176
|
+
* 移除所有控件
|
|
8177
|
+
* @param {boolean} [destroy=true]
|
|
8178
|
+
*/
|
|
8179
|
+
removeAllControls(destroy = true) {
|
|
8180
|
+
this._controls.forEach((control, type) => {
|
|
8181
|
+
control.remove(destroy);
|
|
8182
|
+
});
|
|
8183
|
+
this._controls.clear();
|
|
8184
|
+
return this;
|
|
8185
|
+
}
|
|
8186
|
+
|
|
8187
|
+
/**
|
|
8188
|
+
* 销毁模块
|
|
8189
|
+
*/
|
|
8190
|
+
destroy() {
|
|
8191
|
+
this.removeAllControls(true);
|
|
8192
|
+
if (this._toolbar && this._toolbar.parentNode) {
|
|
8193
|
+
this._toolbar.parentNode.removeChild(this._toolbar);
|
|
8194
|
+
}
|
|
8195
|
+
this._toolbar = null;
|
|
8196
|
+
this._viewer = null;
|
|
8197
|
+
}
|
|
8198
|
+
|
|
8199
|
+
/**
|
|
8200
|
+
* 获取或创建统一工具栏
|
|
8201
|
+
*/
|
|
8202
|
+
_getOrCreateToolbar() {
|
|
8203
|
+
if (this._toolbar) return this._toolbar;
|
|
8204
|
+
|
|
8205
|
+
const viewerContainer = this._viewer?.container;
|
|
8206
|
+
if (!viewerContainer) return null;
|
|
8207
|
+
|
|
8208
|
+
this._toolbar = document.createElement('div');
|
|
8209
|
+
this._toolbar.className = 'xs3d-viewer-toolbar';
|
|
8210
|
+
this._toolbar.style.position = 'absolute';
|
|
8211
|
+
this._toolbar.style.top = '70px';
|
|
8212
|
+
this._toolbar.style.right = '10px';
|
|
8213
|
+
this._toolbar.style.display = 'flex';
|
|
8214
|
+
this._toolbar.style.flexDirection = 'column';
|
|
8215
|
+
this._toolbar.style.alignItems = 'center';
|
|
8216
|
+
this._toolbar.style.gap = '4px';
|
|
8217
|
+
this._toolbar.style.padding = '4px';
|
|
8218
|
+
// this._toolbar.style.background = 'rgba(63, 72, 84, 0.95)';
|
|
8219
|
+
this._toolbar.style.borderRadius = '4px';
|
|
8220
|
+
this._toolbar.style.boxShadow = '0 1px 4px rgba(0,0,0,0.35)';
|
|
8221
|
+
this._toolbar.style.zIndex = '1001';
|
|
8222
|
+
viewerContainer.appendChild(this._toolbar);
|
|
8223
|
+
return this._toolbar;
|
|
8224
|
+
}
|
|
8225
|
+
|
|
8226
|
+
/**
|
|
8227
|
+
* 根据类型创建内置控件
|
|
8228
|
+
*/
|
|
8229
|
+
_createControl(type, options) {
|
|
8230
|
+
switch (type) {
|
|
8231
|
+
case 'locationBar':
|
|
8232
|
+
return new LocationBar(options);
|
|
8233
|
+
case 'zoom':
|
|
8234
|
+
return new Zoom(options);
|
|
8235
|
+
case 'compass':
|
|
8236
|
+
return new Compass(options);
|
|
8237
|
+
case 'distanceLegend':
|
|
8238
|
+
return new DistanceLegend(options);
|
|
8239
|
+
default:
|
|
8240
|
+
console.warn(`ControlModule: 未注册的内置控件类型 "${type}"`);
|
|
8241
|
+
return null;
|
|
8242
|
+
}
|
|
8243
|
+
}
|
|
8244
|
+
}
|
|
8245
|
+
|
|
7025
8246
|
/**
|
|
7026
8247
|
* 模块管理器(对应平台 GetModule 逻辑)
|
|
7027
8248
|
* 统一管理场景、图元、事件模块的实例
|
|
@@ -7070,6 +8291,16 @@ class ModuleManager {
|
|
|
7070
8291
|
}).init();
|
|
7071
8292
|
this.registerModule('EditorModule', editorModule);
|
|
7072
8293
|
|
|
8294
|
+
// 初始化控件模块
|
|
8295
|
+
const controlModule = new ControlModule(this.cesiumViewer);
|
|
8296
|
+
this.registerModule('ControlModule', controlModule);
|
|
8297
|
+
|
|
8298
|
+
// 如果配置中声明了 controls,自动挂载
|
|
8299
|
+
const controlsConfig = Config.getInstance().getConfig().controls;
|
|
8300
|
+
if (controlsConfig) {
|
|
8301
|
+
controlModule.addControls(controlsConfig);
|
|
8302
|
+
}
|
|
8303
|
+
|
|
7073
8304
|
return this.cesiumViewer;
|
|
7074
8305
|
}
|
|
7075
8306
|
|
|
@@ -7125,6 +8356,14 @@ class ModuleManager {
|
|
|
7125
8356
|
return this.getModule('EditorModule');
|
|
7126
8357
|
}
|
|
7127
8358
|
|
|
8359
|
+
/**
|
|
8360
|
+
* 控件模块快捷访问
|
|
8361
|
+
* @returns {ControlModule}
|
|
8362
|
+
*/
|
|
8363
|
+
get controlModule() {
|
|
8364
|
+
return this.getModule('ControlModule');
|
|
8365
|
+
}
|
|
8366
|
+
|
|
7128
8367
|
/**
|
|
7129
8368
|
* 注册模块(内部使用)
|
|
7130
8369
|
* @param {string} moduleName 模块名称
|
|
@@ -7158,6 +8397,15 @@ class ModuleManager {
|
|
|
7158
8397
|
console.warn('EditorModule 销毁失败:', e);
|
|
7159
8398
|
}
|
|
7160
8399
|
|
|
8400
|
+
try {
|
|
8401
|
+
const controlModule = this.modules.get('ControlModule');
|
|
8402
|
+
if (controlModule && typeof controlModule.destroy === 'function') {
|
|
8403
|
+
controlModule.destroy();
|
|
8404
|
+
}
|
|
8405
|
+
} catch (e) {
|
|
8406
|
+
console.warn('ControlModule 销毁失败:', e);
|
|
8407
|
+
}
|
|
8408
|
+
|
|
7161
8409
|
this.cesiumViewer.destroy();
|
|
7162
8410
|
this.cesiumViewer = null;
|
|
7163
8411
|
this.modules.clear();
|
|
@@ -7371,8 +8619,15 @@ const xs3d = {
|
|
|
7371
8619
|
GraphicModule,
|
|
7372
8620
|
EventModule,
|
|
7373
8621
|
EditorModule,
|
|
8622
|
+
ControlModule,
|
|
7374
8623
|
CoordinateUtil,
|
|
7375
8624
|
GeoCalcUtil,
|
|
8625
|
+
// 导出控件类
|
|
8626
|
+
BaseControl,
|
|
8627
|
+
LocationBar,
|
|
8628
|
+
Zoom,
|
|
8629
|
+
Compass,
|
|
8630
|
+
DistanceLegend,
|
|
7376
8631
|
// 导出工具枚举
|
|
7377
8632
|
DrawType,
|
|
7378
8633
|
EditMode,
|