cesium-xs-sdk 1.0.0

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.
@@ -0,0 +1,961 @@
1
+ import { BaseTool } from './BaseTool.js';
2
+ import { PlotGeometryUtil } from '../../utils/PlotGeometryUtil.js';
3
+ import { PlotGraphicType, isLinePlotType } from '../PlotGraphicType.js';
4
+
5
+ /**
6
+ * 绘制类型枚举
7
+ */
8
+ export const DrawType = {
9
+ POINT: 'point',
10
+ LINE: 'line',
11
+ POLYGON: 'polygon',
12
+ RECTANGLE: 'rectangle',
13
+ CIRCLE: 'circle',
14
+
15
+ // 标绘类型
16
+ TRIANGLE: 'triangle',
17
+ SECTOR: 'sector',
18
+ ELLIPSE: 'ellipse',
19
+ FINE_ARROW: 'fineArrow',
20
+ ASSAULT_DIRECTION: 'assaultDirection',
21
+ STRAIGHT_ARROW: 'straightArrow',
22
+ ATTACK_ARROW: 'attackArrow',
23
+ SWALLOWTAIL_ATTACK_ARROW: 'swallowtailAttackArrow',
24
+ SQUAD_COMBAT: 'squadCombat',
25
+ SWALLOWTAIL_SQUAD_COMBAT: 'swallowtailSquadCombat',
26
+ DOUBLE_ARROW: 'doubleArrow',
27
+ CURVED_ARROW: 'curvedArrow',
28
+ CURVE: 'curve'
29
+ };
30
+
31
+ /**
32
+ * 标绘绘制配置表
33
+ * 描述每种标绘类型的:渲染类型、最少点数、完成触发方式、对应 PlotGraphicType
34
+ * 注:当前仅配置已实现的标绘类型,后续随算法实现逐步放开
35
+ */
36
+ const PLOT_DRAW_CONFIG = {
37
+ [DrawType.TRIANGLE]: {
38
+ category: 'polygon',
39
+ minPoints: 3,
40
+ finishOnCount: 3,
41
+ plotType: PlotGraphicType.TRIANGLE
42
+ },
43
+ [DrawType.SECTOR]: {
44
+ category: 'polygon',
45
+ minPoints: 3,
46
+ finishOnCount: 3,
47
+ plotType: PlotGraphicType.SECTOR
48
+ },
49
+ [DrawType.ELLIPSE]: {
50
+ category: 'polygon',
51
+ minPoints: 3,
52
+ finishOnCount: 3,
53
+ plotType: PlotGraphicType.ELLIPSE
54
+ },
55
+ [DrawType.FINE_ARROW]: {
56
+ category: 'polygon',
57
+ minPoints: 2,
58
+ finishOnCount: 2,
59
+ plotType: PlotGraphicType.FINE_ARROW
60
+ },
61
+ [DrawType.ASSAULT_DIRECTION]: {
62
+ category: 'polygon',
63
+ minPoints: 2,
64
+ finishOnCount: 2,
65
+ plotType: PlotGraphicType.ASSAULT_DIRECTION
66
+ },
67
+ [DrawType.STRAIGHT_ARROW]: {
68
+ category: 'line',
69
+ minPoints: 2,
70
+ finishOnCount: 2,
71
+ plotType: PlotGraphicType.STRAIGHT_ARROW
72
+ },
73
+ [DrawType.ATTACK_ARROW]: {
74
+ category: 'polygon',
75
+ minPoints: 3,
76
+ finishOnDoubleClick: true,
77
+ plotType: PlotGraphicType.ATTACK_ARROW
78
+ },
79
+ [DrawType.SWALLOWTAIL_ATTACK_ARROW]: {
80
+ category: 'polygon',
81
+ minPoints: 3,
82
+ finishOnDoubleClick: true,
83
+ plotType: PlotGraphicType.SWALLOWTAIL_ATTACK_ARROW
84
+ },
85
+ [DrawType.SQUAD_COMBAT]: {
86
+ category: 'polygon',
87
+ minPoints: 2,
88
+ finishOnDoubleClick: true,
89
+ plotType: PlotGraphicType.SQUAD_COMBAT
90
+ },
91
+ [DrawType.SWALLOWTAIL_SQUAD_COMBAT]: {
92
+ category: 'polygon',
93
+ minPoints: 2,
94
+ finishOnDoubleClick: true,
95
+ plotType: PlotGraphicType.SWALLOWTAIL_SQUAD_COMBAT
96
+ },
97
+ [DrawType.DOUBLE_ARROW]: {
98
+ category: 'polygon',
99
+ minPoints: 4,
100
+ finishOnCount: 4,
101
+ plotType: PlotGraphicType.DOUBLE_ARROW
102
+ },
103
+ [DrawType.CURVED_ARROW]: {
104
+ category: 'line',
105
+ minPoints: 2,
106
+ finishOnDoubleClick: true,
107
+ plotType: PlotGraphicType.CURVED_ARROW
108
+ },
109
+ [DrawType.CURVE]: {
110
+ category: 'line',
111
+ minPoints: 2,
112
+ finishOnDoubleClick: true,
113
+ plotType: PlotGraphicType.CURVE
114
+ }
115
+ };
116
+
117
+ /**
118
+ * 绘制工具
119
+ * 支持点、线、面、矩形、圆的交互式绘制
120
+ */
121
+ export class DrawTool extends BaseTool {
122
+ constructor(editorModule, options = {}) {
123
+ super(editorModule, options);
124
+ this._drawType = options.drawType || DrawType.POINT;
125
+ this._tempEntities = []; // 临时图元(辅助点等)
126
+ this._tempPositions = []; // 用户点击的位置数组
127
+ this._isDrawing = false; // 是否正在绘制
128
+ this._startPos = null; // 起始位置(用于矩形、圆)
129
+ this._mouseMoveHandler = null;
130
+ this._clickCount = 0;
131
+ // 预览线/面实体(单独保存,避免每次重建)
132
+ this._previewLine = null;
133
+ this._previewPolygon = null;
134
+ this._previewRect = null;
135
+ this._previewCircle = null;
136
+ this._previewPlot = null; // 标绘图元预览
137
+ this._currentMousePos = null; // 当前鼠标位置
138
+ }
139
+
140
+ /**
141
+ * 设置绘制类型
142
+ * @param {string} type 绘制类型 DrawType
143
+ */
144
+ setDrawType(type) {
145
+ this._drawType = type;
146
+ return this;
147
+ }
148
+
149
+ /**
150
+ * 获取绘制类型
151
+ */
152
+ getDrawType() {
153
+ return this._drawType;
154
+ }
155
+
156
+ onActivate() {
157
+ // 注册鼠标事件
158
+ this.registerObserver('mouse:click', this._onClick.bind(this));
159
+ this.registerObserver('mouse:move', this._onMove.bind(this));
160
+ this.registerObserver('mouse:rightclick', this._onRightClick.bind(this));
161
+ this.registerObserver('keyboard:keydown', this._onKeyDown.bind(this));
162
+ }
163
+
164
+ onDeactivate() {
165
+ this.clearTempEntities();
166
+ this._isDrawing = false;
167
+ this._tempPositions = [];
168
+ this._clickCount = 0;
169
+ this._currentMousePos = null;
170
+ this._startPos = null;
171
+ this._previewPlot = null;
172
+ }
173
+
174
+ _onClick(lngLat, entity, movement) {
175
+ // lngLat 已经是经纬度数组 [lon, lat, height]
176
+ if (!lngLat) return;
177
+
178
+ // 优先处理标绘类型
179
+ const plotConfig = PLOT_DRAW_CONFIG[this._drawType];
180
+ if (plotConfig) {
181
+ this._handlePlotClick(lngLat, plotConfig);
182
+ return;
183
+ }
184
+
185
+ switch (this._drawType) {
186
+ case DrawType.POINT:
187
+ this._drawPoint(lngLat);
188
+ break;
189
+ case DrawType.LINE:
190
+ this._handleLineClick(lngLat);
191
+ break;
192
+ case DrawType.POLYGON:
193
+ this._handlePolygonClick(lngLat);
194
+ break;
195
+ case DrawType.RECTANGLE:
196
+ this._handleRectangleClick(lngLat);
197
+ break;
198
+ case DrawType.CIRCLE:
199
+ this._handleCircleClick(lngLat);
200
+ break;
201
+ }
202
+ }
203
+
204
+ _onMove(data) {
205
+ // mouse:move 事件的 movement 对象使用 endPosition
206
+ const screenPos = data.position || data.endPosition;
207
+ if (!screenPos) return;
208
+ const lonLat = this.screenToLonLat({ x: screenPos.x, y: screenPos.y });
209
+ if (!lonLat) return;
210
+
211
+ this._currentMousePos = lonLat;
212
+
213
+ if (this._drawType === DrawType.POINT) return;
214
+
215
+ // 标绘类型预览
216
+ const plotConfig = PLOT_DRAW_CONFIG[this._drawType];
217
+ if (plotConfig) {
218
+ if (this._isDrawing && this._tempPositions.length >= plotConfig.minPoints - 1) {
219
+ this._updatePlotPreview(plotConfig, lonLat);
220
+ }
221
+ return;
222
+ }
223
+
224
+ if (this._isDrawing) {
225
+ if (this._drawType === DrawType.LINE) {
226
+ this._updateTempLine(lonLat);
227
+ } else if (this._drawType === DrawType.POLYGON) {
228
+ this._updateTempPolygon(lonLat);
229
+ }
230
+ }
231
+
232
+ if (this._startPos) {
233
+ this._updateRectOrCirclePreview(lonLat);
234
+ }
235
+ }
236
+
237
+ _onKeyDown(data) {
238
+ // ESC 取消绘制
239
+ if (data.key === 'Escape') {
240
+ this.cancelDrawing();
241
+ }
242
+ // Enter 完成当前绘制(线、面、标绘)
243
+ if (data.key === 'Enter') {
244
+ const plotConfig = PLOT_DRAW_CONFIG[this._drawType];
245
+ if (plotConfig && this._isDrawing && this._tempPositions.length >= plotConfig.minPoints) {
246
+ this._finishPlotDrawing(plotConfig);
247
+ return;
248
+ }
249
+ this.finishDrawing();
250
+ }
251
+ // Backspace 删除上一个点(用于线、面、标绘)
252
+ if (data.key === 'Backspace' && this._isDrawing && this._tempPositions.length > 0) {
253
+ const plotConfig = PLOT_DRAW_CONFIG[this._drawType];
254
+ if (plotConfig) {
255
+ this._tempPositions.pop();
256
+ const lastTemp = this._tempEntities.pop();
257
+ if (lastTemp) {
258
+ this.removeTempEntity(lastTemp);
259
+ }
260
+ this._updatePlotPreview(plotConfig);
261
+ return;
262
+ }
263
+
264
+ this._tempPositions.pop();
265
+ // 删除对应的临时辅助点
266
+ const lastTemp = this._tempEntities.pop();
267
+ if (lastTemp) {
268
+ this.removeTempEntity(lastTemp);
269
+ }
270
+ this._updateTempPositions();
271
+ }
272
+ }
273
+
274
+ _onRightClick(_lngLat, _entity, _movement) {
275
+ // 右键结束绘制(用于线、面、标绘)
276
+ if (this._isDrawing) {
277
+ const plotConfig = PLOT_DRAW_CONFIG[this._drawType];
278
+ if (plotConfig && this._tempPositions.length >= plotConfig.minPoints) {
279
+ this._finishPlotDrawing(plotConfig);
280
+ return;
281
+ }
282
+ this.finishDrawing();
283
+ }
284
+ }
285
+
286
+ _drawPoint(lonLat) {
287
+ const id = this.generateId('point');
288
+ this._graphicModule.CreatePoint(id)
289
+ .setPosition(lonLat)
290
+ .setColor(Cesium.Color.RED)
291
+ .setPixelSize(10);
292
+
293
+ this._emitGraphicCreated('point', id);
294
+ }
295
+
296
+ _handleLineClick(lonLat) {
297
+ this._tempPositions.push(lonLat);
298
+ this._isDrawing = true;
299
+
300
+ // 创建辅助点
301
+ const tempPoint = this.createTempEntity(this.generateId('temp_point'), {
302
+ position: Cesium.Cartesian3.fromDegrees(...lonLat),
303
+ point: {
304
+ color: Cesium.Color.YELLOW,
305
+ pixelSize: 8
306
+ }
307
+ });
308
+ this._tempEntities.push(tempPoint);
309
+
310
+ // 如果已经有至少2个点,创建或更新临时线
311
+ if (this._tempPositions.length >= 2) {
312
+ this._updateTempLine();
313
+ }
314
+
315
+ this._emitDrawProgress('line', this._tempPositions.slice());
316
+ }
317
+
318
+ _handlePolygonClick(lonLat) {
319
+ this._tempPositions.push(lonLat);
320
+ this._isDrawing = true;
321
+
322
+ // 创建辅助点
323
+ const tempPoint = this.createTempEntity(this.generateId('temp_point'), {
324
+ position: Cesium.Cartesian3.fromDegrees(...lonLat),
325
+ point: {
326
+ color: Cesium.Color.YELLOW,
327
+ pixelSize: 8
328
+ }
329
+ });
330
+ this._tempEntities.push(tempPoint);
331
+
332
+ // 如果已经有至少3个点,显示临时面
333
+ if (this._tempPositions.length >= 3) {
334
+ this._updateTempPolygon();
335
+ }
336
+
337
+ this._emitDrawProgress('polygon', this._tempPositions.slice());
338
+ }
339
+
340
+ _handleRectangleClick(lonLat) {
341
+ if (!this._startPos) {
342
+ // 第一次点击:设置起始点
343
+ this._startPos = lonLat;
344
+ // 创建起始点标记
345
+ const startMarker = this.createTempEntity(this.generateId('temp_start'), {
346
+ position: Cesium.Cartesian3.fromDegrees(...lonLat),
347
+ point: {
348
+ color: Cesium.Color.YELLOW,
349
+ pixelSize: 10
350
+ }
351
+ });
352
+ this._tempEntities.push(startMarker);
353
+ } else {
354
+ // 第二次点击:完成矩形
355
+ this._finishRectangle(lonLat);
356
+ }
357
+ }
358
+
359
+ _handleCircleClick(lonLat) {
360
+ if (!this._startPos) {
361
+ this._startPos = lonLat;
362
+ const startMarker = this.createTempEntity(this.generateId('temp_start'), {
363
+ position: Cesium.Cartesian3.fromDegrees(...lonLat),
364
+ point: {
365
+ color: Cesium.Color.YELLOW,
366
+ pixelSize: 10
367
+ }
368
+ });
369
+ this._tempEntities.push(startMarker);
370
+ } else {
371
+ this._finishCircle(lonLat);
372
+ }
373
+ }
374
+
375
+ _updateTempLine(mousePos) {
376
+ // 构建预览线位置:已有位置 + 鼠标当前位置(跟随鼠标)
377
+ const previewPositions = [...this._tempPositions];
378
+ if (mousePos) {
379
+ previewPositions.push(mousePos);
380
+ }
381
+
382
+ // 计算 Cartesian3 数组
383
+ const flatCoords = [];
384
+ for (const c of previewPositions) {
385
+ flatCoords.push(c[0], c[1], c[2] || 0);
386
+ }
387
+ const cartesians = Cesium.Cartesian3.fromDegreesArrayHeights(flatCoords);
388
+
389
+ if (!this._previewLine) {
390
+ // 首次创建预览线
391
+ this._previewLine = this.createTempEntity(this.generateId('temp_line'), {
392
+ polyline: {
393
+ positions: cartesians,
394
+ material: Cesium.Color.YELLOW,
395
+ width: 2
396
+ }
397
+ });
398
+ this._tempEntities.push(this._previewLine);
399
+ } else {
400
+ // 直接更新位置属性,避免重建实体
401
+ // this._previewLine.polyline.positions = cartesians;
402
+ this._previewLine.polyline.positions = new Cesium.CallbackProperty(() => {
403
+ return cartesians;
404
+ }, false);
405
+ }
406
+ }
407
+
408
+ _updateTempPolygon(mousePos) {
409
+ if (this._tempPositions.length < 3) return;
410
+
411
+ // 构建预览多边形:已有位置 + 鼠标当前位置
412
+ const previewPositions = [...this._tempPositions];
413
+ if (mousePos) {
414
+ previewPositions.push(mousePos);
415
+ }
416
+ // previewPositions.push(this._tempPositions[0]); // 闭合
417
+
418
+ const flatCoords = [];
419
+ for (const c of previewPositions) {
420
+ flatCoords.push(c[0], c[1], c[2] || 0);
421
+ }
422
+ const cartesians = Cesium.Cartesian3.fromDegreesArrayHeights(flatCoords);
423
+
424
+ if (!this._previewPolygon) {
425
+ // 首次创建预览面
426
+ this._previewPolygon = this.createTempEntity(this.generateId('temp_polygon'), {
427
+ polygon: {
428
+ hierarchy: new Cesium.PolygonHierarchy(cartesians),
429
+ material: Cesium.Color.YELLOW.withAlpha(0.5),
430
+ outline: false,
431
+ outlineColor: Cesium.Color.YELLOW,
432
+ }
433
+ });
434
+ this._tempEntities.push(this._previewPolygon);
435
+ } else {
436
+ // 直接更新层级,避免重建实体,使用 CallbackProperty 防止闪烁
437
+ this._previewPolygon.polygon.hierarchy = new Cesium.CallbackProperty(() => {
438
+ return new Cesium.PolygonHierarchy(cartesians);
439
+ }, false);
440
+ }
441
+ }
442
+
443
+ _updateRectOrCirclePreview(currentLonLat) {
444
+ if (!this._startPos) return;
445
+
446
+ if (this._drawType === DrawType.RECTANGLE) {
447
+ this._updateRectPreview(currentLonLat);
448
+ } else if (this._drawType === DrawType.CIRCLE) {
449
+ this._updateCirclePreview(currentLonLat);
450
+ }
451
+ }
452
+
453
+ _updateRectPreview(currentLonLat) {
454
+ const corner1 = this._startPos;
455
+ const corner2 = currentLonLat;
456
+
457
+ // 计算矩形四个角
458
+ const positions = [
459
+ [corner1[0], corner1[1]],
460
+ [corner2[0], corner1[1]],
461
+ [corner2[0], corner2[1]],
462
+ [corner1[0], corner2[1]],
463
+ [corner1[0], corner1[1]]
464
+ ];
465
+
466
+ const flatCoords = [];
467
+ for (const c of positions) {
468
+ flatCoords.push(c[0], c[1], 0);
469
+ }
470
+ const cartesians = Cesium.Cartesian3.fromDegreesArrayHeights(flatCoords);
471
+
472
+ if (!this._previewRect) {
473
+ this._previewRect = this.createTempEntity(this.generateId('temp_rect'), {
474
+ polyline: {
475
+ positions: new Cesium.CallbackProperty(() => cartesians, false),
476
+ material: Cesium.Color.YELLOW.withAlpha(0.7),
477
+ width: 2
478
+ },
479
+ polygon: {
480
+ hierarchy: new Cesium.CallbackProperty(() => new Cesium.PolygonHierarchy(cartesians), false),
481
+ material: Cesium.Color.YELLOW.withAlpha(0.2),
482
+ outline: false
483
+ }
484
+ });
485
+ this._previewRect._isPreview = true;
486
+ this._tempEntities.push(this._previewRect);
487
+ } else {
488
+ // 直接更新位置属性
489
+ this._previewRect.polyline.positions = new Cesium.CallbackProperty(() => {
490
+ const c1 = this._startPos;
491
+ const c2 = this._currentMousePos;
492
+ if (!c1 || !c2) return cartesians;
493
+ const pos = [
494
+ [c1[0], c1[1]],
495
+ [c2[0], c1[1]],
496
+ [c2[0], c2[1]],
497
+ [c1[0], c2[1]],
498
+ [c1[0], c1[1]]
499
+ ];
500
+ const flat = [];
501
+ for (const p of pos) {
502
+ flat.push(p[0], p[1], 0);
503
+ }
504
+ return Cesium.Cartesian3.fromDegreesArrayHeights(flat);
505
+ }, false);
506
+ this._previewRect.polygon.hierarchy = new Cesium.CallbackProperty(() => {
507
+ const c1 = this._startPos;
508
+ const c2 = this._currentMousePos;
509
+ if (!c1 || !c2) return new Cesium.PolygonHierarchy(cartesians);
510
+ const pos = [
511
+ [c1[0], c1[1]],
512
+ [c2[0], c1[1]],
513
+ [c2[0], c2[1]],
514
+ [c1[0], c2[1]],
515
+ [c1[0], c1[1]]
516
+ ];
517
+ const flat = [];
518
+ for (const p of pos) {
519
+ flat.push(p[0], p[1], 0);
520
+ }
521
+ return new Cesium.PolygonHierarchy(Cesium.Cartesian3.fromDegreesArrayHeights(flat));
522
+ }, false);
523
+ }
524
+ }
525
+
526
+ _updateCirclePreview(currentLonLat) {
527
+ const center = this._startPos;
528
+ const radius = this._calcDistance(center, currentLonLat);
529
+ const segments = 64;
530
+
531
+ if (!this._previewCircle) {
532
+ this._previewCircle = this.createTempEntity(this.generateId('temp_circle'), {
533
+ polygon: {
534
+ hierarchy: new Cesium.CallbackProperty(() => {
535
+ const positions = this._generateCirclePositions(center, radius, segments);
536
+ const flat = positions.flatMap(c => [c[0], c[1], 0]);
537
+ return new Cesium.PolygonHierarchy(Cesium.Cartesian3.fromDegreesArrayHeights(flat));
538
+ }, false),
539
+ material: Cesium.Color.YELLOW.withAlpha(0.2),
540
+ outline: false // 地形贴合时不支持 outline,设为 false 避免警告
541
+ }
542
+ });
543
+ this._previewCircle._isPreview = true;
544
+ this._tempEntities.push(this._previewCircle);
545
+ } else {
546
+ // 直接更新
547
+ this._previewCircle.polygon.hierarchy = new Cesium.CallbackProperty(() => {
548
+ const r = this._calcDistance(this._startPos, this._currentMousePos);
549
+ const pos = this._generateCirclePositions(this._startPos, r, 64);
550
+ const flat = pos.flatMap(c => [c[0], c[1], 0]);
551
+ return new Cesium.PolygonHierarchy(Cesium.Cartesian3.fromDegreesArrayHeights(flat));
552
+ }, false);
553
+ }
554
+ }
555
+
556
+ _finishRectangle(corner2) {
557
+ const corner1 = this._startPos;
558
+ const positions = [
559
+ [corner1[0], corner1[1]],
560
+ [corner2[0], corner1[1]],
561
+ [corner2[0], corner2[1]],
562
+ [corner1[0], corner2[1]]
563
+ ];
564
+
565
+ const id = this.generateId('rectangle');
566
+ const entity = this._graphicModule.CreatePolygon(id)
567
+ .setPositions(positions)
568
+ .setColor(Cesium.Color.BLUE.withAlpha(0.5))
569
+ .setOutlineColor(Cesium.Color.BLUE)
570
+ .setOutlineWidth(2)
571
+ .setHeightReference(Cesium.HeightReference.NONE);
572
+
573
+ this.clearTempEntities();
574
+ this._startPos = null;
575
+ this._emitGraphicCreated('rectangle', id);
576
+ }
577
+
578
+ _finishCircle(edgeLonLat) {
579
+ const center = this._startPos;
580
+ const radius = this._calcDistance(center, edgeLonLat);
581
+ const positions = this._generateCirclePositions(center, radius, 64);
582
+
583
+ const id = this.generateId('circle');
584
+ console.log('创建圆:', { center, radius, edgeLonLat, positionsCount: positions.length });
585
+
586
+ // 创建圆并标记为圆形
587
+ const wrapper = this._graphicModule.CreatePolygon(id);
588
+ wrapper.setPositions(positions)
589
+ .setColor(Cesium.Color.BLUE.withAlpha(0.5))
590
+ .setOutlineColor(Cesium.Color.BLUE)
591
+ .setOutlineWidth(2);
592
+
593
+ // 给 entity 添加圆形标记(用于 EditTool 特殊处理)
594
+ const entity = wrapper._entity;
595
+ entity._isCircle = true;
596
+ entity._circleCenter = center; // [lon, lat]
597
+ entity._circleRadius = radius; // 米
598
+
599
+ this.clearTempEntities();
600
+ this._startPos = null;
601
+ this._currentMousePos = null;
602
+ this._emitGraphicCreated('circle', id);
603
+ }
604
+
605
+ _generateCirclePositions(center, radius, segments) {
606
+ const [lon, lat] = center;
607
+ const positions = [];
608
+ const radPerDeg = Math.PI / 180;
609
+
610
+ // 当前纬度的经度米数(随纬度变化)
611
+ const metersPerLonDeg = 111320 * Math.cos(lat * radPerDeg);
612
+ const metersPerLatDeg = 110540; // 纬度方向相对固定
613
+
614
+ for (let i = 0; i <= segments; i++) {
615
+ const angle = (i / segments) * Math.PI * 2;
616
+
617
+ // 东向和北向偏移(米)→ 转换为度
618
+ const dLon = (radius * Math.cos(angle) / metersPerLonDeg);
619
+ const dLat = (radius * Math.sin(angle) / metersPerLatDeg);
620
+
621
+ positions.push([lon + dLon, lat + dLat]);
622
+ }
623
+
624
+ return positions;
625
+ }
626
+
627
+ _calcDistance(coord1, coord2) {
628
+ const earthRadius = 6371000; // 米
629
+ const [lon1, lat1] = coord1.map(d => d * Math.PI / 180);
630
+ const [lon2, lat2] = coord2.map(d => d * Math.PI / 180);
631
+
632
+ const dLat = lat2 - lat1;
633
+ const dLon = lon2 - lon1;
634
+
635
+ const a = Math.sin(dLat / 2) ** 2 +
636
+ Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
637
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
638
+
639
+ return earthRadius * c;
640
+ }
641
+
642
+ /**
643
+ * 将椭圆控制点中的短轴端点投影到与长轴垂直的直线上
644
+ * @param {Array} points [center, majorEnd, minorEndOrMouse]
645
+ * @returns {Array} [center, majorEnd, projectedMinorEnd]
646
+ */
647
+ _normalizeEllipseControlPoints(points) {
648
+ if (!points || points.length < 3) return points;
649
+
650
+ const center = points[0];
651
+ const majorEnd = points[1];
652
+ const rawMinor = points[2];
653
+
654
+ const major = [majorEnd[0] - center[0], majorEnd[1] - center[1]];
655
+ const majorLen = Math.hypot(major[0], major[1]);
656
+ if (majorLen === 0) return points;
657
+
658
+ const ux = major[0] / majorLen;
659
+ const uy = major[1] / majorLen;
660
+ // 长轴逆时针旋转 90° 得到短轴方向
661
+ const perpUx = -uy;
662
+ const perpUy = ux;
663
+
664
+ const mouseVec = [rawMinor[0] - center[0], rawMinor[1] - center[1]];
665
+ const projectedLen = mouseVec[0] * perpUx + mouseVec[1] * perpUy;
666
+
667
+ return [
668
+ center,
669
+ majorEnd,
670
+ [center[0] + perpUx * projectedLen, center[1] + perpUy * projectedLen, center[2] || 0]
671
+ ];
672
+ }
673
+
674
+ // ==================== 标绘类型绘制方法 ====================
675
+
676
+ /**
677
+ * 处理标绘类型点击事件
678
+ * @param {Array} lngLat [lon, lat, height]
679
+ * @param {object} config 标绘配置
680
+ */
681
+ _handlePlotClick(lngLat, config) {
682
+ this._tempPositions.push(lngLat);
683
+ this._isDrawing = true;
684
+
685
+ // 创建辅助点
686
+ const tempPoint = this.createTempEntity(this.generateId('temp_point'), {
687
+ position: Cesium.Cartesian3.fromDegrees(...lngLat),
688
+ point: {
689
+ color: Cesium.Color.YELLOW,
690
+ pixelSize: 8
691
+ }
692
+ });
693
+ this._tempEntities.push(tempPoint);
694
+
695
+ // 更新预览
696
+ if (this._tempPositions.length >= config.minPoints) {
697
+ this._updatePlotPreview(config);
698
+ }
699
+
700
+ this._emitDrawProgress(config.plotType, this._tempPositions.slice());
701
+
702
+ // 判断是否完成绘制
703
+ if (config.finishOnCount && this._tempPositions.length >= config.finishOnCount) {
704
+ this._finishPlotDrawing(config);
705
+ return;
706
+ }
707
+
708
+ // 点数达到最小值后可以右键/Enter 结束(用于双击结束的类型)
709
+ if (config.finishOnDoubleClick && this._tempPositions.length >= config.minPoints) {
710
+ // 不自动完成,等待右键/Enter/双击
711
+ console.log(`标绘 ${config.plotType} 已达到最小点数,可右键或按 Enter 完成`);
712
+ }
713
+ }
714
+
715
+ /**
716
+ * 更新标绘图元预览
717
+ * @param {object} config 标绘配置
718
+ * @param {Array} mousePos 当前鼠标位置 [lon, lat, height]
719
+ */
720
+ _updatePlotPreview(config, mousePos) {
721
+ const isEllipse = config.plotType === PlotGraphicType.ELLIPSE;
722
+ const normalizeForEllipse = (pts) => isEllipse ? this._normalizeEllipseControlPoints(pts) : pts;
723
+
724
+ const previewPositions = [...this._tempPositions];
725
+ if (mousePos) {
726
+ previewPositions.push(mousePos);
727
+ }
728
+
729
+ if (previewPositions.length < config.minPoints) return;
730
+
731
+ const factoryName = `create${this._capitalizePlotType(config.plotType)}`;
732
+ if (!PlotGeometryUtil[factoryName]) {
733
+ console.warn(`未知标绘类型: ${config.plotType}`);
734
+ return;
735
+ }
736
+
737
+ const geometryPositions = normalizeForEllipse(previewPositions);
738
+ const positions = PlotGeometryUtil[factoryName](geometryPositions);
739
+ const isLine = config.category === 'line';
740
+ const flatCoords = [];
741
+ for (const c of positions) {
742
+ flatCoords.push(c[0], c[1], c[2] || 0);
743
+ }
744
+ const cartesians = Cesium.Cartesian3.fromDegreesArrayHeights(flatCoords);
745
+
746
+ if (!this._previewPlot) {
747
+ if (isLine) {
748
+ this._previewPlot = this.createTempEntity(this.generateId('temp_plot'), {
749
+ polyline: {
750
+ positions: new Cesium.CallbackProperty(() => cartesians, false),
751
+ material: Cesium.Color.YELLOW,
752
+ width: 2
753
+ }
754
+ });
755
+ } else {
756
+ this._previewPlot = this.createTempEntity(this.generateId('temp_plot'), {
757
+ polygon: {
758
+ hierarchy: new Cesium.CallbackProperty(() => new Cesium.PolygonHierarchy(cartesians), false),
759
+ material: Cesium.Color.YELLOW.withAlpha(0.3),
760
+ outline: false
761
+ },
762
+ polyline: {
763
+ positions: new Cesium.CallbackProperty(() => [...cartesians, cartesians[0]], false),
764
+ material: Cesium.Color.YELLOW,
765
+ width: 1
766
+ }
767
+ });
768
+ }
769
+ this._previewPlot._isPreview = true;
770
+ this._tempEntities.push(this._previewPlot);
771
+ } else {
772
+ const self = this;
773
+ if (isLine) {
774
+ this._previewPlot.polyline.positions = new Cesium.CallbackProperty(() => {
775
+ const dynamicPositions = [...self._tempPositions];
776
+ if (self._currentMousePos) {
777
+ dynamicPositions.push(self._currentMousePos);
778
+ }
779
+ if (dynamicPositions.length < config.minPoints) return [];
780
+ const pos = PlotGeometryUtil[factoryName](normalizeForEllipse(dynamicPositions));
781
+ const flat = [];
782
+ for (const c of pos) {
783
+ flat.push(c[0], c[1], c[2] || 0);
784
+ }
785
+ return Cesium.Cartesian3.fromDegreesArrayHeights(flat);
786
+ }, false);
787
+ } else {
788
+ this._previewPlot.polygon.hierarchy = new Cesium.CallbackProperty(() => {
789
+ const dynamicPositions = [...self._tempPositions];
790
+ if (self._currentMousePos) {
791
+ dynamicPositions.push(self._currentMousePos);
792
+ }
793
+ if (dynamicPositions.length < config.minPoints) return new Cesium.PolygonHierarchy([]);
794
+ const pos = PlotGeometryUtil[factoryName](normalizeForEllipse(dynamicPositions));
795
+ const flat = [];
796
+ for (const c of pos) {
797
+ flat.push(c[0], c[1], c[2] || 0);
798
+ }
799
+ return new Cesium.PolygonHierarchy(Cesium.Cartesian3.fromDegreesArrayHeights(flat));
800
+ }, false);
801
+ this._previewPlot.polyline.positions = new Cesium.CallbackProperty(() => {
802
+ const dynamicPositions = [...self._tempPositions];
803
+ if (self._currentMousePos) {
804
+ dynamicPositions.push(self._currentMousePos);
805
+ }
806
+ if (dynamicPositions.length < config.minPoints) return [];
807
+ const pos = PlotGeometryUtil[factoryName](normalizeForEllipse(dynamicPositions));
808
+ const flat = [];
809
+ for (const c of pos) {
810
+ flat.push(c[0], c[1], c[2] || 0);
811
+ }
812
+ const cart = Cesium.Cartesian3.fromDegreesArrayHeights(flat);
813
+ return [...cart, cart[0]];
814
+ }, false);
815
+ }
816
+ }
817
+ }
818
+
819
+ /**
820
+ * 完成标绘图元绘制
821
+ * @param {object} config 标绘配置
822
+ */
823
+ _finishPlotDrawing(config) {
824
+ if (this._tempPositions.length < config.minPoints) return;
825
+
826
+ const factoryName = `create${this._capitalizePlotType(config.plotType)}`;
827
+ if (!PlotGeometryUtil[factoryName]) {
828
+ console.warn(`未知标绘类型: ${config.plotType}`);
829
+ return;
830
+ }
831
+
832
+ const id = this.generateId(config.plotType);
833
+ const isLine = config.category === 'line';
834
+
835
+ // 椭圆:将第三点击点投影到与长轴垂直的直线上
836
+ const finalPositions = config.plotType === PlotGraphicType.ELLIPSE
837
+ ? this._normalizeEllipseControlPoints(this._tempPositions)
838
+ : this._tempPositions;
839
+
840
+ // 创建标绘图元
841
+ const wrapper = this._graphicModule.createPlotGraphic(
842
+ id,
843
+ config.plotType,
844
+ finalPositions
845
+ );
846
+
847
+ // 应用默认样式
848
+ if (isLine) {
849
+ wrapper.setColor(Cesium.Color.BLUE).setWidth(2);
850
+ } else {
851
+ wrapper
852
+ .setColor(Cesium.Color.BLUE.withAlpha(0.5))
853
+ .setOutlineColor(Cesium.Color.BLUE)
854
+ .setOutlineWidth(2);
855
+ }
856
+
857
+ this.clearTempEntities();
858
+ this._startPos = null;
859
+ this._currentMousePos = null;
860
+ this._isDrawing = false;
861
+ this._tempPositions = [];
862
+ this._previewPlot = null;
863
+
864
+ this._emitGraphicCreated(config.plotType, id);
865
+ }
866
+
867
+ /**
868
+ * 将标绘类型名转为工厂方法名
869
+ * @private
870
+ */
871
+ _capitalizePlotType(plotType) {
872
+ return plotType
873
+ .replace(/-([a-z])/g, (g) => g[1].toUpperCase())
874
+ .replace(/^./, s => s.toUpperCase());
875
+ }
876
+
877
+ _updateTempPositions() {
878
+ if (this._drawType === DrawType.LINE) {
879
+ this._updateTempLine();
880
+ } else if (this._drawType === DrawType.POLYGON) {
881
+ this._updateTempPolygon();
882
+ }
883
+ }
884
+
885
+ /**
886
+ * 取消当前绘制
887
+ */
888
+ cancelDrawing() {
889
+ this.clearTempEntities();
890
+ this._tempPositions = [];
891
+ this._isDrawing = false;
892
+ this._clickCount = 0;
893
+ this._startPos = null;
894
+ this._currentMousePos = null;
895
+ this._emitDrawCancel();
896
+ }
897
+
898
+ /**
899
+ * 完成当前绘制(回车键触发)
900
+ */
901
+ finishDrawing() {
902
+ if (this._drawType === DrawType.LINE && this._tempPositions.length >= 2) {
903
+ const id = this.generateId('line');
904
+ this._graphicModule.CreateLine(id)
905
+ .setPositions(this._tempPositions)
906
+ .setColor(Cesium.Color.BLUE)
907
+ .setWidth(2);
908
+
909
+ this._emitGraphicCreated('line', id);
910
+ } else if (this._drawType === DrawType.POLYGON && this._tempPositions.length >= 3) {
911
+ const positions = [...this._tempPositions, this._tempPositions[0]]; // 闭合
912
+ const id = this.generateId('polygon');
913
+ this._graphicModule.CreatePolygon(id)
914
+ .setPositions(positions)
915
+ .setColor(Cesium.Color.BLUE.withAlpha(0.5))
916
+ .setOutlineColor(Cesium.Color.BLUE)
917
+
918
+ this._emitGraphicCreated('polygon', id);
919
+ }
920
+
921
+ this.clearTempEntities();
922
+ this._tempPositions = [];
923
+ this._isDrawing = false;
924
+ this._clickCount = 0;
925
+ this._currentMousePos = null;
926
+ }
927
+
928
+ /**
929
+ * 清除所有临时图元
930
+ */
931
+ clearTempEntities() {
932
+ this._tempEntities.forEach(entity => {
933
+ this.removeTempEntity(entity);
934
+ });
935
+ this._tempEntities = [];
936
+ this._previewLine = null;
937
+ this._previewPolygon = null;
938
+ this._previewRect = null;
939
+ this._previewCircle = null;
940
+ this._previewPlot = null;
941
+ }
942
+
943
+ _emitGraphicCreated(type, id) {
944
+ // 触发图元创建事件
945
+ if (this._options.onGraphicCreated) {
946
+ this._options.onGraphicCreated({ type, id });
947
+ }
948
+ }
949
+
950
+ _emitDrawProgress(type, positions) {
951
+ if (this._options.onDrawProgress) {
952
+ this._options.onDrawProgress({ type, positions });
953
+ }
954
+ }
955
+
956
+ _emitDrawCancel() {
957
+ if (this._options.onDrawCancel) {
958
+ this._options.onDrawCancel();
959
+ }
960
+ }
961
+ }