| | |
| | | <script setup lang="ts"> |
| | | import { onBeforeUnmount, onMounted, ref, watch } from "vue"; |
| | | import { AimOutlined, BorderOutlined } from "@ant-design/icons-vue"; |
| | | import { |
| | | ArcGisMapServerImageryProvider, |
| | | Color, |
| | | ColorMaterialProperty, |
| | | ConstantProperty, |
| | |
| | | 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"); |
| | |
| | | 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) { |
| | |
| | | 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 () => { |
| | |
| | | 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> |