@jorgmoritz/gis-manager 0.1.41 → 0.1.42

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.
@@ -373,6 +373,287 @@ var LayerManager = class {
373
373
  getTileset(id) {
374
374
  return this.tilesetIdMap.get(id);
375
375
  }
376
+ /**
377
+ * 从配置加载地图图层(正射影像、中文注记、地形)
378
+ * @param config 地图图层配置
379
+ * @param options 可选配置
380
+ * @returns 加载结果
381
+ */
382
+ async loadMapLayersFromConfig(config, options) {
383
+ const result = {
384
+ success: true,
385
+ errors: []
386
+ };
387
+ const {
388
+ tiandituToken = "f44fcec420c3eb1c56656e9a22dabb17",
389
+ subdomains = ["0", "1", "2", "3", "4", "5", "6", "7"],
390
+ maximumLevel = 18
391
+ } = options || {};
392
+ const C = this.CesiumNS;
393
+ const { imgUrl, ciaUrl, terrUrl } = config;
394
+ if (imgUrl) {
395
+ try {
396
+ const imageryProvider = this.createImageryProviderFromUrl(imgUrl, {
397
+ tiandituToken,
398
+ subdomains,
399
+ maximumLevel
400
+ });
401
+ if (imageryProvider) {
402
+ result.imageryLayer = this.viewer.imageryLayers.addImageryProvider(imageryProvider);
403
+ console.log("[LayerManager] \u6B63\u5C04\u5F71\u50CF\u52A0\u8F7D\u6210\u529F:", imgUrl);
404
+ }
405
+ } catch (error) {
406
+ const errMsg = `\u6B63\u5C04\u5F71\u50CF\u52A0\u8F7D\u5931\u8D25: ${error}`;
407
+ console.error("[LayerManager]", errMsg);
408
+ result.errors.push(errMsg);
409
+ }
410
+ }
411
+ if (ciaUrl) {
412
+ try {
413
+ const ciaProvider = this.createImageryProviderFromUrl(ciaUrl, {
414
+ tiandituToken,
415
+ subdomains,
416
+ maximumLevel
417
+ });
418
+ if (ciaProvider) {
419
+ result.annotationLayer = this.viewer.imageryLayers.addImageryProvider(ciaProvider);
420
+ console.log("[LayerManager] \u4E2D\u6587\u6CE8\u8BB0\u52A0\u8F7D\u6210\u529F:", ciaUrl);
421
+ }
422
+ } catch (error) {
423
+ const errMsg = `\u4E2D\u6587\u6CE8\u8BB0\u52A0\u8F7D\u5931\u8D25: ${error}`;
424
+ console.error("[LayerManager]", errMsg);
425
+ result.errors.push(errMsg);
426
+ }
427
+ }
428
+ if (terrUrl) {
429
+ try {
430
+ const terrainProvider = await C.CesiumTerrainProvider.fromUrl(terrUrl);
431
+ this.viewer.terrainProvider = terrainProvider;
432
+ result.terrainProvider = terrainProvider;
433
+ console.log("[LayerManager] \u5730\u5F62\u6570\u636E\u52A0\u8F7D\u6210\u529F:", terrUrl);
434
+ } catch (error) {
435
+ const errMsg = `\u5730\u5F62\u6570\u636E\u52A0\u8F7D\u5931\u8D25: ${error}`;
436
+ console.warn("[LayerManager]", errMsg);
437
+ result.errors.push(errMsg);
438
+ }
439
+ }
440
+ result.success = result.errors.length === 0;
441
+ return result;
442
+ }
443
+ /**
444
+ * 根据 URL 格式自动创建合适的 ImageryProvider
445
+ * @param url 影像 URL
446
+ * @param options 配置选项
447
+ */
448
+ createImageryProviderFromUrl(url, options) {
449
+ const C = this.CesiumNS;
450
+ const { tiandituToken, subdomains, maximumLevel } = options;
451
+ if (url.includes("{z}") || url.includes("{TileMatrix}")) {
452
+ return new C.UrlTemplateImageryProvider({
453
+ url,
454
+ subdomains,
455
+ tilingScheme: new C.WebMercatorTilingScheme(),
456
+ maximumLevel
457
+ });
458
+ }
459
+ if (url.includes("wmts")) {
460
+ const tkMatch = url.match(/tk=([^&]+)/);
461
+ const tk = tkMatch ? tkMatch[1] : tiandituToken;
462
+ return new C.WebMapTileServiceImageryProvider({
463
+ url: `https://t{s}.tianditu.gov.cn/img_w/wmts?tk=${tk}`,
464
+ subdomains,
465
+ layer: "img",
466
+ style: "default",
467
+ tileMatrixSetID: "w",
468
+ format: "image/jpeg",
469
+ tilingScheme: new C.WebMercatorTilingScheme(),
470
+ maximumLevel
471
+ });
472
+ }
473
+ return new C.UrlTemplateImageryProvider({
474
+ url,
475
+ subdomains,
476
+ tilingScheme: new C.WebMercatorTilingScheme(),
477
+ maximumLevel
478
+ });
479
+ }
480
+ /**
481
+ * 从 SliceJson 配置加载 TIF 切片数据(DOM 正射影像或 Terrain 地形)
482
+ * @param sliceJsonData JSON 字符串或已解析的对象
483
+ * @param options 加载选项
484
+ * @returns 加载结果
485
+ */
486
+ async loadFromSliceJson(sliceJsonData, options) {
487
+ const { layerId, flyTo = true, flyDuration = 2, opacity = 1 } = options || {};
488
+ let data;
489
+ try {
490
+ data = typeof sliceJsonData === "string" ? JSON.parse(sliceJsonData) : sliceJsonData;
491
+ } catch (error) {
492
+ return {
493
+ success: false,
494
+ type: "dom",
495
+ data: {},
496
+ error: `JSON \u89E3\u6790\u5931\u8D25: ${error}`
497
+ };
498
+ }
499
+ if (!data || !data.type || !data.urlroot) {
500
+ return {
501
+ success: false,
502
+ type: data?.type || "dom",
503
+ data: data || {},
504
+ error: "\u65E0\u6548\u7684 SliceJson \u6570\u636E\uFF1A\u7F3A\u5C11 type \u6216 urlroot"
505
+ };
506
+ }
507
+ if (data.type === "dom") {
508
+ return this.loadDomFromSliceJson(data, { layerId, flyTo, flyDuration, opacity });
509
+ } else if (data.type === "terrain") {
510
+ return this.loadTerrainFromSliceJson(data, { flyTo, flyDuration });
511
+ } else {
512
+ return {
513
+ success: false,
514
+ type: data.type,
515
+ data,
516
+ error: `\u4E0D\u652F\u6301\u7684\u6570\u636E\u7C7B\u578B: ${data.type}`
517
+ };
518
+ }
519
+ }
520
+ /**
521
+ * 加载 DOM 正射影像
522
+ */
523
+ async loadDomFromSliceJson(data, options) {
524
+ const { layerId, flyTo, flyDuration, opacity } = options;
525
+ const C = this.CesiumNS;
526
+ try {
527
+ const url = `${data.urlroot}/{z}/{x}/{reverseY}.png`;
528
+ let bounds = data.bounds;
529
+ if (bounds && bounds.length >= 4) {
530
+ if (Math.abs(bounds[1]) > 90 && Math.abs(bounds[2]) <= 90) {
531
+ bounds = [bounds[0], bounds[2], bounds[1], bounds[3]];
532
+ }
533
+ }
534
+ const rectangle = bounds ? C.Rectangle.fromDegrees(bounds[0], bounds[1], bounds[2], bounds[3]) : void 0;
535
+ const provider = new C.UrlTemplateImageryProvider({
536
+ url,
537
+ minimumLevel: data.minZoom,
538
+ maximumLevel: data.maxZoom,
539
+ rectangle
540
+ });
541
+ const layer = this.viewer.imageryLayers.addImageryProvider(provider);
542
+ layer.alpha = opacity;
543
+ const id = layerId || `slice_dom_${++layerIdSeq}`;
544
+ layer._customId = id;
545
+ this.imageryIdMap.set(id, layer);
546
+ const handle = { id, type: "imagery", dispose: () => this.remove(id) };
547
+ this.events.emit({ added: handle });
548
+ console.log("[LayerManager] DOM \u6B63\u5C04\u5F71\u50CF\u52A0\u8F7D\u6210\u529F:", data.urlroot);
549
+ if (flyTo) {
550
+ await this.flyToSliceJsonTarget(data, flyDuration);
551
+ }
552
+ return {
553
+ success: true,
554
+ type: "dom",
555
+ handle,
556
+ imageryLayer: layer,
557
+ data
558
+ };
559
+ } catch (error) {
560
+ const errMsg = `DOM \u52A0\u8F7D\u5931\u8D25: ${error}`;
561
+ console.error("[LayerManager]", errMsg);
562
+ return {
563
+ success: false,
564
+ type: "dom",
565
+ data,
566
+ error: errMsg
567
+ };
568
+ }
569
+ }
570
+ /**
571
+ * 加载 Terrain 地形数据
572
+ */
573
+ async loadTerrainFromSliceJson(data, options) {
574
+ const { flyTo, flyDuration } = options;
575
+ const C = this.CesiumNS;
576
+ try {
577
+ const timestamp = Date.now();
578
+ const terrainUrl = `${data.urlroot}?t=${timestamp}`;
579
+ const terrainProvider = await C.CesiumTerrainProvider.fromUrl(terrainUrl, {
580
+ requestVertexNormals: true,
581
+ requestWaterMask: true
582
+ });
583
+ this.viewer.terrainProvider = terrainProvider;
584
+ this.terrainId = `slice_ter_${++layerIdSeq}`;
585
+ console.log("[LayerManager] Terrain \u5730\u5F62\u52A0\u8F7D\u6210\u529F:", data.urlroot);
586
+ if (flyTo) {
587
+ await new Promise((resolve) => setTimeout(resolve, 500));
588
+ await this.flyToSliceJsonTarget(data, flyDuration, true);
589
+ }
590
+ return {
591
+ success: true,
592
+ type: "terrain",
593
+ terrainProvider,
594
+ data
595
+ };
596
+ } catch (error) {
597
+ const errMsg = `Terrain \u52A0\u8F7D\u5931\u8D25: ${error}`;
598
+ console.error("[LayerManager]", errMsg);
599
+ return {
600
+ success: false,
601
+ type: "terrain",
602
+ data,
603
+ error: errMsg
604
+ };
605
+ }
606
+ }
607
+ /**
608
+ * 飞行到 SliceJson 数据的目标位置
609
+ */
610
+ async flyToSliceJsonTarget(data, duration, isVertical = false) {
611
+ const C = this.CesiumNS;
612
+ let centerLon;
613
+ let centerLat;
614
+ let cameraHeight = 2e3;
615
+ if (data.center && data.center.length >= 2 && (data.center[0] !== 0 || data.center[1] !== 0)) {
616
+ centerLon = data.center[0];
617
+ centerLat = data.center[1];
618
+ }
619
+ if (data.bounds && data.bounds.length >= 4) {
620
+ const bounds = data.bounds;
621
+ if (centerLon === void 0 || centerLat === void 0) {
622
+ centerLon = (bounds[0] + bounds[2]) / 2;
623
+ centerLat = (bounds[1] + bounds[3]) / 2;
624
+ }
625
+ const lonSpan = Math.abs(bounds[2] - bounds[0]);
626
+ const latSpan = Math.abs(bounds[3] - bounds[1]);
627
+ const maxSpan = Math.max(lonSpan, latSpan);
628
+ const heightMultiplier = isVertical ? 2 : 0.8;
629
+ cameraHeight = Math.max(maxSpan * 111e3 * heightMultiplier, 500);
630
+ }
631
+ if (centerLon === void 0 || centerLat === void 0) {
632
+ console.warn("[LayerManager] \u65E0\u6CD5\u83B7\u53D6\u6709\u6548\u7684\u4E2D\u5FC3\u70B9");
633
+ return;
634
+ }
635
+ let terrainHeight = 0;
636
+ try {
637
+ const terrainProvider = this.viewer.terrainProvider;
638
+ if (terrainProvider && !(terrainProvider instanceof C.EllipsoidTerrainProvider)) {
639
+ const positions = [C.Cartographic.fromDegrees(centerLon, centerLat)];
640
+ const sampledPositions = await C.sampleTerrainMostDetailed(terrainProvider, positions);
641
+ terrainHeight = sampledPositions[0]?.height || 0;
642
+ }
643
+ } catch (error) {
644
+ console.warn("[LayerManager] \u5730\u5F62\u91C7\u6837\u5931\u8D25:", error);
645
+ }
646
+ const finalHeight = terrainHeight + cameraHeight;
647
+ this.viewer.camera.flyTo({
648
+ destination: C.Cartesian3.fromDegrees(centerLon, centerLat, finalHeight),
649
+ orientation: {
650
+ heading: C.Math.toRadians(0),
651
+ pitch: C.Math.toRadians(isVertical ? -90 : -45),
652
+ roll: 0
653
+ },
654
+ duration
655
+ });
656
+ }
376
657
  };
