一、虚拟数据实现飞行原理在进行实时通信模拟飞机飞行的试验之前需要先了解一下虚拟数据下的飞机飞行原理。说白了其实原理很简单就是在cesium上添加一个飞机模型然后给定初始时间和结束时间给定初始位置和结束位置将位置均匀的分布在每一秒上这样就有了飞机飞行的路径然后让飞机沿着路径从头飞到尾就行。效果如图airline我们可以看到视频里面有几个重要的元素飞机模型、飞机航线、时间轴、时间轴操作按钮甚至还有鹰眼地图实时跟随。实现逻辑如下1.1 鹰眼地图鹰眼地图实现其实就是再创建一个cesium地球只不过需要增加一段代码这些代码就一个作用就是让鹰眼地图的视角view以飞机位置为主也就是所谓的跟随。还是老方法创建地图容器给定样式初始化地图代码如下template div classmap-content div idhawkEyeContainer /div /div /template script export default { mounted() { this.$nextTick(() { initViewMap(); }); }, }; /script style langscss scoped .map-content { position: absolute; width: 96%; height: 96%; top: 2%; left: 2%; overflow: hidden; } #hawkEyeContainer { position: absolute; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%; z-index: 4; } /style其中initViewMap与之前文章中提到的初始化地图几乎一模一样。function initViewMap(){ hawkEyeViewer new Cesium.Viewer(hawkEyeContainer, { timeline: true, animation: false, navigation: false, geocoder: false, sceneModePicker: false, navigationHelpButton: false, skyBox: false, //隐藏天空盒 shouldAnimate: true, homeButton: false, //是否显示Home按钮 fullscreenButton: false, //是否显示全屏按钮 baseLayerPicker: false, //是否显示图层选择控件 navigationHelpButton: false, //是否显示帮助信息控件 infoBox: false, //是否显示点击要素之后显示的信息 // terrainProvider : false, scene3DOnly: false, //每个几何实例将只能以3D渲染以节省GPU内存 selectionIndicator: false, // 隐藏双击entity时的选中框 orderIndependentTranslucency: false, imageryProvider: false, contextOptions: { webgl: { alpha: true, }, }, }); // 与主地图相同的本地瓦片断网时鹰眼仍可显示并随相机/实体同步 hawkEyeViewer.imageryLayers.addImageryProvider( new Cesium.WebMapTileServiceImageryProvider({ url: TDT_IMG_C, layer: tdtImg_c, style: default, format: tiles, tileMatrixSetID: c, subdomains: [t0, t1, t2, t3, t4, t5, t6, t7], tilingScheme: new Cesium.GeographicTilingScheme(), tileMatrixLabels: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ], maximumLevel: 50, show: false, }) ); hawkEyeViewer.sceneMode Cesium.SceneMode.SCENE2D; // 隐藏鹰眼图的相关控件只保留地图显示 hawkEyeViewer.cesiumWidget.creditContainer.style.display none; //隐藏logo hawkEyeViewer._cesiumWidget._creditContainer.style.display none; //隐藏大气圈及星空背景 hawkEyeViewer.scene.skyAtmosphere.brightShift -1; hawkEyeViewer.scene.backgroundColor new Cesium.Color(0.0, 0.0, 0.0, 0.0); hawkEyeViewer.scene.globe.baseColor new Cesium.Color(0.0, 0.0, 0.0, 0.0); ////判断是否支持图像渲染像素化处理 if (Cesium.FeatureDetection.supportsImageRenderingPixelated()) { hawkEyeViewer.resolutionScale window.devicePixelRatio; } hawkEyeViewer.scene.fxaa true; hawkEyeViewer.scene.postProcessStages.fxaa.enabled true; // 设置缩放的最大高度 hawkEyeViewer.scene.screenSpaceCameraController.minimumZoomDistance 100; hawkEyeViewer.scene.screenSpaceCameraController.maximumZoomDistance 50050000; //隐藏时间轴 hawkEyeViewer.timeline.container.style.display none; }以上就是将鹰眼地图初始化完成然后在飞机飞行时实时定位视角即可实现鹰眼地图跟随效果。1.2 实现模拟飞机飞行首先需要准备一个飞机模型我这里是用的cesium官方的飞机模型Cesium_Air.glb初始化cesium地图和时间轴后就可以直接调用生成飞机飞行轨迹。具体代码如下//模拟数据 function createModel() { let params windowTimeParams(); // 设置飞机飞行时间线 const start Cesium.JulianDate.fromDate(new Date(params.str_current)); const stop Cesium.JulianDate.addSeconds(start, 3600, new Cesium.JulianDate()); const property computeFly(); const orientation new Cesium.VelocityOrientationProperty(property); const viewFrom orientation.getValue(start) viewFrom.z 20 // 相对上下 viewFrom.x -80 // 相对前后 viewFrom.y -80 // 相对前后 // 创建飞机实体 const planeEntity viewer.entities.add({ position: property, model: { uri: static/data/Cesium_Air.glb, scale: 1.0, minimumPixelSize: 64 }, orientation: orientation, // 朝向飞行方向 viewFrom: viewFrom, path: { show: true, leadTime: 0, trailTime: 300, // 显示5分钟尾迹 resolution: 1, material: new Cesium.PolylineGlowMaterialProperty({ glowPower: 0.1, color: Cesium.Color.GREEN.withAlpha(1) }), width: 5 } }); // 鹰眼图跟随逻辑 - 始终以飞机为中心 function updateHawkEyeView() { // 获取当前飞机位置 const currentTime viewer.clock.currentTime; const planePosition planeEntity.position.getValue(currentTime); if (planePosition) { // 计算飞机位置的经纬度 const cartographic Cesium.Cartographic.fromCartesian(planePosition); const longitude Cesium.Math.toDegrees(cartographic.longitude); const latitude Cesium.Math.toDegrees(cartographic.latitude); // 关键改进设置鹰眼图相机中心为飞机当前位置 // 保持固定高度和俯视角度确保飞机始终在鹰眼图中心 hawkEyeViewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(longitude, latitude, 1500), // 视角高度 orientation: { heading: Cesium.Math.toRadians(0), // 朝向北方 pitch: Cesium.Math.toRadians(-90), // 完全俯视 roll: 0 } }); } } // 每帧更新鹰眼图视图确保实时跟随 viewer.clock.onTick.addEventListener(updateHawkEyeView); // 初始更新一次鹰眼图 updateHawkEyeView(); // 设置 viewer 一直跟随 entity // viewer.trackedEntity entity } function computeFly() { let params windowTimeParams(); var startTime Cesium.JulianDate.fromDate(new Date(params.str_current)); const property new Cesium.SampledPositionProperty(); property.setInterpolationOptions({ interpolationDegree: 2, // 二次插值运动更平滑 interpolationAlgorithm: Cesium.LagrangePolynomialApproximation }); const startPoint [116.81701689886864, 40.46263853921053]; const endPoint [116.97619523643587, 40.32489599191241]; const length 180; // 增加采样点数量从50提高到180时间间隔更密 const totalSeconds 360; // 总时长3600秒1小时 const lonAvg (endPoint[0] - startPoint[0]) / length const latAvg (endPoint[1] - startPoint[1]) / length const timeStep totalSeconds / (length - 1); for (let index 0; index length; index) { const time Cesium.JulianDate.addSeconds(startTime, timeStep * index, new Cesium.JulianDate()) const position Cesium.Cartesian3.fromDegrees(startPoint[0] lonAvg * index, startPoint[1] latAvg * index, 500) property.addSample(time, position) } return property } function windowTimeParams(date_str) { //获取系统时间用于setLayer时设定图层时间 let start, stop, current, timeStr, ctime, multiplier, str_start, str_stop, str_current, realTime; let e_date, e_y, e_m, e_d, e_h; //结束时间 let s_date, s_y, s_m, s_d, s_h; //开始时间 let c_date, c_y, c_m, c_d, c_h, c_minute, c_seconds; //预测时间事件信息 let local_y, local_m, local_d, local_h, local_minute, local_seconds; //本地时间事件信息 if (date_str) { let y date_str.split(/)[0] * 1; let m date_str.split(/)[1] * 1; let d date_str.split(/)[2] * 1; s_date new Date(y, m - 1, d); e_date new Date(s_date.getTime() 1000 * 60 * 60 * 24); if (e_date.getTime() Date.now()) { e_date new Date(new Date() - 1000 * 60 * 60 * 1); } c_date e_date; } else { // 一个小时的延迟预测24小时内的数据 e_date new Date(new Date().getTime() 1000 * 60 * 60 * 24); s_date new Date(e_date - 1000 * 60 * 60 * 24 * 2); c_date new Date(new Date().getTime()); } c_y c_date.getUTCFullYear(); c_m c_date.getUTCMonth() 1; c_d c_date.getUTCDate(); c_h c_date.getUTCHours(); c_minute c_date.getUTCMinutes(); c_seconds c_date.getUTCSeconds(); e_y e_date.getUTCFullYear(); e_m e_date.getUTCMonth() 1; e_d e_date.getUTCDate(); e_h e_date.getUTCHours(); s_y s_date.getUTCFullYear(); s_m s_date.getUTCMonth() 1; s_d s_date.getUTCDate(); s_h s_date.getUTCHours(); local_y c_date.getFullYear(); local_m c_date.getMonth() 1; local_d c_date.getDate(); local_h c_date.getHours(); local_minute c_date.getMinutes(); local_seconds c_date.getSeconds(); str_start s_y - convers(s_m) - convers(s_d) T convers(s_h) :00:00.00Z; str_stop e_y - convers(e_m) - convers(e_d) T convers(e_h) :00:00.00Z; str_current c_y - convers(c_m) - convers(c_d) T convers(c_h) : convers(c_minute) : convers(c_seconds) .00Z; ctime str_current; start Cesium.JulianDate.fromIso8601(str_start); stop Cesium.JulianDate.fromIso8601(str_stop); current Cesium.JulianDate.fromIso8601(str_current); multiplier 3600 / 3; timeStr local_y / convers(local_m) / convers(local_d) nbsp convers(local_h) : convers(local_minute) : convers(local_seconds); realTime local_y - convers(local_m) - convers(local_d) T convers(local_h) : convers(local_minute) : convers(local_seconds) .00Z; let params { start: start, stop: stop, current: current, timeStr: timeStr, ctime: ctime, multiplier: multiplier, str_start: str_start, str_stop: str_stop, str_current: str_current, }; return params; }然后在页面初始化之后调用createModel()即可。二、雷达通信真实数据实时飞行模拟这边的雷达通讯是飞机上有一个通信设备然后地面有一个接收设备每一秒发一次信号接收回信息后需要后端建立通信协议接收到消息后将信息通过websocket实时传给前端前端利用websocket与后端建立通信实时接收后端传回来的数据。由于项目属于军工项目有些不便展示这里仅讲一下前端实现过程具体实现逻辑如下createWebSocket() { var that this; enableTimelineAutoSyncToggle(); this.websocket new WebSocket(ws://xxxxxxx); this.websocket.onopen () { console.log(连接成功); }; this.websocket.onmessage (evt) { let res JSON.parse(evt.data); //惯导 this.position res; //同步时间轴 syncCesiumClockToServer(res.timestr, { pastSeconds: 60, futureSeconds: 300 }); let data { type: subscribe, remarkType: table_change } if (this.websocket) { this.websocket.send(JSON.stringify(data)); } }; // 连接关闭 this.websocket.onclose () { console.log(WebSocket连接已关闭) } // 连接错误 this.websocket.onerror (error) { console.error(WebSocket错误:, error) } },/** * 监听 timeline DOM用户操作时暂停自动同步操作结束后恢复 */ function enableTimelineAutoSyncToggle() { const timelineEl document.querySelector(.cesium-timeline) || document.getElementById(cesium-timeline) || document.getElementById(cesiumContainer); if (!timelineEl) return; timelineEl.addEventListener(mousedown, () { _autoSyncEnabled false; }); timelineEl.addEventListener(touchstart, () { _autoSyncEnabled false; }); const restore () { setTimeout(() { _autoSyncEnabled true; }, 400); }; window.addEventListener(mouseup, restore); window.addEventListener(touchend, restore); }这里的position就是通信设备返回的飞机实时位置然后在watch中监听飞机位置这里就是为了建立这一秒的飞机轨迹有了一秒前的起始点和当前位置也就是一秒后的终点和时间就可以按照之前的逻辑建立飞机飞行轨迹。具体实现如下//真实数据情况 — 使用实体属性直接更新避免闪烁 let _planeEntity null; // 飞机实体 let _sampledPosition null; // 采样位置属性 let _velocityOrientation null; // 速度朝向属性 let _hawkTickHandler null; // 鹰眼图跟随处理器 let _timeTxtHandler null; // 时间牌更新处理器 let _lastUpdateTime 0; // 上次更新时间 const UPDATE_INTERVAL 50; // 最小更新间隔毫秒- 降低节流间隔提高流畅度 const MAX_SAMPLES 3600; // 最大位置样本数约1小时对应trailTime let _isFirstUpdate true; // 是否首次更新 let _firstTime null; // 记录第一个时间点用于设置时间轴范围 /** * 初始化飞机实体 */ function initPlaneEntity() { if (_planeEntity) return; // 创建采样位置属性支持插值 _sampledPosition new Cesium.SampledPositionProperty(); _sampledPosition.setInterpolationOptions({ interpolationDegree: 2, interpolationAlgorithm: Cesium.LagrangePolynomialApproximation }); // 创建速度朝向属性 _velocityOrientation new Cesium.VelocityOrientationProperty(_sampledPosition); // 创建飞机实体 _planeEntity viewer.entities.add({ id: real-time-plane, position: _sampledPosition, orientation: _velocityOrientation, model: { uri: static/data/Cesium_Air.glb, scale: 1.0, minimumPixelSize: 64 }, path: { show: true, leadTime: 0, trailTime: 7200, // 显示1小时轨迹 resolution: 1, material: new Cesium.PolylineGlowMaterialProperty({ glowPower: 0.2, color: Cesium.Color.GREEN }), width: 4, clampToGround: false } }); // console.log(Plane entity initialized); } /** * 更新飞机位置核心方法 */ function updatePlanePosition(lon, lat, height, time) { if (!_sampledPosition) return; // 添加位置样本 _sampledPosition.addSample(time, Cesium.Cartesian3.fromDegrees(lon, lat, height)); // 限制样本数量防止内存泄漏 const samples _sampledPosition._property._times; if (samples samples.length MAX_SAMPLES) { // 创建新的SampledPositionProperty并复制最近的样本 const newProperty new Cesium.SampledPositionProperty(); newProperty.setInterpolationOptions({ interpolationDegree: 2, interpolationAlgorithm: Cesium.LagrangePolynomialApproximation }); // 复制最近的MAX_SAMPLES个样本 const startIndex samples.length - MAX_SAMPLES; for (let i startIndex; i samples.length; i) { const sampleTime samples[i]; const samplePos _sampledPosition.getValue(sampleTime); if (samplePos) { newProperty.addSample(sampleTime, samplePos); } } // 替换旧属性 _sampledPosition newProperty; _velocityOrientation new Cesium.VelocityOrientationProperty(_sampledPosition); // 更新实体的属性引用 _planeEntity.position _sampledPosition; _planeEntity.orientation _velocityOrientation; } // 同步Cesium时钟到当前数据时间 viewer.clock.currentTime time; viewer.clock.multiplier 1; viewer.clock.shouldAnimate false; // 启用动画播放确保飞机平滑移动 } /** * 设置鹰眼图跟随 */ function setupHawkEyeFollow() { if (_hawkTickHandler) return; _hawkTickHandler function updateHawkEyeView() { if (!_planeEntity) return; const currentTime viewer.clock.currentTime; const planePosition _planeEntity.position.getValue(currentTime); if (planePosition typeof hawkEyeViewer ! undefined hawkEyeViewer hawkEyeViewer.camera) { const cartographic Cesium.Cartographic.fromCartesian(planePosition); const longitude Cesium.Math.toDegrees(cartographic.longitude); const latitude Cesium.Math.toDegrees(cartographic.latitude); hawkEyeViewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(longitude, latitude, 2000), orientation: { heading: Cesium.Math.toRadians(0), pitch: Cesium.Math.toRadians(-90), roll: 0 } }); } }; viewer.clock.onTick.addEventListener(_hawkTickHandler); } /** * 主入口根据位置和时间更新飞机 */ function createModelReal(newData, oldData) { try { // 参数校验 if (!newData || typeof newData ! object || !newData.timestr || !newData.lon || !newData.lat) { console.warn(createModelReal skipped: invalid newData, newData); return; } const currentTimeMs Date.now(); // 节流避免过于频繁的更新 if (!_isFirstUpdate currentTimeMs - _lastUpdateTime UPDATE_INTERVAL) { return; } _lastUpdateTime currentTimeMs; _isFirstUpdate false; // 解析位置和时间 const lon Number(newData.lon); const lat Number(newData.lat); const height 2000; const time Cesium.JulianDate.fromDate(new Date(newData.timestr)); // 记录第一个时间点用于设置时间轴范围 if (!_firstTime) { _firstTime time.clone(); // 设置时间轴范围从第一个时间点开始往后1小时 const endTime Cesium.JulianDate.addSeconds(_firstTime, 3600, new Cesium.JulianDate()); viewer.clock.startTime _firstTime.clone(); viewer.clock.stopTime endTime; viewer.timeline.zoomTo(_firstTime, endTime); // 注册时间牌更新处理器 if (!_timeTxtHandler) { _timeTxtHandler function updateTimeTxt() { try { const currentTime viewer.clock.currentTime; const gregorianDate Cesium.JulianDate.toGregorianDate(currentTime); const localTime Cesium.JulianDate.addHours(currentTime, 8, new Cesium.JulianDate()); const localGregorian Cesium.JulianDate.toGregorianDate(localTime); const timeStr localGregorian.year / convers(localGregorian.month) / convers(localGregorian.day) nbsp convers(localGregorian.hour) : convers(localGregorian.minute) : convers(localGregorian.second); const timeTxtElement document.getElementById(time-txt); if (timeTxtElement) { timeTxtElement.innerHTML timeStr; } } catch (err) { console.warn(updateTimeTxt error:, err); } }; viewer.clock.onTick.addEventListener(_timeTxtHandler); } // 初始更新时间牌 const localTime Cesium.JulianDate.addHours(time, 8, new Cesium.JulianDate()); const localGregorian Cesium.JulianDate.toGregorianDate(localTime); const timeStr localGregorian.year / convers(localGregorian.month) / convers(localGregorian.day) nbsp convers(localGregorian.hour) : convers(localGregorian.minute) : convers(localGregorian.second); const timeTxtElement document.getElementById(time-txt); if (timeTxtElement) { timeTxtElement.innerHTML timeStr; } } // 初始化飞机实体仅首次 initPlaneEntity(); // 更新位置 updatePlanePosition(lon, lat, height, time); // 设置鹰眼图跟随 setupHawkEyeFollow(); // console.log(Plane updated:, lon, lat, newData.timestr); } catch (err) { console.error(createModelReal error:, err); } }最终效果如图