罗广辉
15 hours ago 7cc239cee1a9af4e2e8a0f3d5b7a00a074b17214
apps/workbench-console/src/components/TrajectoryMap.vue
@@ -1,6 +1,8 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
import { AimOutlined, BorderOutlined } from "@ant-design/icons-vue";
import {
  ArcGisMapServerImageryProvider,
  Color,
  ColorMaterialProperty,
  ConstantProperty,
@@ -8,17 +10,21 @@
  GridImageryProvider,
  HeightReference,
  PointGraphics,
  UrlTemplateImageryProvider,
  Viewer
} from "cesium";
import { artifactUrl } from "@/api/artifacts";
const props = defineProps<{ caseId: "difficult" | "normal" }>();
const props = defineProps<{ artifactRoot: string; showSpatialContext: boolean; showFlyableZones: boolean }>();
const host = ref<HTMLElement>();
const mapError = ref<string | null>(null);
let viewer: Viewer | null = null;
let trajectorySource: GeoJsonDataSource | null = null;
let eventSource: GeoJsonDataSource | null = null;
let routeSource: GeoJsonDataSource | null = null;
let zoneSource: GeoJsonDataSource | null = null;
let flyableZoneSource: GeoJsonDataSource | null = null;
function eventColor(type: string) {
  if (type === "route_deviation") return Color.fromCssColorString("#9a5da8");
@@ -33,7 +39,15 @@
    mapError.value = null;
    if (trajectorySource) viewer.dataSources.remove(trajectorySource, true);
    if (eventSource) viewer.dataSources.remove(eventSource, true);
    const root = `shared/outputs/15-trajectory-analysis/${props.caseId}`;
    if (routeSource) viewer.dataSources.remove(routeSource, true);
    if (zoneSource) viewer.dataSources.remove(zoneSource, true);
    if (flyableZoneSource) viewer.dataSources.remove(flyableZoneSource, true);
    trajectorySource = null;
    eventSource = null;
    routeSource = null;
    zoneSource = null;
    flyableZoneSource = null;
    const root = props.artifactRoot;
    trajectorySource = await GeoJsonDataSource.load(artifactUrl(`${root}/trajectories.geojson`), { clampToGround: false });
    trajectorySource.entities.values.forEach((entity) => {
      if (entity.polyline) {
@@ -52,12 +66,78 @@
        heightReference: new ConstantProperty(HeightReference.NONE)
      });
    });
    if (props.showSpatialContext) {
      routeSource = await GeoJsonDataSource.load(artifactUrl(`${root}/reference_routes.geojson`), { clampToGround: false });
      routeSource.entities.values.forEach((entity) => {
        if (entity.polyline) {
          entity.polyline.width = new ConstantProperty(3);
          entity.polyline.material = new ColorMaterialProperty(Color.fromCssColorString("#444444"));
        }
      });
      zoneSource = await GeoJsonDataSource.load(artifactUrl(`${root}/zones.geojson`), { clampToGround: false });
      zoneSource.entities.values.forEach((entity) => {
        if (entity.polygon) {
          entity.polygon.material = new ColorMaterialProperty(Color.fromCssColorString("#e5c951").withAlpha(0.16));
          entity.polygon.outline = new ConstantProperty(true);
          entity.polygon.outlineColor = new ConstantProperty(Color.fromCssColorString("#9a7a00"));
        }
      });
      if (props.showFlyableZones) {
        flyableZoneSource = await GeoJsonDataSource.load(artifactUrl(`${root}/flyable_zones.geojson`), { clampToGround: false });
        flyableZoneSource.entities.values.forEach((entity) => {
          if (entity.polygon) {
            entity.polygon.material = new ColorMaterialProperty(Color.fromCssColorString("#36a269").withAlpha(0.22));
            entity.polygon.outline = new ConstantProperty(true);
            entity.polygon.outlineColor = new ConstantProperty(Color.fromCssColorString("#167344"));
          }
        });
      }
    }
    if (zoneSource) await viewer.dataSources.add(zoneSource);
    if (flyableZoneSource) await viewer.dataSources.add(flyableZoneSource);
    if (routeSource) await viewer.dataSources.add(routeSource);
    await viewer.dataSources.add(trajectorySource);
    await viewer.dataSources.add(eventSource);
    await viewer.flyTo(trajectorySource, { duration: 0 });
    await viewer.flyTo(props.showSpatialContext && zoneSource ? zoneSource : trajectorySource, { duration: 0 });
  } catch (error) {
    mapError.value = error instanceof Error ? error.message : "Cesium 地图加载失败";
  }
}
async function focusTrajectory() {
  if (viewer && trajectorySource) await viewer.flyTo(trajectorySource, { duration: 0.4 });
}
async function focusZones() {
  if (viewer && zoneSource) await viewer.flyTo(zoneSource, { duration: 0.4 });
}
async function addBaseLayers() {
  if (!viewer) return;
  viewer.imageryLayers.removeAll();
  try {
    const arcgis = await ArcGisMapServerImageryProvider.fromUrl(
      "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer"
    );
    viewer.imageryLayers.addImageryProvider(arcgis);
  } catch {
    viewer.imageryLayers.addImageryProvider(new GridImageryProvider({ cells: 8, glowWidth: 0 }));
  }
  const token = (import.meta.env.VITE_TIANDITU_TOKEN ?? "").trim();
  if (!token) return;
  const subdomains = ["0", "1", "2", "3", "4", "5", "6", "7"];
  viewer.imageryLayers.addImageryProvider(new UrlTemplateImageryProvider({
    url: `https://t{s}.tianditu.gov.cn/DataServer?T=img_w&x={x}&y={y}&l={z}&tk=${token}`,
    subdomains,
    maximumLevel: 18,
    credit: "Tianditu imagery"
  }));
  viewer.imageryLayers.addImageryProvider(new UrlTemplateImageryProvider({
    url: `https://t{s}.tianditu.gov.cn/DataServer?T=cva_w&x={x}&y={y}&l={z}&tk=${token}`,
    subdomains,
    maximumLevel: 18,
    credit: "Tianditu labels"
  }));
}
onMounted(async () => {
@@ -74,15 +154,14 @@
    selectionIndicator: false,
    timeline: false
  });
  viewer.imageryLayers.removeAll();
  viewer.imageryLayers.addImageryProvider(new GridImageryProvider({ cells: 8, glowWidth: 0 }));
  await addBaseLayers();
  await drawCase();
});
watch(() => props.caseId, drawCase);
watch(() => [props.artifactRoot, props.showSpatialContext, props.showFlyableZones], drawCase);
onBeforeUnmount(() => viewer?.destroy());
</script>
<template>
  <div class="trajectory-map"><div ref="host" class="cesium-host"></div><a-alert v-if="mapError" class="map-error" type="warning" show-icon :message="mapError" /></div>
  <div class="trajectory-map"><div ref="host" class="cesium-host"></div><div v-if="showSpatialContext" class="map-legend"><span><i class="legend-track"></i>实飞轨迹</span><span><i class="legend-route"></i>计划航线</span><span><i class="legend-restricted"></i>禁飞区</span><span v-if="showFlyableZones"><i class="legend-flyable"></i>适飞区</span></div><div v-if="showSpatialContext" class="map-tools"><a-tooltip title="聚焦轨迹"><a-button shape="circle" aria-label="聚焦轨迹" @click="focusTrajectory"><AimOutlined /></a-button></a-tooltip><a-tooltip title="查看禁飞区范围"><a-button shape="circle" aria-label="查看禁飞区范围" @click="focusZones"><BorderOutlined /></a-button></a-tooltip></div><a-alert v-if="mapError" class="map-error" type="warning" show-icon :message="mapError" /></div>
</template>