193.Vue3 + OpenLayers 实战:圆孔相机模型推算卫星拍摄区域

🎯 一、效果展示

实现功能:

  • 输入经纬度、飞行高度

  • 设置拍摄比例

  • 一键绘制地面覆盖区域(圆形)

  • 支持清除图层

👉 本质:根据高度推算一个地面半径


📌 二、前言

在做无人机巡检系统 / 卫星遥感 / 地图可视化项目时,经常会遇到一个问题:

👉 已知相机(或卫星)的经纬度、高度,如何推算它在地面的拍摄范围?

这篇文章,我将带你用:

  • ✅ Vue3(Composition API)

  • ✅ OpenLayers

  • ✅ 圆孔相机简化模型

实现一个拍摄区域可视化 Demo(画圆)


🧠 三、核心原理(非常重要)

这里用的是一个简化版圆孔相机模型

📍 计算逻辑:

半径 = 高度 / 拍摄比例

即:

javascript 复制代码
const r = alt / proportion

📐 为什么这样算?

可以这样理解:

  • 相机在高空(alt)

  • 向地面拍摄

  • 拍摄范围随着高度变大而扩大

👉 proportion 相当于一个"缩放系数":

参数 含义
alt 相机高度(米)
proportion 拍摄缩放比例
r 地面覆盖半径

⚠️ 注意:

这个公式是工程简化模型,并不是真实物理模型(后面会讲)


🗺️ 四、核心实现思路

1️⃣ 经纬度 → 地图坐标

OpenLayers 使用的是:

EPSG:3857(墨卡托投影)

所以必须转换:

javascript 复制代码
fromLonLat([lon, lat])

2️⃣ 使用 Circle 绘制区域

javascript 复制代码
new Circle(center, radius)

3️⃣ 加入地图图层

javascript 复制代码
dataSource.addFeature(feature)

💻 五、完整核心代码

javascript 复制代码
<!--
 * @Author: 彭麒
 * @Date: 2026/3/20
 * @Email: 1062470959@qq.com
 * @Description: 此源码版权归吉檀迦俐所有,可供学习和借鉴或商用。
 -->
<template>
  <div class="container">
    <div class="w-full flex justify-center flex-wrap">
      <div class="font-bold text-[24px]">
        Vue3 + Openlayers:圆孔相机拍摄区域推算
      </div>
    </div>
    <div class="nav">
      <el-input v-model="lon" size="small">
        <template #prepend>经度</template>
      </el-input>

      <el-input v-model="lat" size="small">
        <template #prepend>纬度</template>
      </el-input>

      <el-input v-model="alt" size="small">
        <template #prepend>高度</template>
      </el-input>

      <el-input v-model="pitch" size="small" disabled>
        <template #prepend>俯仰角</template>
      </el-input>

      <el-input v-model="proportion" size="small">
        <template #prepend>拍摄比例</template>
      </el-input>

      <el-button type="primary" size="small" @click="showCircle">
        显示圆形
      </el-button>

      <el-button type="primary" size="small" @click="clearLayer">
        清除图层
      </el-button>
    </div>

    <div id="vue-openlayers"></div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import 'ol/ol.css'

import { Map, View } from 'ol'
import TileLayer from 'ol/layer/Tile'
import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'
import XYZ from 'ol/source/XYZ'
import Feature from 'ol/Feature'
import { Circle } from 'ol/geom'
import Style from 'ol/style/Style'
import Fill from 'ol/style/Fill'
import Stroke from 'ol/style/Stroke'
import CircleStyle from 'ol/style/Circle'
import { fromLonLat } from 'ol/proj'

const map = ref(null)

const dataSource = new VectorSource({
  wrapX: false
})

const lon = ref(16.3979471)
const lat = ref(39.9081726)
const alt = ref(500000)
const pitch = ref(0)
const proportion = ref(2)

const featureStyle = () => {
  return new Style({
    fill: new Fill({
      color: 'rgba(0,0,0,0.1)'
    }),
    stroke: new Stroke({
      width: 2,
      color: '#f00'
    }),
    image: new CircleStyle({
      radius: 3,
      fill: new Fill({
        color: '#0000ff'
      })
    })
  })
}

// 清空图层
const clearLayer = () => {
  dataSource.clear()
}

