三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

cesium 实战系列之雷达通信、实时模拟真实飞行场景、鹰眼地图实时跟随

cesium 实战系列之雷达通信、实时模拟真实飞行场景、鹰眼地图实时跟随

一、虚拟数据实现飞行原理

在进行实时通信模拟飞机飞行的试验之前,需要先了解一下虚拟数据下的飞机飞行原理。说白了,其实原理很简单,就是在cesium上添加一个飞机模型,然后给定初始时间和结束时间,给定初始位置和结束位置,将位置均匀的分布在每一秒上,这样就有了飞机飞行的路径,然后让飞机沿着路径从头飞到尾就行。效果如图:

airline

我们可以看到视频里面有几个重要的元素,飞机模型、飞机航线、时间轴、时间轴操作按钮,甚至还有鹰眼地图实时跟随。实现逻辑如下:

1.1 鹰眼地图

鹰眼地图实现其实就是再创建一个cesium地球,只不过需要增加一段代码,这些代码就一个作用,就是让鹰眼地图的视角view以飞机位置为主也就是所谓的跟随。

还是老方法,创建地图容器,给定样式,初始化地图,代码如下:

<template> <div class="map-content"> <div id="hawkEyeContainer"> </div> </div> </template> <script> export default { mounted() { this.$nextTick(() => { initViewMap(); }); }, }; </script> <style lang="scss" 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); } }

最终效果如图:

← 返回列表