bigmap-plugin 1.0.5 → 1.0.6
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/README.md +402 -12
- package/dist/bigmap-plugin.js +2445 -1690
- package/dist/bigmap-plugin.umd.cjs +14 -14
- package/dist/index.d.ts +382 -20
- package/dist/libs/bigemap/BigemapElevationContourLayer.js +613 -0
- package/package.json +8 -1
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
(function (global) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var DEFAULT_COLOR_STOPS = [
|
|
5
|
+
{ height: -414, color: '#00F5FF' },
|
|
6
|
+
{ height: 0, color: '#0066FF' },
|
|
7
|
+
{ height: 500, color: '#4D00FF' },
|
|
8
|
+
{ height: 1000, color: '#9D00FF' },
|
|
9
|
+
{ height: 1500, color: '#E600FF' },
|
|
10
|
+
{ height: 2000, color: '#39FF14' },
|
|
11
|
+
{ height: 2500, color: '#FFF500' },
|
|
12
|
+
{ height: 3000, color: '#FF8A00' },
|
|
13
|
+
{ height: 3500, color: '#FF2D2D' },
|
|
14
|
+
{ height: 4000, color: '#FF007A' },
|
|
15
|
+
{ height: 4500, color: '#E000FF' },
|
|
16
|
+
{ height: 5000, color: '#B6FF00' },
|
|
17
|
+
{ height: 5500, color: '#54FF00' },
|
|
18
|
+
{ height: 6000, color: '#00F078' },
|
|
19
|
+
{ height: 6500, color: '#00E0B8' },
|
|
20
|
+
{ height: 7000, color: '#00C8FF' },
|
|
21
|
+
{ height: 7500, color: '#2A7FFF' },
|
|
22
|
+
{ height: 8000, color: '#7EC8FF' },
|
|
23
|
+
{ height: 8500, color: '#D6F0FF' },
|
|
24
|
+
{ height: 8777, color: '#FFFFFF' }
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
var DEFAULT_OPTIONS = {
|
|
28
|
+
enabled: true,
|
|
29
|
+
contourSpacing: 150,
|
|
30
|
+
contourWidth: 2,
|
|
31
|
+
outlineWidth: 2,
|
|
32
|
+
labelEvery: 5,
|
|
33
|
+
maxLabels: 28,
|
|
34
|
+
labelMinDistance: 140,
|
|
35
|
+
labelOutlineWidth: 3,
|
|
36
|
+
sampleStep: 80,
|
|
37
|
+
scanStep: 120,
|
|
38
|
+
viewportMargin: 48,
|
|
39
|
+
legendColorStep: 500,
|
|
40
|
+
minHeight: -414,
|
|
41
|
+
maxHeight: 8777
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* BIGEMAP 三维影像彩色等高线管理类。
|
|
46
|
+
*
|
|
47
|
+
* 只管理等高线材质、高程色带以及当前视区高程标签,不查找或操作页面按钮。
|
|
48
|
+
* HTML 可以通过公开方法绑定自己的按钮、面板或其他业务交互。
|
|
49
|
+
*/
|
|
50
|
+
class BigemapElevationContourLayer {
|
|
51
|
+
/**
|
|
52
|
+
* @param {object} viewer 已创建的 bmgl.Viewer 实例
|
|
53
|
+
* @param {object} [options] 等高线和标签配置
|
|
54
|
+
* @param {number} [options.contourSpacing=150] 等高距,单位为米
|
|
55
|
+
* @param {number} [options.contourWidth=2] 彩色主线宽度,单位为屏幕像素
|
|
56
|
+
* @param {number} [options.outlineWidth=2] 黑色描边宽度,单位为屏幕像素
|
|
57
|
+
* @param {number} [options.labelEvery=5] 每隔多少条等高线显示一个标签
|
|
58
|
+
* @param {number} [options.maxLabels=28] 当前视区最多显示的标签数量
|
|
59
|
+
* @param {Array<{height:number,color:string}>} [options.colorStops] 高程颜色节点
|
|
60
|
+
*/
|
|
61
|
+
constructor(viewer, options) {
|
|
62
|
+
if (!viewer || !viewer.scene || !viewer.scene.globe) {
|
|
63
|
+
throw new Error('BigemapElevationContourLayer 需要有效的 bmgl.Viewer 实例。');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
options = options || {};
|
|
67
|
+
this.viewer = viewer;
|
|
68
|
+
this.bmgl = options.bmgl || global.bmgl;
|
|
69
|
+
if (!this.bmgl) {
|
|
70
|
+
throw new Error('未找到 BIGEMAP bmgl SDK。');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.options = Object.assign({}, DEFAULT_OPTIONS, options);
|
|
74
|
+
this.options.colorStops = (options.colorStops || DEFAULT_COLOR_STOPS).map(function (stop) {
|
|
75
|
+
return { height: stop.height, color: stop.color };
|
|
76
|
+
}).sort(function (first, second) {
|
|
77
|
+
return first.height - second.height;
|
|
78
|
+
});
|
|
79
|
+
// 自定义颜色节点时默认从首尾节点推导高程范围,减少重复配置。
|
|
80
|
+
if (this.options.colorStops.length > 0 &&
|
|
81
|
+
!Object.prototype.hasOwnProperty.call(options, 'minHeight')) {
|
|
82
|
+
this.options.minHeight = this.options.colorStops[0].height;
|
|
83
|
+
}
|
|
84
|
+
if (this.options.colorStops.length > 0 &&
|
|
85
|
+
!Object.prototype.hasOwnProperty.call(options, 'maxHeight')) {
|
|
86
|
+
this.options.maxHeight = this.options.colorStops[this.options.colorStops.length - 1].height;
|
|
87
|
+
}
|
|
88
|
+
this._validateOptions();
|
|
89
|
+
|
|
90
|
+
this.enabled = this.options.enabled !== false;
|
|
91
|
+
this.labelCollection = null;
|
|
92
|
+
this.labelCount = 0;
|
|
93
|
+
this.labelsLoading = false;
|
|
94
|
+
this.material = null;
|
|
95
|
+
this.destroyed = false;
|
|
96
|
+
this._applyMaterial();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 显示彩色等高线,并恢复已有高程标签的可见性。
|
|
101
|
+
*
|
|
102
|
+
* @returns {BigemapElevationContourLayer} 当前实例,便于链式调用
|
|
103
|
+
*/
|
|
104
|
+
show() {
|
|
105
|
+
return this.setVisible(true);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 隐藏彩色等高线和高程标签,但不删除已经加载的标签数据。
|
|
110
|
+
*
|
|
111
|
+
* @returns {BigemapElevationContourLayer} 当前实例,便于链式调用
|
|
112
|
+
*/
|
|
113
|
+
hide() {
|
|
114
|
+
return this.setVisible(false);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 切换等高线显示状态。
|
|
119
|
+
*
|
|
120
|
+
* @returns {BigemapElevationContourLayer} 当前实例,便于链式调用
|
|
121
|
+
*/
|
|
122
|
+
toggle() {
|
|
123
|
+
return this.setVisible(!this.enabled);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 设置等高线显示状态。
|
|
128
|
+
*
|
|
129
|
+
* @param {boolean} visible 是否显示等高线
|
|
130
|
+
* @returns {BigemapElevationContourLayer} 当前实例,便于链式调用
|
|
131
|
+
*/
|
|
132
|
+
setVisible(visible) {
|
|
133
|
+
this.enabled = Boolean(visible);
|
|
134
|
+
this._applyMaterial();
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 动态设置彩色主线和黑色描边宽度,并立即刷新地图。
|
|
140
|
+
*
|
|
141
|
+
* @param {number} contourWidth 彩色主线宽度,单位为屏幕像素
|
|
142
|
+
* @param {number} outlineWidth 黑色描边宽度,单位为屏幕像素
|
|
143
|
+
* @returns {BigemapElevationContourLayer} 当前实例,便于链式调用
|
|
144
|
+
*/
|
|
145
|
+
setLineWidths(contourWidth, outlineWidth) {
|
|
146
|
+
if (!Number.isFinite(contourWidth) || contourWidth <= 0 ||
|
|
147
|
+
!Number.isFinite(outlineWidth) || outlineWidth <= 0) {
|
|
148
|
+
throw new Error('等高线宽度必须是大于 0 的数字。');
|
|
149
|
+
}
|
|
150
|
+
this.options.contourWidth = contourWidth;
|
|
151
|
+
this.options.outlineWidth = outlineWidth;
|
|
152
|
+
this._applyMaterial();
|
|
153
|
+
return this;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 返回当前等高线与标签状态,供 HTML 自行更新按钮和提示信息。
|
|
158
|
+
* 返回的是普通数据对象,不包含任何 DOM 元素。
|
|
159
|
+
*
|
|
160
|
+
* @returns {{enabled:boolean,labelsLoading:boolean,labelCount:number,contourSpacing:number,labelSpacing:number,contourWidth:number,outlineWidth:number}}
|
|
161
|
+
*/
|
|
162
|
+
getState() {
|
|
163
|
+
return {
|
|
164
|
+
enabled: this.enabled,
|
|
165
|
+
labelsLoading: this.labelsLoading,
|
|
166
|
+
labelCount: this.labelCount,
|
|
167
|
+
contourSpacing: this.options.contourSpacing,
|
|
168
|
+
labelSpacing: this.options.contourSpacing * this.options.labelEvery,
|
|
169
|
+
contourWidth: this.options.contourWidth,
|
|
170
|
+
outlineWidth: this.options.outlineWidth
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 返回绘制 HTML 图例所需的数据副本,避免页面直接读取类的内部配置。
|
|
176
|
+
*
|
|
177
|
+
* @returns {{minHeight:number,maxHeight:number,legendColorStep:number,colorStops:Array<{height:number,color:string}>}}
|
|
178
|
+
*/
|
|
179
|
+
getLegendData() {
|
|
180
|
+
return {
|
|
181
|
+
minHeight: this.options.minHeight,
|
|
182
|
+
maxHeight: this.options.maxHeight,
|
|
183
|
+
legendColorStep: this.options.legendColorStep,
|
|
184
|
+
colorStops: this.options.colorStops.map(function (stop) {
|
|
185
|
+
return { height: stop.height, color: stop.color };
|
|
186
|
+
})
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 按需读取当前位置可见地形,并替换为当前视区的高程标签。
|
|
192
|
+
* 地图移动和缩放不会自动触发计算,避免持续占用前端主线程。
|
|
193
|
+
*
|
|
194
|
+
* @returns {Promise<{status:string,labelCount:number,error?:Error}>} 加载结果
|
|
195
|
+
*/
|
|
196
|
+
loadCurrentViewLabels() {
|
|
197
|
+
var self = this;
|
|
198
|
+
if (this.destroyed) {
|
|
199
|
+
return Promise.resolve({ status: 'destroyed', labelCount: 0 });
|
|
200
|
+
}
|
|
201
|
+
if (this.labelsLoading) {
|
|
202
|
+
return Promise.resolve({ status: 'busy', labelCount: this.labelCount });
|
|
203
|
+
}
|
|
204
|
+
if (!this.enabled) {
|
|
205
|
+
return Promise.resolve({ status: 'hidden', labelCount: this.labelCount });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this.labelsLoading = true;
|
|
209
|
+
|
|
210
|
+
return new Promise(function (resolve) {
|
|
211
|
+
// 延迟到下一任务,让 HTML 有机会先绘制按钮忙碌状态。
|
|
212
|
+
global.setTimeout(function () {
|
|
213
|
+
var result;
|
|
214
|
+
if (self.destroyed) {
|
|
215
|
+
result = { status: 'destroyed', labelCount: 0 };
|
|
216
|
+
} else if (!self.enabled) {
|
|
217
|
+
result = { status: 'hidden', labelCount: self.labelCount };
|
|
218
|
+
} else {
|
|
219
|
+
try {
|
|
220
|
+
var anchors = self._collectCurrentViewAnchors();
|
|
221
|
+
if (anchors === null) {
|
|
222
|
+
result = { status: 'terrain-loading', labelCount: self.labelCount };
|
|
223
|
+
} else if (anchors.length === 0) {
|
|
224
|
+
result = { status: 'no-results', labelCount: self.labelCount };
|
|
225
|
+
} else {
|
|
226
|
+
var collection = self._getLabelCollection();
|
|
227
|
+
if (!collection) {
|
|
228
|
+
result = { status: 'unsupported', labelCount: self.labelCount };
|
|
229
|
+
} else {
|
|
230
|
+
collection.removeAll();
|
|
231
|
+
anchors.forEach(function (anchor) {
|
|
232
|
+
self._addFixedLabel(collection, anchor.position, anchor.height);
|
|
233
|
+
});
|
|
234
|
+
self.labelCount = anchors.length;
|
|
235
|
+
collection.show = true;
|
|
236
|
+
result = { status: 'success', labelCount: self.labelCount };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
result = {
|
|
241
|
+
status: 'error',
|
|
242
|
+
labelCount: self.labelCount,
|
|
243
|
+
error: error
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
self.labelsLoading = false;
|
|
249
|
+
self._requestRender();
|
|
250
|
+
resolve(result);
|
|
251
|
+
}, 0);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 移除地图上的全部高程标签,不影响影像和等高线材质。
|
|
257
|
+
* LabelCollection 会保留供下次加载复用,减少反复创建集合的开销。
|
|
258
|
+
*
|
|
259
|
+
* @returns {{status:string,labelCount:number}} 清空结果
|
|
260
|
+
*/
|
|
261
|
+
clearLabels() {
|
|
262
|
+
if (this.destroyed) {
|
|
263
|
+
return { status: 'destroyed', labelCount: 0 };
|
|
264
|
+
}
|
|
265
|
+
if (this.labelsLoading) {
|
|
266
|
+
return { status: 'busy', labelCount: this.labelCount };
|
|
267
|
+
}
|
|
268
|
+
if (this.labelCount === 0) {
|
|
269
|
+
return { status: 'empty', labelCount: 0 };
|
|
270
|
+
}
|
|
271
|
+
if (this.labelCollection && typeof this.labelCollection.removeAll === 'function') {
|
|
272
|
+
this.labelCollection.removeAll();
|
|
273
|
+
}
|
|
274
|
+
this.labelCount = 0;
|
|
275
|
+
this._requestRender();
|
|
276
|
+
return { status: 'cleared', labelCount: 0 };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 移除本类创建的材质与标签集合。
|
|
281
|
+
* 页面销毁 Viewer 前可调用该方法释放地图资源引用。
|
|
282
|
+
*
|
|
283
|
+
* @returns {void}
|
|
284
|
+
*/
|
|
285
|
+
destroy() {
|
|
286
|
+
if (this.destroyed) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (this.viewer.scene.globe.material === this.material) {
|
|
290
|
+
this.viewer.scene.globe.material = undefined;
|
|
291
|
+
}
|
|
292
|
+
if (this.labelCollection && this.viewer.scene.primitives &&
|
|
293
|
+
typeof this.viewer.scene.primitives.remove === 'function') {
|
|
294
|
+
this.viewer.scene.primitives.remove(this.labelCollection);
|
|
295
|
+
}
|
|
296
|
+
this.labelCollection = null;
|
|
297
|
+
this.labelCount = 0;
|
|
298
|
+
this.destroyed = true;
|
|
299
|
+
this._requestRender();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** 校验数值范围和颜色节点,尽早暴露错误配置。 @private */
|
|
303
|
+
_validateOptions() {
|
|
304
|
+
var positiveOptions = [
|
|
305
|
+
'contourSpacing', 'contourWidth', 'outlineWidth', 'labelEvery',
|
|
306
|
+
'maxLabels', 'labelMinDistance', 'sampleStep', 'scanStep'
|
|
307
|
+
];
|
|
308
|
+
var self = this;
|
|
309
|
+
positiveOptions.forEach(function (key) {
|
|
310
|
+
if (!Number.isFinite(self.options[key]) || self.options[key] <= 0) {
|
|
311
|
+
throw new Error(key + ' 必须是大于 0 的数字。');
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
if (this.options.maxHeight <= this.options.minHeight) {
|
|
315
|
+
throw new Error('maxHeight 必须大于 minHeight。');
|
|
316
|
+
}
|
|
317
|
+
if (this.options.colorStops.length < 2) {
|
|
318
|
+
throw new Error('colorStops 至少需要两个高程颜色节点。');
|
|
319
|
+
}
|
|
320
|
+
this.options.colorStops.forEach(function (stop) {
|
|
321
|
+
if (!Number.isFinite(stop.height) || typeof stop.color !== 'string') {
|
|
322
|
+
throw new Error('colorStops 中的 height 必须是数字,color 必须是颜色字符串。');
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** 将实际高程换算为色带中的 0~1 位置。 @private */
|
|
328
|
+
_getHeightPosition(height) {
|
|
329
|
+
return (height - this.options.minHeight) /
|
|
330
|
+
(this.options.maxHeight - this.options.minHeight);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** 创建供 ElevationRamp 材质采样的一维高程色带。 @private */
|
|
334
|
+
_getColorRamp() {
|
|
335
|
+
var ramp = global.document.createElement('canvas');
|
|
336
|
+
ramp.width = 256;
|
|
337
|
+
ramp.height = 1;
|
|
338
|
+
var context = ramp.getContext('2d');
|
|
339
|
+
var gradient = context.createLinearGradient(0, 0, ramp.width, 0);
|
|
340
|
+
var self = this;
|
|
341
|
+
this.options.colorStops.forEach(function (stop) {
|
|
342
|
+
var position = Math.max(0, Math.min(1, self._getHeightPosition(stop.height)));
|
|
343
|
+
gradient.addColorStop(position, stop.color);
|
|
344
|
+
});
|
|
345
|
+
context.fillStyle = gradient;
|
|
346
|
+
context.fillRect(0, 0, ramp.width, ramp.height);
|
|
347
|
+
return ramp;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** 创建由黑色描边、彩色主线和高程色带组成的组合材质。 @private */
|
|
351
|
+
_createMaterial() {
|
|
352
|
+
return new this.bmgl.Material({
|
|
353
|
+
fabric: {
|
|
354
|
+
type: 'ImageryElevationColorContour',
|
|
355
|
+
materials: {
|
|
356
|
+
outlineContourMaterial: { type: 'ElevationContour' },
|
|
357
|
+
colorContourMaterial: { type: 'ElevationContour' },
|
|
358
|
+
elevationRampMaterial: { type: 'ElevationRamp' }
|
|
359
|
+
},
|
|
360
|
+
components: {
|
|
361
|
+
diffuse: 'colorContourMaterial.alpha * elevationRampMaterial.diffuse + (1.0 - colorContourMaterial.alpha) * outlineContourMaterial.diffuse',
|
|
362
|
+
alpha: 'max(outlineContourMaterial.alpha, colorContourMaterial.alpha)'
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
translucent: true
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** 根据当前显隐状态设置地球材质和标签可见性。 @private */
|
|
370
|
+
_applyMaterial() {
|
|
371
|
+
var globe = this.viewer.scene.globe;
|
|
372
|
+
if (!this.enabled) {
|
|
373
|
+
globe.material = undefined;
|
|
374
|
+
if (this.labelCollection) {
|
|
375
|
+
this.labelCollection.show = false;
|
|
376
|
+
}
|
|
377
|
+
this._requestRender();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
this.material = this._createMaterial();
|
|
382
|
+
var outlineUniforms = this.material.materials.outlineContourMaterial.uniforms;
|
|
383
|
+
var colorUniforms = this.material.materials.colorContourMaterial.uniforms;
|
|
384
|
+
var elevationUniforms = this.material.materials.elevationRampMaterial.uniforms;
|
|
385
|
+
|
|
386
|
+
outlineUniforms.width = this.options.outlineWidth;
|
|
387
|
+
outlineUniforms.spacing = this.options.contourSpacing;
|
|
388
|
+
outlineUniforms.color = new this.bmgl.Color(0, 0, 0, 0.95);
|
|
389
|
+
colorUniforms.width = this.options.contourWidth;
|
|
390
|
+
colorUniforms.spacing = this.options.contourSpacing;
|
|
391
|
+
colorUniforms.color = this.bmgl.Color.WHITE;
|
|
392
|
+
elevationUniforms.minimumHeight = this.options.minHeight;
|
|
393
|
+
elevationUniforms.maximumHeight = this.options.maxHeight;
|
|
394
|
+
elevationUniforms.image = this._getColorRamp();
|
|
395
|
+
globe.material = this.material;
|
|
396
|
+
|
|
397
|
+
if (this.labelCollection) {
|
|
398
|
+
this.labelCollection.show = true;
|
|
399
|
+
}
|
|
400
|
+
this._requestRender();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** 读取一个屏幕坐标对应的地形位置和高程。 @private */
|
|
404
|
+
_getTerrainScreenSample(x, y) {
|
|
405
|
+
var scene = this.viewer.scene;
|
|
406
|
+
var globe = scene.globe;
|
|
407
|
+
if (!this.bmgl.Cartesian2 || !this.bmgl.Cartographic ||
|
|
408
|
+
typeof this.viewer.camera.getPickRay !== 'function' ||
|
|
409
|
+
typeof globe.pick !== 'function') {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
var ray = this.viewer.camera.getPickRay(new this.bmgl.Cartesian2(x, y));
|
|
414
|
+
var position = ray && globe.pick(ray, scene);
|
|
415
|
+
if (!position) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
var cartographic = this.bmgl.Cartographic.fromCartesian(position);
|
|
419
|
+
if (!cartographic) {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
var terrainHeight = typeof globe.getHeight === 'function'
|
|
423
|
+
? globe.getHeight(cartographic)
|
|
424
|
+
: cartographic.height;
|
|
425
|
+
if (!Number.isFinite(terrainHeight)) {
|
|
426
|
+
terrainHeight = cartographic.height;
|
|
427
|
+
}
|
|
428
|
+
if (!Number.isFinite(terrainHeight)) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
return { x: x, y: y, height: terrainHeight, position: position };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** 检查候选标签与已放置标签的屏幕间距,避免文字重叠。 @private */
|
|
435
|
+
_canPlaceLabel(placedLabels, x, y) {
|
|
436
|
+
var minimumDistance = this.options.labelMinDistance;
|
|
437
|
+
return placedLabels.every(function (label) {
|
|
438
|
+
var dx = label.x - x;
|
|
439
|
+
var dy = label.y - y;
|
|
440
|
+
return Math.sqrt(dx * dx + dy * dy) >= minimumDistance;
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** 在两个采样点之间迭代逼近指定等高线的固定三维锚点。 @private */
|
|
445
|
+
_findContourAnchor(start, end, targetHeight) {
|
|
446
|
+
var first = start;
|
|
447
|
+
var second = end;
|
|
448
|
+
var bestSample = null;
|
|
449
|
+
for (var iteration = 0; iteration < 5; iteration++) {
|
|
450
|
+
var heightDifference = second.height - first.height;
|
|
451
|
+
if (heightDifference === 0) {
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
var ratio = (targetHeight - first.height) / heightDifference;
|
|
455
|
+
ratio = Math.max(0, Math.min(1, ratio));
|
|
456
|
+
var x = first.x + (second.x - first.x) * ratio;
|
|
457
|
+
var y = first.y + (second.y - first.y) * ratio;
|
|
458
|
+
var sample = this._getTerrainScreenSample(x, y);
|
|
459
|
+
if (!sample) {
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
bestSample = sample;
|
|
463
|
+
if (Math.abs(sample.height - targetHeight) <= 2) {
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
if ((first.height - targetHeight) * (sample.height - targetHeight) <= 0) {
|
|
467
|
+
second = sample;
|
|
468
|
+
} else {
|
|
469
|
+
first = sample;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return bestSample;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** 收集两个采样点之间跨越的可标注等高线。 @private */
|
|
476
|
+
_addLabelsBetweenSamples(start, end, labelSpacing, placedLabels, anchors) {
|
|
477
|
+
if (!start || !end || start.height === end.height ||
|
|
478
|
+
placedLabels.length >= this.options.maxLabels) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
var minimum = Math.min(start.height, end.height);
|
|
482
|
+
var maximum = Math.max(start.height, end.height);
|
|
483
|
+
var firstHeight = Math.ceil(minimum / labelSpacing) * labelSpacing;
|
|
484
|
+
|
|
485
|
+
for (var height = firstHeight;
|
|
486
|
+
height <= maximum && placedLabels.length < this.options.maxLabels;
|
|
487
|
+
height += labelSpacing) {
|
|
488
|
+
var ratio = (height - start.height) / (end.height - start.height);
|
|
489
|
+
if (ratio < 0 || ratio > 1) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
var x = start.x + (end.x - start.x) * ratio;
|
|
493
|
+
var y = start.y + (end.y - start.y) * ratio;
|
|
494
|
+
if (!this._canPlaceLabel(placedLabels, x, y)) {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
var anchor = this._findContourAnchor(start, end, height);
|
|
498
|
+
if (!anchor || !anchor.position) {
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
anchors.push({ position: anchor.position, height: height });
|
|
502
|
+
placedLabels.push({ x: x, y: y });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** 水平与垂直扫描当前视口,收集去重后的标签锚点。 @private */
|
|
507
|
+
_collectCurrentViewAnchors() {
|
|
508
|
+
if (this.viewer.scene.globe.tilesLoaded === false) {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
var canvas = this.viewer.scene.canvas;
|
|
512
|
+
if (!canvas) {
|
|
513
|
+
return [];
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
var width = canvas.clientWidth || canvas.width;
|
|
517
|
+
var height = canvas.clientHeight || canvas.height;
|
|
518
|
+
var labelSpacing = this.options.contourSpacing * this.options.labelEvery;
|
|
519
|
+
var placedLabels = [];
|
|
520
|
+
var anchors = [];
|
|
521
|
+
var margin = this.options.viewportMargin;
|
|
522
|
+
var x;
|
|
523
|
+
var y;
|
|
524
|
+
|
|
525
|
+
for (y = margin; y < height - margin && placedLabels.length < this.options.maxLabels;
|
|
526
|
+
y += this.options.scanStep) {
|
|
527
|
+
var previousHorizontal = null;
|
|
528
|
+
for (x = margin; x < width - margin; x += this.options.sampleStep) {
|
|
529
|
+
var horizontal = this._getTerrainScreenSample(x, y);
|
|
530
|
+
this._addLabelsBetweenSamples(
|
|
531
|
+
previousHorizontal, horizontal, labelSpacing, placedLabels, anchors
|
|
532
|
+
);
|
|
533
|
+
previousHorizontal = horizontal;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
for (x = margin; x < width - margin && placedLabels.length < this.options.maxLabels;
|
|
538
|
+
x += this.options.scanStep) {
|
|
539
|
+
var previousVertical = null;
|
|
540
|
+
for (y = margin; y < height - margin; y += this.options.sampleStep) {
|
|
541
|
+
var vertical = this._getTerrainScreenSample(x, y);
|
|
542
|
+
this._addLabelsBetweenSamples(
|
|
543
|
+
previousVertical, vertical, labelSpacing, placedLabels, anchors
|
|
544
|
+
);
|
|
545
|
+
previousVertical = vertical;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return anchors;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** 延迟创建并复用唯一的 LabelCollection。 @private */
|
|
552
|
+
_getLabelCollection() {
|
|
553
|
+
if (this.labelCollection) {
|
|
554
|
+
return this.labelCollection;
|
|
555
|
+
}
|
|
556
|
+
if (!this.bmgl.LabelCollection || !this.viewer.scene.primitives ||
|
|
557
|
+
typeof this.viewer.scene.primitives.add !== 'function') {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
this.labelCollection = this.viewer.scene.primitives.add(new this.bmgl.LabelCollection({
|
|
561
|
+
scene: this.viewer.scene
|
|
562
|
+
}));
|
|
563
|
+
return this.labelCollection;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** 在固定世界坐标创建带描边和背景的高程文字。 @private */
|
|
567
|
+
_addFixedLabel(collection, position, height) {
|
|
568
|
+
var labelOptions = {
|
|
569
|
+
position: position,
|
|
570
|
+
text: height.toLocaleString('zh-CN') + ' m',
|
|
571
|
+
font: '700 14px "Microsoft YaHei", sans-serif',
|
|
572
|
+
fillColor: this.bmgl.Color.WHITE,
|
|
573
|
+
outlineColor: this.bmgl.Color.BLACK,
|
|
574
|
+
outlineWidth: this.options.labelOutlineWidth,
|
|
575
|
+
showBackground: true,
|
|
576
|
+
backgroundColor: new this.bmgl.Color(0, 0, 0, 0.78),
|
|
577
|
+
backgroundPadding: new this.bmgl.Cartesian2(6, 3),
|
|
578
|
+
// 注释后恢复深度检测,标签会被前方山体正常遮挡。
|
|
579
|
+
disableDepthTestDistance: Number.POSITIVE_INFINITY
|
|
580
|
+
};
|
|
581
|
+
// 显式将标签贴到地形表面;当前 SDK 不支持 HeightReference 时,
|
|
582
|
+
// 仍使用 globe.pick() 得到的地形交点作为标签位置。
|
|
583
|
+
if (this.bmgl.HeightReference &&
|
|
584
|
+
this.bmgl.HeightReference.CLAMP_TO_GROUND !== undefined) {
|
|
585
|
+
labelOptions.heightReference = this.bmgl.HeightReference.CLAMP_TO_GROUND;
|
|
586
|
+
}
|
|
587
|
+
if (this.bmgl.LabelStyle) {
|
|
588
|
+
labelOptions.style = this.bmgl.LabelStyle.FILL_AND_OUTLINE;
|
|
589
|
+
}
|
|
590
|
+
if (this.bmgl.HorizontalOrigin) {
|
|
591
|
+
labelOptions.horizontalOrigin = this.bmgl.HorizontalOrigin.CENTER;
|
|
592
|
+
}
|
|
593
|
+
if (this.bmgl.VerticalOrigin) {
|
|
594
|
+
labelOptions.verticalOrigin = this.bmgl.VerticalOrigin.CENTER;
|
|
595
|
+
}
|
|
596
|
+
if (this.bmgl.DistanceDisplayCondition) {
|
|
597
|
+
labelOptions.distanceDisplayCondition =
|
|
598
|
+
new this.bmgl.DistanceDisplayCondition(0, 500000);
|
|
599
|
+
}
|
|
600
|
+
collection.add(labelOptions);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** 请求场景立即刷新,兼容 Viewer 的按需渲染配置。 @private */
|
|
604
|
+
_requestRender() {
|
|
605
|
+
if (this.viewer && this.viewer.scene &&
|
|
606
|
+
typeof this.viewer.scene.requestRender === 'function') {
|
|
607
|
+
this.viewer.scene.requestRender();
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
global.BigemapElevationContourLayer = BigemapElevationContourLayer;
|
|
613
|
+
})(window);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bigmap-plugin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "基于 BigMap/Cesium (bmgl) 的工程化地图插件。独立 NPM 包,内置全部外部 JS 库、样式与资源,安装即用,支持 2D/3D 地图、点位管理、图形绘制、图层与弹窗管理。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"map",
|
|
@@ -45,6 +45,9 @@
|
|
|
45
45
|
"demo:dev": "vite --config vite.demo.config.ts",
|
|
46
46
|
"demo:build": "vite build --config vite.demo.config.ts",
|
|
47
47
|
"demo:preview": "vite preview --config vite.demo.config.ts",
|
|
48
|
+
"test": "vitest",
|
|
49
|
+
"test:run": "vitest run",
|
|
50
|
+
"test:coverage": "vitest run --coverage",
|
|
48
51
|
"copy:libs": "node scripts/copy-libs.mjs",
|
|
49
52
|
"prepublishOnly": "npm run build"
|
|
50
53
|
},
|
|
@@ -55,11 +58,15 @@
|
|
|
55
58
|
"@types/node": "^22.0.0",
|
|
56
59
|
"@vitejs/plugin-vue": "^6.0.8",
|
|
57
60
|
"@vitejs/plugin-vue-jsx": "^5.1.6",
|
|
61
|
+
"@vitest/coverage-v8": "^3.2.7",
|
|
62
|
+
"@vue/test-utils": "^2.4.6",
|
|
58
63
|
"esbuild": "^0.28.2",
|
|
64
|
+
"jsdom": "^26.1.0",
|
|
59
65
|
"sass": "^1.102.0",
|
|
60
66
|
"typescript": "~5.3.3",
|
|
61
67
|
"vite": "^7.3.6",
|
|
62
68
|
"vite-plugin-dts": "^3.9.1",
|
|
69
|
+
"vitest": "^3.2.7",
|
|
63
70
|
"vue": "~3.5.41",
|
|
64
71
|
"vue-tsc": "^1.8.27"
|
|
65
72
|
}
|