@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.
package/dist/index.cjs CHANGED
@@ -13,7 +13,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
13
13
  // package.json
14
14
  var package_default = {
15
15
  name: "@jorgmoritz/gis-manager",
16
- version: "0.1.41"};
16
+ version: "0.1.42"};
17
17
 
18
18
  // src/utils/version.ts
19
19
  var version = package_default.version;
@@ -417,6 +417,287 @@ var LayerManager = class {
417
417
  getTileset(id) {
418
418
  return this.tilesetIdMap.get(id);
419
419
  }
420
+ /**
421
+ * 从配置加载地图图层(正射影像、中文注记、地形)
422
+ * @param config 地图图层配置
423
+ * @param options 可选配置
424
+ * @returns 加载结果
425
+ */
426
+ async loadMapLayersFromConfig(config, options) {
427
+ const result = {
428
+ success: true,
429
+ errors: []
430
+ };
431
+ const {
432
+ tiandituToken = "f44fcec420c3eb1c56656e9a22dabb17",
433
+ subdomains = ["0", "1", "2", "3", "4", "5", "6", "7"],
434
+ maximumLevel = 18
435
+ } = options || {};
436
+ const C = this.CesiumNS;
437
+ const { imgUrl, ciaUrl, terrUrl } = config;
438
+ if (imgUrl) {
439
+ try {
440
+ const imageryProvider = this.createImageryProviderFromUrl(imgUrl, {
441
+ tiandituToken,
442
+ subdomains,
443
+ maximumLevel
444
+ });
445
+ if (imageryProvider) {
446
+ result.imageryLayer = this.viewer.imageryLayers.addImageryProvider(imageryProvider);
447
+ console.log("[LayerManager] \u6B63\u5C04\u5F71\u50CF\u52A0\u8F7D\u6210\u529F:", imgUrl);
448
+ }
449
+ } catch (error) {
450
+ const errMsg = `\u6B63\u5C04\u5F71\u50CF\u52A0\u8F7D\u5931\u8D25: ${error}`;
451
+ console.error("[LayerManager]", errMsg);
452
+ result.errors.push(errMsg);
453
+ }
454
+ }
455
+ if (ciaUrl) {
456
+ try {
457
+ const ciaProvider = this.createImageryProviderFromUrl(ciaUrl, {
458
+ tiandituToken,
459
+ subdomains,
460
+ maximumLevel
461
+ });
462
+ if (ciaProvider) {
463
+ result.annotationLayer = this.viewer.imageryLayers.addImageryProvider(ciaProvider);
464
+ console.log("[LayerManager] \u4E2D\u6587\u6CE8\u8BB0\u52A0\u8F7D\u6210\u529F:", ciaUrl);
465
+ }
466
+ } catch (error) {
467
+ const errMsg = `\u4E2D\u6587\u6CE8\u8BB0\u52A0\u8F7D\u5931\u8D25: ${error}`;
468
+ console.error("[LayerManager]", errMsg);
469
+ result.errors.push(errMsg);
470
+ }
471
+ }
472
+ if (terrUrl) {
473
+ try {
474
+ const terrainProvider = await C.CesiumTerrainProvider.fromUrl(terrUrl);
475
+ this.viewer.terrainProvider = terrainProvider;
476
+ result.terrainProvider = terrainProvider;
477
+ console.log("[LayerManager] \u5730\u5F62\u6570\u636E\u52A0\u8F7D\u6210\u529F:", terrUrl);
478
+ } catch (error) {
479
+ const errMsg = `\u5730\u5F62\u6570\u636E\u52A0\u8F7D\u5931\u8D25: ${error}`;
480
+ console.warn("[LayerManager]", errMsg);
481
+ result.errors.push(errMsg);
482
+ }
483
+ }
484
+ result.success = result.errors.length === 0;
485
+ return result;
486
+ }
487
+ /**
488
+ * 根据 URL 格式自动创建合适的 ImageryProvider
489
+ * @param url 影像 URL
490
+ * @param options 配置选项
491
+ */
492
+ createImageryProviderFromUrl(url, options) {
493
+ const C = this.CesiumNS;
494
+ const { tiandituToken, subdomains, maximumLevel } = options;
495
+ if (url.includes("{z}") || url.includes("{TileMatrix}")) {
496
+ return new C.UrlTemplateImageryProvider({
497
+ url,
498
+ subdomains,
499
+ tilingScheme: new C.WebMercatorTilingScheme(),
500
+ maximumLevel
501
+ });
502
+ }
503
+ if (url.includes("wmts")) {
504
+ const tkMatch = url.match(/tk=([^&]+)/);
505
+ const tk = tkMatch ? tkMatch[1] : tiandituToken;
506
+ return new C.WebMapTileServiceImageryProvider({
507
+ url: `https://t{s}.tianditu.gov.cn/img_w/wmts?tk=${tk}`,
508
+ subdomains,
509
+ layer: "img",
510
+ style: "default",
511
+ tileMatrixSetID: "w",
512
+ format: "image/jpeg",
513
+ tilingScheme: new C.WebMercatorTilingScheme(),
514
+ maximumLevel
515
+ });
516
+ }
517
+ return new C.UrlTemplateImageryProvider({
518
+ url,
519
+ subdomains,
520
+ tilingScheme: new C.WebMercatorTilingScheme(),
521
+ maximumLevel
522
+ });
523
+ }
524
+ /**
525
+ * 从 SliceJson 配置加载 TIF 切片数据(DOM 正射影像或 Terrain 地形)
526
+ * @param sliceJsonData JSON 字符串或已解析的对象
527
+ * @param options 加载选项
528
+ * @returns 加载结果
529
+ */
530
+ async loadFromSliceJson(sliceJsonData, options) {
531
+ const { layerId, flyTo = true, flyDuration = 2, opacity = 1 } = options || {};
532
+ let data;
533
+ try {
534
+ data = typeof sliceJsonData === "string" ? JSON.parse(sliceJsonData) : sliceJsonData;
535
+ } catch (error) {
536
+ return {
537
+ success: false,
538
+ type: "dom",
539
+ data: {},
540
+ error: `JSON \u89E3\u6790\u5931\u8D25: ${error}`
541
+ };
542
+ }
543
+ if (!data || !data.type || !data.urlroot) {
544
+ return {
545
+ success: false,
546
+ type: data?.type || "dom",
547
+ data: data || {},
548
+ error: "\u65E0\u6548\u7684 SliceJson \u6570\u636E\uFF1A\u7F3A\u5C11 type \u6216 urlroot"
549
+ };
550
+ }
551
+ if (data.type === "dom") {
552
+ return this.loadDomFromSliceJson(data, { layerId, flyTo, flyDuration, opacity });
553
+ } else if (data.type === "terrain") {
554
+ return this.loadTerrainFromSliceJson(data, { flyTo, flyDuration });
555
+ } else {
556
+ return {
557
+ success: false,
558
+ type: data.type,
559
+ data,
560
+ error: `\u4E0D\u652F\u6301\u7684\u6570\u636E\u7C7B\u578B: ${data.type}`
561
+ };
562
+ }
563
+ }
564
+ /**
565
+ * 加载 DOM 正射影像
566
+ */
567
+ async loadDomFromSliceJson(data, options) {
568
+ const { layerId, flyTo, flyDuration, opacity } = options;
569
+ const C = this.CesiumNS;
570
+ try {
571
+ const url = `${data.urlroot}/{z}/{x}/{reverseY}.png`;
572
+ let bounds = data.bounds;
573
+ if (bounds && bounds.length >= 4) {
574
+ if (Math.abs(bounds[1]) > 90 && Math.abs(bounds[2]) <= 90) {
575
+ bounds = [bounds[0], bounds[2], bounds[1], bounds[3]];
576
+ }
577
+ }
578
+ const rectangle = bounds ? C.Rectangle.fromDegrees(bounds[0], bounds[1], bounds[2], bounds[3]) : void 0;
579
+ const provider = new C.UrlTemplateImageryProvider({
580
+ url,
581
+ minimumLevel: data.minZoom,
582
+ maximumLevel: data.maxZoom,
583
+ rectangle
584
+ });
585
+ const layer = this.viewer.imageryLayers.addImageryProvider(provider);
586
+ layer.alpha = opacity;
587
+ const id = layerId || `slice_dom_${++layerIdSeq}`;
588
+ layer._customId = id;
589
+ this.imageryIdMap.set(id, layer);
590
+ const handle = { id, type: "imagery", dispose: () => this.remove(id) };
591
+ this.events.emit({ added: handle });
592
+ console.log("[LayerManager] DOM \u6B63\u5C04\u5F71\u50CF\u52A0\u8F7D\u6210\u529F:", data.urlroot);
593
+ if (flyTo) {
594
+ await this.flyToSliceJsonTarget(data, flyDuration);
595
+ }
596
+ return {
597
+ success: true,
598
+ type: "dom",
599
+ handle,
600
+ imageryLayer: layer,
601
+ data
602
+ };
603
+ } catch (error) {
604
+ const errMsg = `DOM \u52A0\u8F7D\u5931\u8D25: ${error}`;
605
+ console.error("[LayerManager]", errMsg);
606
+ return {
607
+ success: false,
608
+ type: "dom",
609
+ data,
610
+ error: errMsg
611
+ };
612
+ }
613
+ }
614
+ /**
615
+ * 加载 Terrain 地形数据
616
+ */
617
+ async loadTerrainFromSliceJson(data, options) {
618
+ const { flyTo, flyDuration } = options;
619
+ const C = this.CesiumNS;
620
+ try {
621
+ const timestamp = Date.now();
622
+ const terrainUrl = `${data.urlroot}?t=${timestamp}`;
623
+ const terrainProvider = await C.CesiumTerrainProvider.fromUrl(terrainUrl, {
624
+ requestVertexNormals: true,
625
+ requestWaterMask: true
626
+ });
627
+ this.viewer.terrainProvider = terrainProvider;
628
+ this.terrainId = `slice_ter_${++layerIdSeq}`;
629
+ console.log("[LayerManager] Terrain \u5730\u5F62\u52A0\u8F7D\u6210\u529F:", data.urlroot);
630
+ if (flyTo) {
631
+ await new Promise((resolve) => setTimeout(resolve, 500));
632
+ await this.flyToSliceJsonTarget(data, flyDuration, true);
633
+ }
634
+ return {
635
+ success: true,
636
+ type: "terrain",
637
+ terrainProvider,
638
+ data
639
+ };
640
+ } catch (error) {
641
+ const errMsg = `Terrain \u52A0\u8F7D\u5931\u8D25: ${error}`;
642
+ console.error("[LayerManager]", errMsg);
643
+ return {
644
+ success: false,
645
+ type: "terrain",
646
+ data,
647
+ error: errMsg
648
+ };
649
+ }
650
+ }
651
+ /**
652
+ * 飞行到 SliceJson 数据的目标位置
653
+ */
654
+ async flyToSliceJsonTarget(data, duration, isVertical = false) {
655
+ const C = this.CesiumNS;
656
+ let centerLon;
657
+ let centerLat;
658
+ let cameraHeight = 2e3;
659
+ if (data.center && data.center.length >= 2 && (data.center[0] !== 0 || data.center[1] !== 0)) {
660
+ centerLon = data.center[0];
661
+ centerLat = data.center[1];
662
+ }
663
+ if (data.bounds && data.bounds.length >= 4) {
664
+ const bounds = data.bounds;
665
+ if (centerLon === void 0 || centerLat === void 0) {
666
+ centerLon = (bounds[0] + bounds[2]) / 2;
667
+ centerLat = (bounds[1] + bounds[3]) / 2;
668
+ }
669
+ const lonSpan = Math.abs(bounds[2] - bounds[0]);
670
+ const latSpan = Math.abs(bounds[3] - bounds[1]);
671
+ const maxSpan = Math.max(lonSpan, latSpan);
672
+ const heightMultiplier = isVertical ? 2 : 0.8;
673
+ cameraHeight = Math.max(maxSpan * 111e3 * heightMultiplier, 500);
674
+ }
675
+ if (centerLon === void 0 || centerLat === void 0) {
676
+ console.warn("[LayerManager] \u65E0\u6CD5\u83B7\u53D6\u6709\u6548\u7684\u4E2D\u5FC3\u70B9");
677
+ return;
678
+ }
679
+ let terrainHeight = 0;
680
+ try {
681
+ const terrainProvider = this.viewer.terrainProvider;
682
+ if (terrainProvider && !(terrainProvider instanceof C.EllipsoidTerrainProvider)) {
683
+ const positions = [C.Cartographic.fromDegrees(centerLon, centerLat)];
684
+ const sampledPositions = await C.sampleTerrainMostDetailed(terrainProvider, positions);
685
+ terrainHeight = sampledPositions[0]?.height || 0;
686
+ }
687
+ } catch (error) {
688
+ console.warn("[LayerManager] \u5730\u5F62\u91C7\u6837\u5931\u8D25:", error);
689
+ }
690
+ const finalHeight = terrainHeight + cameraHeight;
691
+ this.viewer.camera.flyTo({
692
+ destination: C.Cartesian3.fromDegrees(centerLon, centerLat, finalHeight),
693
+ orientation: {
694
+ heading: C.Math.toRadians(0),
695
+ pitch: C.Math.toRadians(isVertical ? -90 : -45),
696
+ roll: 0
697
+ },
698
+ duration
699
+ });
700
+ }
420
701
  };