377
658
 
378
659
  // src/core/toggle2D3D.ts
@@ -537,6 +818,65 @@ function toggle2D3D(ctx, options) {
537
818
  scene.requestRender?.();
538
819
  }
539
820
 
821
+ // src/utils/GeoBounds.ts
822
+ function calculateGeoBounds(points) {
823
+ if (!points || points.length === 0) {
824
+ return null;
825
+ }
826
+ let minLon = Infinity;
827
+ let maxLon = -Infinity;
828
+ let minLat = Infinity;
829
+ let maxLat = -Infinity;
830
+ let minAlt;
831
+ let maxAlt;
832
+ points.forEach((point) => {
833
+ minLon = Math.min(minLon, point.lon);
834
+ maxLon = Math.max(maxLon, point.lon);
835
+ minLat = Math.min(minLat, point.lat);
836
+ maxLat = Math.max(maxLat, point.lat);
837
+ if (point.alt !== void 0) {
838
+ if (minAlt === void 0 || point.alt < minAlt) {
839
+ minAlt = point.alt;
840
+ }
841
+ if (maxAlt === void 0 || point.alt > maxAlt) {
842
+ maxAlt = point.alt;
843
+ }
844
+ }
845
+ });
846
+ return {
847
+ west: minLon,
848
+ east: maxLon,
849
+ south: minLat,
850
+ north: maxLat,
851
+ centerLon: (minLon + maxLon) / 2,
852
+ centerLat: (minLat + maxLat) / 2,
853
+ width: maxLon - minLon,
854
+ height: maxLat - minLat,
855
+ minAlt,
856
+ maxAlt
857
+ };
858
+ }
859
+ function expandBounds(bounds, padding = 0.1) {
860
+ const lonPadding = bounds.width * padding;
861
+ const latPadding = bounds.height * padding;
862
+ const west = bounds.west - lonPadding;
863
+ const east = bounds.east + lonPadding;
864
+ const south = bounds.south - latPadding;
865
+ const north = bounds.north + latPadding;
866
+ return {
867
+ west,
868
+ east,
869
+ south,
870
+ north,
871
+ centerLon: (west + east) / 2,
872
+ centerLat: (south + north) / 2,
873
+ width: east - west,
874
+ height: north - south,
875
+ minAlt: bounds.minAlt,
876
+ maxAlt: bounds.maxAlt
877
+ };
878
+ }
879
+
540
880
  // src/core/CameraManager.ts
