<script setup lang="ts">
|
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
import { AimOutlined, BorderOutlined } from "@ant-design/icons-vue";
|
import {
|
ArcGisMapServerImageryProvider,
|
Color,
|
ColorMaterialProperty,
|
ConstantProperty,
|
GeoJsonDataSource,
|
GridImageryProvider,
|
HeightReference,
|
PointGraphics,
|
UrlTemplateImageryProvider,
|
Viewer
|
} from "cesium";
|
|
import { artifactUrl } from "@/api/artifacts";
|
|
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");
|
if (type === "restricted_zone") return Color.fromCssColorString("#bc861a");
|
if (type === "gathering") return Color.fromCssColorString("#176b50");
|
return Color.fromCssColorString("#c6533f");
|
}
|
|
async function drawCase() {
|
if (!viewer) return;
|
try {
|
mapError.value = null;
|
if (trajectorySource) viewer.dataSources.remove(trajectorySource, true);
|
if (eventSource) viewer.dataSources.remove(eventSource, true);
|
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) {
|
entity.polyline.width = new ConstantProperty(4);
|
entity.polyline.material = new ColorMaterialProperty(Color.fromCssColorString("#176b50"));
|
}
|
});
|
eventSource = await GeoJsonDataSource.load(artifactUrl(`${root}/events.geojson`), { clampToGround: false });
|
eventSource.entities.values.forEach((entity) => {
|
const type = String(entity.properties?.event_type?.getValue() ?? "stop");
|
entity.point = new PointGraphics({
|
pixelSize: new ConstantProperty(12),
|
color: new ConstantProperty(eventColor(type)),
|
outlineColor: new ConstantProperty(Color.WHITE),
|
outlineWidth: new ConstantProperty(2),
|
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(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 () => {
|
if (!host.value) return;
|
viewer = new Viewer(host.value, {
|
animation: false,
|
baseLayerPicker: false,
|
fullscreenButton: false,
|
geocoder: false,
|
homeButton: false,
|
infoBox: false,
|
navigationHelpButton: false,
|
sceneModePicker: false,
|
selectionIndicator: false,
|
timeline: false
|
});
|
await addBaseLayers();
|
await 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><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>
|