@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 +740 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +354 -8
- package/dist/index.d.ts +354 -8
- package/dist/index.js +733 -38
- package/dist/index.js.map +1 -1
- package/dist/vue/index.cjs +647 -36
- package/dist/vue/index.cjs.map +1 -1
- package/dist/vue/index.js +647 -36
- package/dist/vue/index.js.map +1 -1
- package/package.json +1 -1
package/dist/vue/index.cjs
CHANGED
|
@@ -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
|
|
3556
|
-
label.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
|
-
|
|
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/
|
|
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 {
|