541
881
  var CameraManager = class {
542
882
  constructor(CesiumNS, viewer) {
@@ -625,12 +965,6 @@ var CameraManager = class {
625
965
  options
626
966
  );
627
967
  }
628
- /**
629
- * @deprecated Use toggle2D3D instead.
630
- */
631
- toggleBirdsEye3D(options) {
632
- this.toggle2D3D(options);
633
- }
634
968
  /**
635
969
  * 切换鸟瞰视角(垂直俯视)
636
970
  * 在当前3D场景中切换到垂直向下的俯视角度,并可选择使用正交投影
@@ -829,26 +1163,6 @@ var CameraManager = class {
829
1163
  const { destination, heading = 0, pitch = -Math.PI / 4, roll = 0 } = view;
830
1164
  this.viewer.camera.setView({ destination, orientation: { heading, pitch, roll } });
831
1165
  }
832
- // Debug helper: move camera to Chengdu, Sichuan
833
- moveToChengdu() {
834
- const lon = 104.0665;
835
- const lat = 30.5728;
836
- const height = 2e3;
837
- const dest = this.CesiumNS.Cartesian3.fromDegrees(lon, lat, height);
838
- this.viewer.trackedEntity = void 0;
839
- this.viewer.camera.lookAtTransform(this.CesiumNS.Matrix4.IDENTITY);
840
- this.viewer.camera.setView({
841
- destination: dest,
842
- orientation: { heading: 0, pitch: -0.8, roll: 0 }
843
- });
844
- }
845
- isitwork() {
846
- console.log("isitwork!!!!!!!!", this.viewer);
847
- console.log("isitwork cesium!!!!!!!!", this.CesiumNS);
848
- this.viewer.camera.setView({
849
- destination: this.CesiumNS.Cartesian3.fromDegrees(-117.16, 32.71, 15e3)
850
- });
851
- }
852
1166
  setViewEasy(args) {
853
1167
  const parsed = this.parseLonLat(args.destination);
854
1168
  const lon = parsed.lon;
@@ -1051,6 +1365,241 @@ var CameraManager = class {
1051
1365
  });
1052
1366
  });
1053
1367
  }
1368
+ // ==================== 截图相关方法 ====================
1369
+ /**
1370
+ * 飞行到边界框位置(用于截图)
1371
+ * 根据航点计算边界框,并将相机移动到合适位置以覆盖整个区域
1372
+ *
1373
+ * @param waypoints 航点数组
1374
+ * @param options 配置选项
1375
+ * @returns 计算出的相机高度
1376
+ *
1377
+ * @example
1378
+ * ```typescript
1379
+ * const waypoints = [
1380
+ * { lon: 104.0, lat: 30.5 },
1381
+ * { lon: 104.1, lat: 30.6 },
1382
+ * ];
1383
+ * await cameraManager.flyToBoundsForScreenshot(waypoints, { topDown: true });
1384
+ * ```
1385
+ */
1386
+ flyToBoundsForScreenshot(waypoints, options) {
1387
+ const bounds = calculateGeoBounds(waypoints);
1388
+ if (!bounds) return null;
1389
+ const {
1390
+ padding = 0.3,
1391
+ topDown = true,
1392
+ duration = 0,
1393
+ heightMultiplier = 1.05
1394
+ } = options || {};
1395
+ const C = this.CesiumNS;
1396
+ const camera = this.viewer.camera;
1397
+ const scene = this.viewer.scene;
1398
+ const expandedBounds = expandBounds(bounds, padding);
1399
+ const rectangle = C.Rectangle.fromDegrees(
1400
+ expandedBounds.west,
1401
+ expandedBounds.south,
1402
+ expandedBounds.east,
1403
+ expandedBounds.north
1404
+ );
1405
+ const cartographicCenter = C.Rectangle.center(rectangle);
1406
+ const canvas = scene.canvas;
1407
+ const aspectRatio = canvas.width / canvas.height;
1408
+ const earthRadius = 6371e3;
1409
+ const rectWidth = rectangle.width * earthRadius * Math.cos(cartographicCenter.latitude);
1410
+ const rectHeight = rectangle.height * earthRadius;
1411
+ const frustum = camera.frustum;
1412
+ const fovy = frustum.fovy || frustum.fov || C.Math.toRadians(60);
1413
+ const fovx = 2 * Math.atan(Math.tan(fovy / 2) * aspectRatio);
1414
+ const altitudeForHeight = rectHeight / 2 / Math.tan(fovy / 2);
1415
+ const altitudeForWidth = rectWidth / 2 / Math.tan(fovx / 2);
1416
+ const relativeAltitude = Math.max(altitudeForHeight, altitudeForWidth, 500) * heightMultiplier;
1417
+ const maxWaypointAlt = bounds.maxAlt ?? 0;
1418
+ const finalAltitude = maxWaypointAlt + relativeAltitude;
1419
+ console.log("[CameraManager] \u822A\u70B9\u6700\u5927\u6D77\u62D4:", maxWaypointAlt, "\u76F8\u5BF9\u9AD8\u5EA6:", relativeAltitude, "\u6700\u7EC8\u76F8\u673A\u9AD8\u5EA6:", finalAltitude);
1420
+ const destination = C.Cartesian3.fromRadians(
1421
+ cartographicCenter.longitude,
1422
+ cartographicCenter.latitude,
1423
+ finalAltitude
1424
+ );
1425
+ const orientation = topDown ? { heading: 0, pitch: C.Math.toRadians(-90), roll: 0 } : { heading: 0, pitch: C.Math.toRadians(-45), roll: 0 };
1426
+ if (duration > 0) {
1427
+ camera.flyTo({ destination, orientation, duration });
1428
+ } else {
1429
+ camera.setView({ destination, orientation });
1430
+ }
1431
+ return finalAltitude;
1432
+ }
1433
+ /**
1434
+ * 对当前 Cesium 场景进行截图
1435
+ *
1436
+ * @param options 截图选项
1437
+ * @returns 截图结果,包含 dataUrl、blob、file
1438
+ *
1439
+ * @example
1440
+ * ```typescript
1441
+ * const result = cameraManager.captureScreenshot();
1442
+ * console.log(result.dataUrl); // base64 图片
1443
+ * ```
1444
+ */
1445
+ captureScreenshot(options) {
1446
+ const { format = "image/png", quality = 0.92, fileName } = options || {};
1447
+ if (!this.viewer?.canvas) {
1448
+ throw new Error("\u65E0\u6548\u7684 Cesium Viewer \u5B9E\u4F8B\u6216 canvas \u4E0D\u5B58\u5728");
1449
+ }
1450
+ this.viewer.render();
1451
+ const canvas = this.viewer.canvas;
1452
+ const dataUrl = canvas.toDataURL(format, quality);
1453
+ const blob = this.dataURLtoBlob(dataUrl);
1454
+ const finalFileName = fileName || this.generateScreenshotFileName(format);
1455
+ const file = new File([blob], finalFileName, { type: format });
1456
+ return { dataUrl, blob, file };
1457
+ }
1458
+ /**
1459
+ * 调整视角并截图(异步)
1460
+ * 根据航点自动调整相机位置,等待渲染完成后截图
1461
+ *
1462
+ * @param options 截图选项
1463
+ * @returns 截图结果
1464
+ *
1465
+ * @example
1466
+ * ```typescript
1467
+ * const waypoints = [
1468
+ * { lon: 104.0, lat: 30.5 },
1469
+ * { lon: 104.1, lat: 30.6 },
1470
+ * ];
1471
+ * const result = await cameraManager.captureScreenshotWithView({
1472
+ * waypoints,
1473
+ * topDown: true,
1474
+ * padding: 0.3,
1475
+ * });
1476
+ * ```
1477
+ */
1478
+ async captureScreenshotWithView(options) {
1479
+ const {
1480
+ format = "image/png",
1481
+ quality = 0.92,
1482
+ fileName,
1483
+ waypoints,
1484
+ topDown = true,
1485
+ padding = 0.3,
1486
+ restoreCamera = true,
1487
+ renderDelay = 2e3
1488
+ } = options || {};
1489
+ if (!this.viewer?.scene) {
1490
+ throw new Error("\u65E0\u6548\u7684 Cesium Viewer \u5B9E\u4F8B");
1491
+ }
1492
+ const scene = this.viewer.scene;
1493
+ const camera = this.viewer.camera;
1494
+ const originalPosition = camera.position.clone();
1495
+ const originalHeading = camera.heading;
1496
+ const originalPitch = camera.pitch;
1497
+ const originalRoll = camera.roll;
1498
+ try {
1499
+ if (waypoints && waypoints.length > 0) {
1500
+ console.log("[CameraManager] \u5F00\u59CB\u8C03\u6574\u76F8\u673A\u4F4D\u7F6E\uFF0C\u822A\u70B9\u6570\u91CF:", waypoints.length);
1501
+ const altitude = this.flyToBoundsForScreenshot(waypoints, { padding, topDown, duration: 0 });
1502
+ console.log("[CameraManager] \u8BA1\u7B97\u7684\u76F8\u673A\u9AD8\u5EA6:", altitude);
1503
+ scene.requestRender();
1504
+ await this.waitForAnimationFrame();
1505
+ console.log("[CameraManager] \u7B49\u5F85\u74E6\u7247\u52A0\u8F7D (" + renderDelay + "ms)...");
1506
+ await this.delay(renderDelay);
1507
+ for (let i = 0; i < 20; i++) {
1508
+ scene.requestRender();
1509
+ await this.waitForAnimationFrame();
1510
+ if (i % 5 === 4) {
1511
+ await this.delay(100);
1512
+ }
1513
+ }
1514
+ console.log("[CameraManager] \u7B49\u5F85\u5B8C\u6210\uFF0C\u51C6\u5907\u622A\u56FE");
1515
+ }
1516
+ console.log("[CameraManager] \u5F00\u59CB\u622A\u56FE...");
1517
+ const result = await this.captureAfterRender({ format, quality, fileName });
1518
+ console.log("[CameraManager] \u622A\u56FE\u5B8C\u6210");
1519
+ if (restoreCamera && waypoints && waypoints.length > 0) {
1520
+ camera.setView({
1521
+ destination: originalPosition,
1522
+ orientation: {
1523
+ heading: originalHeading,
1524
+ pitch: originalPitch,
1525
+ roll: originalRoll
1526
+ }
1527
+ });
1528
+ }
1529
+ return result;
1530
+ } catch (error) {
1531
+ console.error("[CameraManager] \u622A\u56FE\u5931\u8D25:", error);
1532
+ if (restoreCamera) {
1533
+ camera.setView({
1534
+ destination: originalPosition,
1535
+ orientation: {
1536
+ heading: originalHeading,
1537
+ pitch: originalPitch,
1538
+ roll: originalRoll
1539
+ }
1540
+ });
1541
+ }
1542
+ throw error;
1543
+ }
1544
+ }
1545
+ /**
1546
+ * 等待渲染完成后截图
1547
+ */
1548
+ captureAfterRender(options) {
1549
+ return new Promise((resolve, reject) => {
1550
+ const { format = "image/png", quality = 0.92, fileName } = options;
1551
+ const scene = this.viewer.scene;
1552
+ const removeCallback = scene.postRender.addEventListener(() => {
1553
+ removeCallback();
1554
+ try {
1555
+ const canvas = scene.canvas;
1556
+ const dataUrl = canvas.toDataURL(format, quality);
1557
+ const blob = this.dataURLtoBlob(dataUrl);
1558
+ const finalFileName = fileName || this.generateScreenshotFileName(format);
1559
+ const file = new File([blob], finalFileName, { type: format });
1560
+ resolve({ dataUrl, blob, file });
1561
+ } catch (error) {
1562
+ reject(error);
1563
+ }
1564
+ });
1565
+ scene.requestRender();
1566
+ });
1567
+ }
1568
+ /**
1569
+ * 将 Base64 DataURL 转换为 Blob
1570
+ */
1571
+ dataURLtoBlob(dataUrl) {
1572
+ const arr = dataUrl.split(",");
1573
+ const mimeMatch = arr[0].match(/:(.*?);/);
1574
+ const mime = mimeMatch ? mimeMatch[1] : "image/png";
1575
+ const bstr = atob(arr[1]);
1576
+ let n = bstr.length;
1577
+ const u8arr = new Uint8Array(n);
1578
+ while (n--) {
1579
+ u8arr[n] = bstr.charCodeAt(n);
1580
+ }
1581
+ return new Blob([u8arr], { type: mime });
1582
+ }
1583
+ /**
1584
+ * 生成截图文件名
1585
+ */
1586
+ generateScreenshotFileName(format) {
1587
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1588
+ const ext = format === "image/jpeg" ? "jpg" : format === "image/webp" ? "webp" : "png";
1589
+ return `screenshot_${timestamp}.${ext}`;
1590
+ }
1591
+ /**
1592
+ * 延迟工具函数
1593
+ */
1594
+ delay(ms) {
1595
+ return new Promise((resolve) => setTimeout(resolve, ms));
1596
+ }
1597
+ /**
1598
+ * 等待下一个动画帧
1599
+ */
1600
+ waitForAnimationFrame() {
1601
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
1602
+ }
1054
1603
  };
