194.Vue3 + OpenLayers 实战:动态位置 + 高度 + 角度,模拟卫星地面覆盖范围

发布时间:2026/7/25 9:08:42

194.Vue3 + OpenLayers 实战:动态位置 + 高度 + 角度,模拟卫星地面覆盖范围 一、前言实现效果动态生成“椭圆覆盖区域”并实时展示在地图上在做无人机巡检系统 / 卫星可视化系统时经常会遇到这样一个需求根据设备的位置、高度、俯仰角、方位角动态计算地面覆盖范围比如卫星观测区域无人机摄像头可视范围雷达扫描范围本文将带你用Vue3Composition APIOpenLayersTurf.js 输入参数经度 / 纬度高度alt俯仰角pitch方位角azimuth视场角angle 点击按钮✅ 自动计算✅ 地图中心移动✅ 绘制椭圆覆盖区域 三、核心原理重点这个效果的本质其实是1️⃣ 覆盖中心点计算通过高度alt俯仰角pitch方位角azimuth计算投影到地面的点pp tan(pitch) * alt再拆成x sin(azimuth) * pp y cos(azimuth) * pp 得到地面偏移量2️⃣ 椭圆长短轴计算b tan(angle/2) * alt // 短轴 a (tan(pitchangle/2) - tan(pitch-angle/2)) * alt / 2 // 长轴 解释angle 摄像头视场角pitch 倾斜角所以 覆盖区域 ≠ 圆形而是椭圆3️⃣ 使用 Turf 生成椭圆turf.ellipse(center, a, b, { angle }) 四、完整代码实现!-- * Author: 彭麒 * Date: 2026/3/23 * Email: 1062470959qq.com * Description: 此源码版权归吉檀迦俐所有可供学习和借鉴或商用。 -- template div classcontainer div classw-full flex justify-center flex-wrap div classfont-bold text-[24px] Vue3 Openlayers动态位置高度角度模拟卫星地面覆盖区域的大小 /div /div div classnav el-input v-modellon sizesmalltemplate #prepend经度/template/el-input el-input v-modellat sizesmalltemplate #prepend纬度/template/el-input el-input v-modelalt sizesmalltemplate #prepend高度/template/el-input el-input v-modelpitch sizesmalltemplate #prepend俯仰角/template/el-input el-input v-modelazimuth sizesmalltemplate #prepend转向角/template/el-input el-input v-modelangle sizesmalltemplate #prepend天线可视角/template/el-input el-button typeprimary sizesmall clickellipse显示椭圆形/el-button el-button typeprimary sizesmall clickclearLayer清除图层/el-button /div div idvue-openlayers/div /div /template script setup import { ref, onMounted } from vue import ol/ol.css import Map from ol/Map import View from ol/View import TileLayer from ol/layer/Tile import VectorSource from ol/source/Vector import VectorLayer from ol/layer/Vector import XYZ from ol/source/XYZ import { fromLonLat, toLonLat } from ol/proj import * as turf from turf/turf import GeoJSON from ol/format/GeoJSON import Feature from ol/Feature import { Fill, Stroke, Style, Circle } from ol/style import { Point } from ol/geom // 响应式数据 const map ref(null) const turfSource new VectorSource({ wrapX: false }) const pointSource new VectorSource({ wrapX: false }) const lon ref(-75) const lat ref(40) const alt ref(500000) const pitch ref(45) const angle ref(60) const azimuth ref(0) // 样式 const featureStyle () new Style({ fill: new Fill({ color: rgba(0,0,0,0.1) }), stroke: new Stroke({ width: 2, color: #f00 }), image: new Circle({ radius: 3, fill: new Fill({ color: #0000ff }), }), }) const featureStyle2 () new Style({ fill: new Fill({ color: rgba(0,0,0,0.1) }), stroke: new Stroke({ width: 2, color: #f00 }), image: new Circle({ radius: 3, fill: new Fill({ color: #ff00ff }), }), }) // 方法 const show (geojsonData) { const features new GeoJSON().readFeatures(geojsonData, { dataProjection: EPSG:4326, featureProjection: EPSG:3857, }) turfSource.addFeatures(features) } const clearLayer () { turfSource.clear() } const getcoord (lonVal, latVal, altVal, pitchVal, azimuthVal, angleVal) { const pp Math.tan((pitchVal * Math.PI) / 180) * altVal const ww Math.sin((azimuthVal * Math.PI) / 180) * pp const hh Math.cos((azimuthVal * Math.PI) / 180) * pp const c0c fromLonLat([lonVal, latVal]) const clon c0c[0] ww const clat c0c[1] hh const b Math.tan(((angleVal / 2) * Math.PI) / 180) * altVal const aa Math.tan(((pitchVal angleVal / 2) * Math.PI) / 180) * altVal const ab Math.tan(((pitchVal - angleVal / 2) * Math.PI) / 180) * altVal const a (aa - ab) / 2 const cc toLonLat([clon, clat]) // 中心点 const pointFeature new Feature({ geometry: new Point([clon, clat]), }) turfSource.addFeature(pointFeature) map.value.getView().setCenter([clon, clat]) return [cc, a, b] } const originPoint () { const pointFeature new Feature({ geometry: new Point(fromLonLat([-75, 40])), }) pointSource.addFeature(pointFeature) } const ellipse () { const [center, a, b] getcoord( lon.value, lat.value, alt.value, pitch.value, azimuth.value, angle.value ) const ellipseGeo turf.ellipse(center, a / 1000, b / 1000, { angle: Number(azimuth.value), }) show(ellipseGeo) } const initMap () { const baseLayer new TileLayer({ source: new XYZ({ url: https://www.google.com/maps/vt?lyrsmglenx{x}y{y}z{z}, crossOrigin: anonymous, }), }) const turfLayer new VectorLayer({ source: turfSource, style: featureStyle(), }) const pointLayer new VectorLayer({ source: pointSource, style: featureStyle2(), }) map.value new Map({ target: vue-openlayers, layers: [baseLayer, turfLayer, pointLayer], view: new View({ projection: EPSG:3857, center: fromLonLat([-75, 40]), zoom: 6, }), }) } // 生命周期 onMounted(() { initMap() originPoint() }) /script style scoped .container { width: 840px; height: 620px; margin: 50px auto; border: 1px solid #42B983; } #vue-openlayers { width: 600px; height: 500px; border: 1px solid #42B983; float: left; } .nav { float: left; width: 210px; height: 500px; margin-right: 10px; padding-top: 10px; } .nav .el-input-group { width: 200px; padding: 0 5px; margin-bottom: 10px; } /style⚠️ 五、容易踩坑点非常关键❌ 1. 坐标系问题必须注意EPSG:4326 → EPSG:3857 否则图层会错位❌ 2. 单位问题turf.ellipse(center, a / 1000, b / 1000) Turf 默认单位是公里❌ 3. pitch 边界pitch → 接近 90° 会爆炸tan无穷 建议限制0° pitch 85° 六、进阶优化建议收藏✅ 1. 实时响应推荐不用按钮直接自动更新watch([lon, lat, alt, pitch, azimuth, angle], ellipse)✅ 2. 图层分离工程必备建议拆覆盖区 layer设备 layer轨迹 layer✅ 3. 封装成 composableuseEllipse.js 方便复用到无人机卫星雷达✅ 4. 动画效果高级可以做扫描动画动态覆盖变化飞行轨迹联动 七、应用场景这个方案可以直接用于 卫星覆盖分析 无人机巡检系统 雷达扫描模拟 摄像头视野分析 八、总结一句话总结通过“空间几何 三角函数 Turf OpenLayers”可以优雅实现动态覆盖区域模拟 九、如果你想进阶如果你在做项目无人机巡检系统数字孪生GIS可视化平台我可以帮你继续升级✅ 多机协同覆盖✅ 扇形扫描✅ 3DCesium版本✅ 性能优化10w要素

相关新闻