421
702
 
422
703
  // src/core/toggle2D3D.ts
@@ -581,6 +862,114 @@ function toggle2D3D(ctx, options) {
581
862
  scene.requestRender?.();
582
863
  }
583
864
 
865
+ // src/utils/GeoBounds.ts
866
+ function calculateGeoBounds(points) {
867
+ if (!points || points.length === 0) {
868
+ return null;
869
+ }
870
+ let minLon = Infinity;
871
+ let maxLon = -Infinity;
872
+ let minLat = Infinity;
873
+ let maxLat = -Infinity;
874
+ let minAlt;
875
+ let maxAlt;
876
+ points.forEach((point) => {
877
+ minLon = Math.min(minLon, point.lon);
878
+ maxLon = Math.max(maxLon, point.lon);
879
+ minLat = Math.min(minLat, point.lat);
880
+ maxLat = Math.max(maxLat, point.lat);
881
+ if (point.alt !== void 0) {
882
+ if (minAlt === void 0 || point.alt < minAlt) {
883
+ minAlt = point.alt;
884
+ }
885
+ if (maxAlt === void 0 || point.alt > maxAlt) {
886
+ maxAlt = point.alt;
887
+ }
888
+ }
889
+ });
890
+ return {
891
+ west: minLon,
892
+ east: maxLon,
893
+ south: minLat,
894
+ north: maxLat,
895
+ centerLon: (minLon + maxLon) / 2,
896
+ centerLat: (minLat + maxLat) / 2,
897
+ width: maxLon - minLon,
898
+ height: maxLat - minLat,
899
+ minAlt,
900
+ maxAlt
901
+ };
902
+ }
903
+ function calculateBoundsDiagonal(bounds) {
904
+ const earthRadius = 6371e3;
905
+ const latRad = bounds.centerLat * Math.PI / 180;
906
+ const widthMeters = bounds.width * (Math.PI / 180) * earthRadius * Math.cos(latRad);
907
+ const heightMeters = bounds.height * (Math.PI / 180) * earthRadius;
908
+ return Math.sqrt(widthMeters * widthMeters + heightMeters * heightMeters);
909
+ }
910
+ function expandBounds(bounds, padding = 0.1) {
911
+ const lonPadding = bounds.width * padding;
912
+ const latPadding = bounds.height * padding;
913
+ const west = bounds.west - lonPadding;
914
+ const east = bounds.east + lonPadding;
915
+ const south = bounds.south - latPadding;
916
+ const north = bounds.north + latPadding;
917
+ return {
918
+ west,
919
+ east,
920
+ south,
921
+ north,
922
+ centerLon: (west + east) / 2,
923
+ centerLat: (south + north) / 2,
924
+ width: east - west,
925
+ height: north - south,
926
+ minAlt: bounds.minAlt,
927
+ maxAlt: bounds.maxAlt
928
+ };
929
+ }
930
+ function isPointInBounds(point, bounds) {
931
+ return point.lon >= bounds.west && point.lon <= bounds.east && point.lat >= bounds.south && point.lat <= bounds.north;
932
+ }
933
+ function mergeBounds(boundsArray) {
934
+ if (!boundsArray || boundsArray.length === 0) {
935
+ return null;
936
+ }
937
+ let minLon = Infinity;
938
+ let maxLon = -Infinity;
939
+ let minLat = Infinity;
940
+ let maxLat = -Infinity;
941
+ let minAlt;
942
+ let maxAlt;
943
+ boundsArray.forEach((bounds) => {
944
+ minLon = Math.min(minLon, bounds.west);
945
+ maxLon = Math.max(maxLon, bounds.east);
946
+ minLat = Math.min(minLat, bounds.south);
947
+ maxLat = Math.max(maxLat, bounds.north);
948
+ if (bounds.minAlt !== void 0) {
949
+ if (minAlt === void 0 || bounds.minAlt < minAlt) {
950
+ minAlt = bounds.minAlt;
951
+ }
952
+ }
953
+ if (bounds.maxAlt !== void 0) {
954
+ if (maxAlt === void 0 || bounds.maxAlt > maxAlt) {
955
+ maxAlt = bounds.maxAlt;
956
+ }
957
+ }
958
+ });
959
+ return {
960
+ west: minLon,
961
+ east: maxLon,
962
+ south: minLat,
963
+ north: maxLat,
964
+ centerLon: (minLon + maxLon) / 2,
965
+ centerLat: (minLat + maxLat) / 2,
966
+ width: maxLon - minLon,
967
+ height: maxLat - minLat,
968
+ minAlt,
969
+ maxAlt
970
+ };
971
+ }
972
+
584
973
  // src/core/CameraManager.ts
