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 / 三维地图 / 数字孪生

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

相关推荐
踩着两条虫9 小时前
VTJ.PRO 核心架构全公开!从设计稿到代码,揭秘AI智能体如何“听懂人话”
前端·vue.js·ai编程
蓝冰凌11 小时前
Vue 3 中 defineExpose 的行为【defineExpose暴露ref变量】详解:自动解包、响应性与实际使用
前端·javascript·vue.js
奔跑的呱呱牛11 小时前
generate-route-vue基于文件系统的 Vue Router 动态路由生成工具
前端·javascript·vue.js
sp42a11 小时前
在 NativeScript-Vue 中实现流畅的共享元素转场动画
vue.js·nativescript·app 开发
小彭努力中12 小时前
192.Vue3 + OpenLayers 实战:点击地图 Feature,列表自动滚动定位
vue·webgl·openlayers·geojson·webgis
还是大剑师兰特13 小时前
Vue3 中 computed(计算属性)完整使用指南
前端·javascript·vue.js
孜孜不倦不忘初心13 小时前
Ant Design Vue 表格组件空数据统一处理 踩坑
前端·vue.js·ant design
csdn_aspnet13 小时前
查看 vite 与 vue 版本
javascript·vue.js