// 画圆
const showCircle = () => {
  const r = alt.value / proportion.value
  const center = fromLonLat([lon.value, lat.value])

  const circleFeature = new Feature({
    geometry: new Circle(center, r)
  })

  dataSource.addFeature(circleFeature)
}

// 初始化地图
const initMap = () => {
  const baseLayer = new TileLayer({
    source: new XYZ({
      url:
          'https://www.google.com/maps/vt?lyrs=m&gl=en&x={x}&y={y}&z={z}',
      crossOrigin: 'anonymous'
    })
  })

  const vectorLayer = new VectorLayer({
    source: dataSource,
    style: featureStyle()
  })

  map.value = new Map({
    target: 'vue-openlayers',
    layers: [baseLayer, vectorLayer],
    view: new View({
      projection: 'EPSG:3857',
      center: fromLonLat([16.5, 39.7]),
      zoom: 5
    })
  })
}

onMounted(() => {
  initMap()
})
</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 :deep(.el-input-group) {
  width: 200px;
  padding: 0 5px;
  margin-bottom: 10px;
}
</style>

⚠️ 六、这个方案的局限性(面试加分点🔥)

很多人写到这里就结束了,但其实这是不严谨的!

❌ 问题:

这个"圆形区域"并不真实,因为:

  1. 没有考虑相机视场角(FOV)

  2. 没有考虑俯仰角(pitch)

  3. 忽略了地球曲率

  4. 忽略了投影畸变

  5. 实际拍摄区域是矩形而不是圆


🧠 七、真实工程中应该怎么做?

如果你是做:

👉 无人机巡检系统 / 航测 / GIS系统

建议使用:

✅ 更真实模型:

1️⃣ 相机模型(针孔相机)

2️⃣ 关键参数:

  • 高度(alt)

  • 视场角(FOV)

  • 俯仰角(pitch)


📐 真实计算公式(核心思想)

地面宽度 ≈ 2 * alt * tan(FOV / 2)


👉 进一步:

  • pitch ≠ 0 → 区域会偏移

  • 最终是一个 倾斜矩形


🚀 八、可以怎么优化升级?

你这个 Demo 可以升级成项目级能力:

🔥 升级方向:

  • ✅ 圆 → 矩形(真实拍摄范围)

  • ✅ 加入 pitch(倾斜摄影)

  • ✅ 动态轨迹绘制(航线覆盖)

  • ✅ 多点连续覆盖分析

  • ✅ 热力图分析覆盖率

  • ✅ WebGL 加速大数据渲染


🧩 九、总结

本文实现了:

✔ Vue3 + OpenLayers 地图搭建

✔ 经纬度转投影坐标

✔ 基于高度推算拍摄范围

✔ 使用 Circle 绘制覆盖区域


👉 核心一句话总结:

利用"高度 / 比例"简化模型,在地图上快速模拟相机拍摄范围。


💬 十、适用场景

这个方案适用于:

  • 无人机巡检系统(快速预览)

  • 卫星可视化

  • GIS演示系统

  • 教学 Demo


🎁 十一、结语

如果你正在做:

👉 无人机 / WebGIS / 三维地图 / 数字孪生

这个思路可以作为一个基础能力模块

相关推荐
像我这样帅的人丶你还1 天前
别再让JS耽误你进步了。
css·vue.js
@yanyu6661 天前
07-引入element布局及spring boot完善后端
javascript·vue.js·spring boot
王霸天1 天前
💥别再抄网上的Scale缩放代码了!50行源码教你写一个永不翻车的大屏适配
前端·vue.js·数据可视化
悟空瞎说1 天前
深入 Vue3 响应式:为什么有的要加.value,有的不用?从设计到源码彻底讲透
前端·vue.js
Jacky-0081 天前
Vue3+elementPlus+Vite项目
vue
SuperEugene1 天前
前端通用基础组件设计:按钮/输入框/弹窗,统一设计标准|组件化设计基础篇
前端·javascript·vue.js·架构
我命由我123451 天前
在 React 项目中,可以执行 npm start 命令,但是,无法执行 npm build 命令
前端·javascript·vue.js·react.js·前端框架·json·ecmascript
没学上了1 天前
多相机协同拍照原理底层刨析
数码相机
aidou13141 天前
Vue3自定义实现日期选择器(可单选或多选)
前端·javascript·vue.js·日期选择器·transition
忆琳1 天前
Vue3 优雅解决单引号注入问题:自定义指令 + 全局插件双方案
vue.js·element