585
974
  var CameraManager = class {
586
975
  constructor(CesiumNS, viewer) {
@@ -669,12 +1058,6 @@ var CameraManager = class {
669
1058
  options
670
1059
  );
671
1060
  }
672
- /**
673
- * @deprecated Use toggle2D3D instead.
674
- */
675
- toggleBirdsEye3D(options) {
676
- this.toggle2D3D(options);
677
- }
678
1061
  /**
679
1062
  * 切换鸟瞰视角(垂直俯视)
680
1063
  * 在当前3D场景中切换到垂直向下的俯视角度,并可选择使用正交投影
@@ -873,26 +1256,6 @@ var CameraManager = class {
873
1256
  const { destination, heading = 0, pitch = -Math.PI / 4, roll = 0 } = view;
874
1257
  this.viewer.camera.setView({ destination, orientation: { heading, pitch, roll } });
875
1258
  }
876
- // Debug helper: move camera to Chengdu, Sichuan
877
- moveToChengdu() {
878
- const lon = 104.0665;
879
- const lat = 30.5728;
880
- const height = 2e3;
881
- const dest = this.CesiumNS.Cartesian3.fromDegrees(lon, lat, height);
882
- this.viewer.trackedEntity = void 0;
883
- this.viewer.camera.lookAtTransform(this.CesiumNS.Matrix4.IDENTITY);
884
- this.viewer.camera.setView({
885
- destination: dest,
886
- orientation: { heading: 0, pitch: -0.8, roll: 0 }
887
- });
888
- }
889
- isitwork() {
890
- console.log("isitwork!!!!!!!!", this.viewer);
891
- console.log("isitwork cesium!!!!!!!!", this.CesiumNS);
892
- this.viewer.camera.setView({
893
- destination: this.CesiumNS.Cartesian3.fromDegrees(-117.16, 32.71, 15e3)
894
- });
895
- }
896
1259
  setViewEasy(args) {
897
1260
  const parsed = this.parseLonLat(args.destination);
898
1261
  const lon = parsed.lon;
@@ -1095,6 +1458,241 @@ var CameraManager = class {
1095
1458
  });
1096
1459
  });
1097
1460
  }
1461
+ // ==================== 截图相关方法 ====================
1462
+ /**
1463
+ * 飞行到边界框位置(用于截图)
1464
+ * 根据航点计算边界框,并将相机移动到合适位置以覆盖整个区域
1465
+ *
1466
+ * @param waypoints 航点数组
1467
+ * @param options 配置选项
1468
+ * @returns 计算出的相机高度
1469
+ *
1470
+ * @example
1471
+ * ```typescript
1472
+ * const waypoints = [
1473
+ * { lon: 104.0, lat: 30.5 },
1474
+ * { lon: 104.1, lat: 30.6 },
1475
+ * ];
1476
+ * await cameraManager.flyToBoundsForScreenshot(waypoints, { topDown: true });
1477
+ * ```
1478
+ */
1479
+ flyToBoundsForScreenshot(waypoints, options) {
1480
+ const bounds = calculateGeoBounds(waypoints);
1481
+ if (!bounds) return null;
1482
+ const {
1483
+ padding = 0.3,
1484
+ topDown = true,
1485
+ duration = 0,
1486
+ heightMultiplier = 1.05
1487
+ } = options || {};
1488
+ const C = this.CesiumNS;
1489
+ const camera = this.viewer.camera;
1490
+ const scene = this.viewer.scene;
1491
+ const expandedBounds = expandBounds(bounds, padding);
1492
+ const rectangle = C.Rectangle.fromDegrees(
1493
+ expandedBounds.west,
1494
+ expandedBounds.south,
1495
+ expandedBounds.east,
1496
+ expandedBounds.north
1497
+ );
1498
+ const cartographicCenter = C.Rectangle.center(rectangle);
1499
+ const canvas = scene.canvas;
1500
+ const aspectRatio = canvas.width / canvas.height;
1501
+ const earthRadius = 6371e3;
1502
+ const rectWidth = rectangle.width * earthRadius * Math.cos(cartographicCenter.latitude);
1503
+ const rectHeight = rectangle.height * earthRadius;
1504
+ const frustum = camera.frustum;
1505
+ const fovy = frustum.fovy || frustum.fov || C.Math.toRadians(60);
1506
+ const fovx = 2 * Math.atan(Math.tan(fovy / 2) * aspectRatio);
1507
+ const altitudeForHeight = rectHeight / 2 / Math.tan(fovy / 2);
1508
+ const altitudeForWidth = rectWidth / 2 / Math.tan(fovx / 2);
1509
+ const relativeAltitude = Math.max(altitudeForHeight, altitudeForWidth, 500) * heightMultiplier;
1510
+ const maxWaypointAlt = bounds.maxAlt ?? 0;
1511
+ const finalAltitude = maxWaypointAlt + relativeAltitude;
1512
+ console.log("[CameraManager] \u822A\u70B9\u6700\u5927\u6D77\u62D4:", maxWaypointAlt, "\u76F8\u5BF9\u9AD8\u5EA6:", relativeAltitude, "\u6700\u7EC8\u76F8\u673A\u9AD8\u5EA6:", finalAltitude);
1513
+ const destination = C.Cartesian3.fromRadians(
1514
+ cartographicCenter.longitude,
1515
+ cartographicCenter.latitude,
1516
+ finalAltitude
1517
+ );
1518
+ const orientation = topDown ? { heading: 0, pitch: C.Math.toRadians(-90), roll: 0 } : { heading: 0, pitch: C.Math.toRadians(-45), roll: 0 };
1519
+ if (duration > 0) {
1520
+ camera.flyTo({ destination, orientation, duration });
1521
+ } else {
1522
+ camera.setView({ destination, orientation });
1523
+ }
1524
+ return finalAltitude;
1525
+ }
1526
+ /**
1527
+ * 对当前 Cesium 场景进行截图
1528
+ *
1529
+ * @param options 截图选项
1530
+ * @returns 截图结果,包含 dataUrl、blob、file
1531
+ *
1532
+ * @example
1533
+ * ```typescript
1534
+ * const result = cameraManager.captureScreenshot();
1535
+ * console.log(result.dataUrl); // base64 图片
1536
+ * ```
1537
+ */
1538
+ captureScreenshot(options) {
1539
+ const { format = "image/png", quality = 0.92, fileName } = options || {};
1540
+ if (!this.viewer?.canvas) {
1541
+ throw new Error("\u65E0\u6548\u7684 Cesium Viewer \u5B9E\u4F8B\u6216 canvas \u4E0D\u5B58\u5728");
1542
+ }
1543
+ this.viewer.render();
1544
+ const canvas = this.viewer.canvas;
1545
+ const dataUrl = canvas.toDataURL(format, quality);
1546
+ const blob = this.dataURLtoBlob(dataUrl);
1547
+ const finalFileName = fileName || this.generateScreenshotFileName(format);
1548
+ const file = new File([blob], finalFileName, { type: format });
1549
+ return { dataUrl, blob, file };
1550
+ }
1551
+ /**
1552
+ * 调整视角并截图(异步)
1553
+ * 根据航点自动调整相机位置,等待渲染完成后截图
1554
+ *
1555
+ * @param options 截图选项
1556
+ * @returns 截图结果
1557
+ *
1558
+ * @example
1559
+ * ```typescript
1560
+ * const waypoints = [
1561
+ * { lon: 104.0, lat: 30.5 },
1562
+ * { lon: 104.1, lat: 30.6 },
1563
+ * ];
1564
+ * const result = await cameraManager.captureScreenshotWithView({
1565
+ * waypoints,
1566
+ * topDown: true,
1567
+ * padding: 0.3,
1568
+ * });
1569
+ * ```
1570
+ */
1571
+ async captureScreenshotWithView(options) {
1572
+ const {
1573
+ format = "image/png",
1574
+ quality = 0.92,
1575
+ fileName,
1576
+ waypoints,
1577
+ topDown = true,
1578
+ padding = 0.3,
1579
+ restoreCamera = true,
1580
+ renderDelay = 2e3
1581
+ } = options || {};
1582
+ if (!this.viewer?.scene) {
1583
+ throw new Error("\u65E0\u6548\u7684 Cesium Viewer \u5B9E\u4F8B");
1584
+ }
1585
+ const scene = this.viewer.scene;
1586
+ const camera = this.viewer.camera;
1587
+ const originalPosition = camera.position.clone();
1588
+ const originalHeading = camera.heading;
1589
+ const originalPitch = camera.pitch;
1590
+ const originalRoll = camera.roll;
1591
+ try {
1592
+ if (waypoints && waypoints.length > 0) {
1593
+ console.log("[CameraManager] \u5F00\u59CB\u8C03\u6574\u76F8\u673A\u4F4D\u7F6E\uFF0C\u822A\u70B9\u6570\u91CF:", waypoints.length);
1594
+ const altitude = this.flyToBoundsForScreenshot(waypoints, { padding, topDown, duration: 0 });
1595
+ console.log("[CameraManager] \u8BA1\u7B97\u7684\u76F8\u673A\u9AD8\u5EA6:", altitude);
1596
+ scene.requestRender();
1597
+ await this.waitForAnimationFrame();
1598
+ console.log("[CameraManager] \u7B49\u5F85\u74E6\u7247\u52A0\u8F7D (" + renderDelay + "ms)...");
1599
+ await this.delay(renderDelay);
1600
+ for (let i = 0; i < 20; i++) {
1601
+ scene.requestRender();
1602
+ await this.waitForAnimationFrame();
1603
+ if (i % 5 === 4) {
1604
+ await this.delay(100);
1605
+ }
1606
+ }
1607
+ console.log("[CameraManager] \u7B49\u5F85\u5B8C\u6210\uFF0C\u51C6\u5907\u622A\u56FE");
1608
+ }
1609
+ console.log("[CameraManager] \u5F00\u59CB\u622A\u56FE...");
1610
+ const result = await this.captureAfterRender({ format, quality, fileName });
1611
+ console.log("[CameraManager] \u622A\u56FE\u5B8C\u6210");
1612
+ if (restoreCamera && waypoints && waypoints.length > 0) {
1613
+ camera.setView({
1614
+ destination: originalPosition,
1615
+ orientation: {
1616
+ heading: originalHeading,
1617
+ pitch: originalPitch,
1618
+ roll: originalRoll
1619
+ }
1620
+ });
1621
+ }
1622
+ return result;
1623
+ } catch (error) {
1624
+ console.error("[CameraManager] \u622A\u56FE\u5931\u8D25:", error);
1625
+ if (restoreCamera) {
1626
+ camera.setView({
1627
+ destination: originalPosition,
1628
+ orientation: {
1629
+ heading: originalHeading,
1630
+ pitch: originalPitch,
1631
+ roll: originalRoll
1632
+ }
1633
+ });
1634
+ }
1635
+ throw error;
1636
+ }
1637
+ }
1638
+ /**
1639
+ * 等待渲染完成后截图
1640
+ */
1641
+ captureAfterRender(options) {
1642
+ return new Promise((resolve, reject) => {
1643
+ const { format = "image/png", quality = 0.92, fileName } = options;
1644
+ const scene = this.viewer.scene;
1645
+ const removeCallback = scene.postRender.addEventListener(() => {
1646
+ removeCallback();
1647
+ try {
1648
+ const canvas = scene.canvas;
1649
+ const dataUrl = canvas.toDataURL(format, quality);
1650
+ const blob = this.dataURLtoBlob(dataUrl);
1651
+ const finalFileName = fileName || this.generateScreenshotFileName(format);
1652
+ const file = new File([blob], finalFileName, { type: format });
1653
+ resolve({ dataUrl, blob, file });
1654
+ } catch (error) {
1655
+ reject(error);
1656
+ }
1657
+ });
1658
+ scene.requestRender();
1659
+ });
1660
+ }
1661
+ /**
1662
+ * 将 Base64 DataURL 转换为 Blob
1663
+ */
1664
+ dataURLtoBlob(dataUrl) {
1665
+ const arr = dataUrl.split(",");
1666
+ const mimeMatch = arr[0].match(/:(.*?);/);
1667
+ const mime = mimeMatch ? mimeMatch[1] : "image/png";
1668
+ const bstr = atob(arr[1]);
1669
+ let n = bstr.length;
1670
+ const u8arr = new Uint8Array(n);
1671
+ while (n--) {
1672
+ u8arr[n] = bstr.charCodeAt(n);
1673
+ }
1674
+ return new Blob([u8arr], { type: mime });
1675
+ }
1676
+ /**
1677
+ * 生成截图文件名
1678
+ */
1679
+ generateScreenshotFileName(format) {
1680
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1681
+ const ext = format === "image/jpeg" ? "jpg" : format === "image/webp" ? "webp" : "png";
1682
+ return `screenshot_${timestamp}.${ext}`;
1683
+ }
1684
+ /**
1685
+ * 延迟工具函数
1686
+ */
1687
+ delay(ms) {
1688
+ return new Promise((resolve) => setTimeout(resolve, ms));
1689
+ }
1690
+ /**
1691
+ * 等待下一个动画帧
1692
+ */
1693
+ waitForAnimationFrame() {
1694
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
1695
+ }
1098
1696
  };