1055
1604
 
1056
1605
  // src/core/SceneManager.ts
@@ -3535,6 +4084,28 @@ var VertexLabelManager = class {
3535
4084
  }
3536
4085
  if (this.labels[index]) {
3537
4086
  this.layer.entities.remove(this.labels[index]);
4087
+ this.labels[index] = void 0;
4088
+ }
4089
+ try {
4090
+ const entitiesToRemove = [];
4091
+ const allEntities = this.layer.entities.values;
4092
+ for (let i = 0; i < allEntities.length; i++) {
4093
+ const entity = allEntities[i];
4094
+ try {
4095
+ const props = entity.properties;
4096
+ const type = props?._type?.getValue?.() ?? props?._type;
4097
+ const ownerId = props?._ownerId?.getValue?.() ?? props?._ownerId;
4098
+ const vertexIndex = props?._vertexIndex?.getValue?.() ?? props?._vertexIndex;
4099
+ if (type === "vertex-label" && ownerId === this.ownerId && vertexIndex === index) {
4100
+ entitiesToRemove.push(entity);
4101
+ }
4102
+ } catch {
4103
+ }
4104
+ }
4105
+ for (const entity of entitiesToRemove) {
4106
+ this.layer.entities.remove(entity);
4107
+ }
4108
+ } catch {
3538
4109
  }