1099
1697
 
1100
1698
  // src/core/SceneManager.ts
@@ -3579,6 +4177,28 @@ var VertexLabelManager = class {
3579
4177
  }
3580
4178
  if (this.labels[index]) {
3581
4179
  this.layer.entities.remove(this.labels[index]);
4180
+ this.labels[index] = void 0;
4181
+ }
4182
+ try {
4183
+ const entitiesToRemove = [];
4184
+ const allEntities = this.layer.entities.values;
4185
+ for (let i = 0; i < allEntities.length; i++) {
4186
+ const entity = allEntities[i];
4187
+ try {
4188
+ const props = entity.properties;
4189
+ const type = props?._type?.getValue?.() ?? props?._type;
4190
+ const ownerId = props?._ownerId?.getValue?.() ?? props?._ownerId;
4191
+ const vertexIndex = props?._vertexIndex?.getValue?.() ?? props?._vertexIndex;
4192
+ if (type === "vertex-label" && ownerId === this.ownerId && vertexIndex === index) {
4193
+ entitiesToRemove.push(entity);
4194
+ }
4195
+ } catch {
4196
+ }
4197
+ }
4198
+ for (const entity of entitiesToRemove) {
4199
+ this.layer.entities.remove(entity);
4200
+ }
4201
+ } catch {
3582
4202
  }