3539
4110
  this.labels[index] = this.createLabel(position, index, state);
3540
4111
  } catch (e) {
@@ -3552,8 +4123,8 @@ var VertexLabelManager = class {
3552
4123
  return;
3553
4124
  }
3554
4125
  const label = this.labels[index];
3555
- if (label && label.position) {
3556
- label.position.setValue(position);
4126
+ if (label) {
4127
+ label.position = position;
3557
4128
  }
3558
4129
  } catch (e) {
3559
4130
  console.error("Failed to update vertex label position:", e);
@@ -3561,12 +4132,11 @@ var VertexLabelManager = class {
3561
4132
  }
3562
4133
  /**
3563
4134
  * 插入新标签(在指定索引处)
4135
+ * 注意:此方法只更新内部数组,调用方需要随后调用 recreateAllLabels() 来重建所有标签
3564
4136
  */
3565
4137
  insertLabel(insertAt, position, state = "normal") {
3566
4138
  try {
3567
- const newLabel = this.createLabel(position, insertAt, state);
3568
- this.labels.splice(insertAt, 0, newLabel);
3569
- this.recreateAllLabels();
4139
+ this.labels.splice(insertAt, 0, void 0);
3570
4140
  } catch (e) {
3571
4141
  console.error("Failed to insert vertex label:", e);
3572
4142
  }
@@ -3596,6 +4166,27 @@ var VertexLabelManager = class {
3596
4166
  this.layer.entities.remove(this.labels[i]);
3597
4167
  }
3598
4168
  }
4169
+ try {
4170
+ const entitiesToRemove = [];
4171
+ const allEntities = this.layer.entities.values;
4172
+ for (let i = 0; i < allEntities.length; i++) {
4173
+ const entity = allEntities[i];
4174
+ try {
4175
+ const props = entity.properties;
4176
+ const type = props?._type?.getValue?.() ?? props?._type;
4177
+ const ownerId = props?._ownerId?.getValue?.() ?? props?._ownerId;
4178
+ if (type === "vertex-label" && ownerId === this.ownerId) {
4179
+ entitiesToRemove.push(entity);
4180
+ }
4181
+ } catch {
4182
+ }
4183
+ }
4184
+ for (const entity of entitiesToRemove) {
4185
+ this.layer.entities.remove(entity);
4186
+ }
4187
+ } catch {
4188
+ }
4189
+ this.labels = [];
3599
4190
  if (positions) {
3600
4191
  for (let i = 0; i < positions.length; i++) {
3601
4192
  const isEdited = editedIndices?.has(i) ?? false;
@@ -3624,6 +4215,26 @@ var VertexLabelManager = class {
3624
4215
  this.layer.entities.remove(label);
3625
4216
  }
3626
4217
  });
4218
+ try {
4219
+ const entitiesToRemove = [];
4220
+ const allEntities = this.layer.entities.values;
4221
+ for (let i = 0; i < allEntities.length; i++) {
4222
+ const entity = allEntities[i];
4223
+ try {
4224
+ const props = entity.properties;
4225
+ const type = props?._type?.getValue?.() ?? props?._type;
4226
+ const ownerId = props?._ownerId?.getValue?.() ?? props?._ownerId;
4227
+ if (type === "vertex-label" && ownerId === this.ownerId) {
4228
+ entitiesToRemove.push(entity);
4229
+ }
4230
+ } catch {
4231
+ }
4232
+ }
4233
+ for (const entity of entitiesToRemove) {
4234
+ this.layer.entities.remove(entity);
4235
+ }
4236
+ } catch {
4237
+ }
3627
4238
  this.labels = [];
3628
4239
  } catch (e) {
3629
4240
  console.error("Failed to destroy vertex labels:", e);
@@ -4254,7 +4865,7 @@ var VertexDragHandler = class {
4254
4865
  }
4255
4866
  };
4256
4867
 
4257
- // src/core/path-manager/utils/TerrainHeightQuery.ts
4868
+ // src/utils/TerrainHeightQuery.ts
4258
4869
  async function queryTerrainHeights(CesiumNS, viewer, positions) {
4259
4870
  const C = CesiumNS;
4260
4871
  if (!positions || positions.length === 0) {
@@ -6179,15 +6790,15 @@ function startPathEditing(CesiumNS, viewer, entityOrId, options) {
6179
6790
  } catch {
6180
6791
  }
6181
6792
  }
6182
- try {
6183
- vertexLabelManager.updateLabelPosition(i, newPosition);
6184
- } catch {
6185
- }
6186
6793
  try {
6187
6794
  createOrUpdateMarkerForIndex(i);
6188
6795
  } catch {
6189
6796
  }
6190
6797
  }
6798
+ try {
6799
+ vertexLabelManager.recreateAllLabels(positions, editedIndices, activeIndex);
6800
+ } catch {
6801
+ }
6191
6802
  currentAltitudeMode = newMode;
6192
6803
  quickEditOptions.altitudeMode = newMode;
6193
6804
  try {