3583
4203
  this.labels[index] = this.createLabel(position, index, state);
3584
4204
  } catch (e) {
@@ -3596,8 +4216,8 @@ var VertexLabelManager = class {
3596
4216
  return;
3597
4217
  }
3598
4218
  const label = this.labels[index];
3599
- if (label && label.position) {
3600
- label.position.setValue(position);
4219
+ if (label) {
4220
+ label.position = position;
3601
4221
  }
3602
4222
  } catch (e) {
3603
4223
  console.error("Failed to update vertex label position:", e);
@@ -3605,12 +4225,11 @@ var VertexLabelManager = class {
3605
4225
  }
3606
4226
  /**
3607
4227
  * 插入新标签(在指定索引处)
4228
+ * 注意:此方法只更新内部数组,调用方需要随后调用 recreateAllLabels() 来重建所有标签
3608
4229
  */
3609
4230
  insertLabel(insertAt, position, state = "normal") {
3610
4231
  try {
3611
- const newLabel = this.createLabel(position, insertAt, state);
3612
- this.labels.splice(insertAt, 0, newLabel);
3613
- this.recreateAllLabels();
4232
+ this.labels.splice(insertAt, 0, void 0);
3614
4233
  } catch (e) {
3615
4234
  console.error("Failed to insert vertex label:", e);
3616
4235
  }
@@ -3640,6 +4259,27 @@ var VertexLabelManager = class {
3640
4259
  this.layer.entities.remove(this.labels[i]);
3641
4260
  }
3642
4261
  }
4262
+ try {
4263
+ const entitiesToRemove = [];
4264
+ const allEntities = this.layer.entities.values;
4265
+ for (let i = 0; i < allEntities.length; i++) {
4266
+ const entity = allEntities[i];
4267
+ try {
4268
+ const props = entity.properties;
4269
+ const type = props?._type?.getValue?.() ?? props?._type;
4270
+ const ownerId = props?._ownerId?.getValue?.() ?? props?._ownerId;
4271
+ if (type === "vertex-label" && ownerId === this.ownerId) {
4272
+ entitiesToRemove.push(entity);
4273
+ }
4274
+ } catch {
4275
+ }
4276
+ }
4277
+ for (const entity of entitiesToRemove) {
4278
+ this.layer.entities.remove(entity);
4279
+ }
4280
+ } catch {
4281
+ }
4282
+ this.labels = [];
3643
4283
  if (positions) {
3644
4284
  for (let i = 0; i < positions.length; i++) {
3645
4285
  const isEdited = editedIndices?.has(i) ?? false;
@@ -3668,6 +4308,26 @@ var VertexLabelManager = class {
3668
4308
  this.layer.entities.remove(label);
3669
4309
  }
3670
4310
  });
4311
+ try {
4312
+ const entitiesToRemove = [];
4313
+ const allEntities = this.layer.entities.values;
4314
+ for (let i = 0; i < allEntities.length; i++) {
4315
+ const entity = allEntities[i];
4316
+ try {
4317
+ const props = entity.properties;
4318
+ const type = props?._type?.getValue?.() ?? props?._type;
4319
+ const ownerId = props?._ownerId?.getValue?.() ?? props?._ownerId;
4320
+ if (type === "vertex-label" && ownerId === this.ownerId) {
4321
+ entitiesToRemove.push(entity);
4322
+ }
4323
+ } catch {
4324
+ }
4325
+ }
4326
+ for (const entity of entitiesToRemove) {
4327
+ this.layer.entities.remove(entity);
4328
+ }
4329
+ } catch {
4330
+ }
3671
4331
  this.labels = [];
3672
4332
  } catch (e) {
3673
4333
  console.error("Failed to destroy vertex labels:", e);
@@ -4298,7 +4958,7 @@ var VertexDragHandler = class {
4298
4958
  }
4299
4959
  };
4300
4960
 
4301
- // src/core/path-manager/utils/TerrainHeightQuery.ts
4961
+ // src/utils/TerrainHeightQuery.ts
4302
4962
  async function queryTerrainHeights(CesiumNS, viewer, positions) {
4303
4963
  const C = CesiumNS;
4304
4964
  if (!positions || positions.length === 0) {
@@ -4350,6 +5010,41 @@ async function queryTerrainHeightAsync(CesiumNS, viewer, position) {
4350
5010
  const heights = await queryTerrainHeights(CesiumNS, viewer, [position]);
4351
5011
  return heights[0] ?? 0;
4352
5012
  }
5013
+ async function queryTerrainHeightByLonLat(CesiumNS, viewer, lon, lat) {
5014
+ const C = CesiumNS;
5015
+ const position = C.Cartesian3.fromDegrees(lon, lat, 0);
5016
+ return queryTerrainHeightAsync(CesiumNS, viewer, position);
5017
+ }
5018
+ async function queryTerrainHeightsByLonLat(CesiumNS, viewer, coordinates) {
5019
+ const C = CesiumNS;
5020
+ if (!coordinates || coordinates.length === 0) {
5021
+ return [];
5022
+ }
5023
+ const positions = coordinates.map((coord) => {
5024
+ if (Array.isArray(coord)) {
5025
+ return C.Cartesian3.fromDegrees(coord[0], coord[1], 0);
5026
+ } else {
5027
+ return C.Cartesian3.fromDegrees(coord.lon, coord.lat, 0);
5028
+ }
5029
+ });
5030
+ return queryTerrainHeights(CesiumNS, viewer, positions);
5031
+ }
5032
+ function queryTerrainHeightByLonLatSync(CesiumNS, viewer, lon, lat) {
5033
+ const C = CesiumNS;
5034
+ try {
5035
+ const cartographic = C.Cartographic.fromDegrees(lon, lat);
5036
+ const globe = viewer.scene.globe;
5037
+ if (globe && typeof globe.getHeight === "function") {
5038
+ const height = globe.getHeight(cartographic);
5039
+ if (typeof height === "number" && !isNaN(height)) {
5040
+ return height;
5041
+ }
5042
+ }
5043
+ } catch (error) {
5044
+ console.warn("[TerrainHeightQuery] \u540C\u6B65\u67E5\u8BE2\u5931\u8D25:", error);
5045
+ }
5046
+ return 0;
5047
+ }
4353
5048
  function calculateAbsoluteHeight(altitudeMode, defaultAltitude, startPointAltitude, terrainHeight) {
4354
5049
  switch (altitudeMode) {
4355
5050
  case "absolute":
@@ -6227,15 +6922,15 @@ function startPathEditing(CesiumNS, viewer, entityOrId, options) {
6227
6922
  } catch {
6228
6923
  }
6229
6924
  }
6230
- try {
6231
- vertexLabelManager.updateLabelPosition(i, newPosition);
6232
- } catch {
6233
- }
6234
6925
  try {
6235
6926
  createOrUpdateMarkerForIndex(i);
6236
6927
  } catch {
6237
6928
  }
6238
6929
  }
6930
+ try {
6931
+ vertexLabelManager.recreateAllLabels(positions, editedIndices, activeIndex);
6932
+ } catch {
6933
+ }
6239
6934
  currentAltitudeMode = newMode;
6240
6935
  quickEditOptions.altitudeMode = newMode;
6241
6936
  try {
@@ -11823,6 +12518,8 @@ exports.Selector = Selector;
11823
12518
  exports.StateManager = StateManager;
11824
12519
  exports.assertCesiumAssetsConfigured = assertCesiumAssetsConfigured;
11825
12520
  exports.calculateAbsoluteHeight = calculateAbsoluteHeight;
12521
+ exports.calculateBoundsDiagonal = calculateBoundsDiagonal;
12522
+ exports.calculateGeoBounds = calculateGeoBounds;
11826
12523
  exports.calculateRelativeHeight = calculateRelativeHeight;
11827
12524
  exports.configureCesiumAssets = configureCesiumAssets;
11828
12525
  exports.configureCesiumIonToken = configureCesiumIonToken;
@@ -11831,14 +12528,20 @@ exports.convertSinoflyWayline = convertSinoflyWayline;
11831
12528
  exports.convertSinoflyWaylines = convertSinoflyWaylines;
11832
12529
  exports.droneModelUrl = droneModelUrl;
11833
12530
  exports.ensureCesiumIonToken = ensureCesiumIonToken;
12531
+ exports.expandBounds = expandBounds;
11834
12532
  exports.getCesiumBaseUrl = getCesiumBaseUrl;
11835
12533
  exports.getCesiumIonToken = getCesiumIonToken;
11836
12534
  exports.globalCameraEventBus = globalCameraEventBus;
11837
12535
  exports.globalState = globalState;
12536
+ exports.isPointInBounds = isPointInBounds;
12537
+ exports.mergeBounds = mergeBounds;
11838
12538
  exports.placeholder = placeholder;
11839
12539
  exports.queryTerrainHeightAsync = queryTerrainHeightAsync;
12540
+ exports.queryTerrainHeightByLonLat = queryTerrainHeightByLonLat;
12541
+ exports.queryTerrainHeightByLonLatSync = queryTerrainHeightByLonLatSync;
11840
12542
  exports.queryTerrainHeightSync = queryTerrainHeightSync;
11841
12543
  exports.queryTerrainHeights = queryTerrainHeights;
12544
+ exports.queryTerrainHeightsByLonLat = queryTerrainHeightsByLonLat;
11842
12545
  exports.renderFlightPath = renderFlightPath;
11843
12546
  exports.renderFlightPathPreview = renderFlightPathPreview;
11844
12547
  exports.toggle2D3D = toggle2D3D;