主要版本

This commit is contained in:
2024-02-22 11:22:19 +08:00
commit e23a4bc500
33 changed files with 5321 additions and 0 deletions

60
src/render/App.vue Normal file
View File

@@ -0,0 +1,60 @@
<template>
<div class="w-h-full fixed">
<EarthView ref="earthViewInstance" />
</div>
<div class="w-h-full fixed grid grid-cols-2 p-10 front-container">
<div class="text-left">
<SNRView ref="snrViewInstance" />
</div>
<div class="text-center w-1/2 ml-auto">
<PlanisphereView ref="planisphereViewInstance" />
</div>
<div class="text-left">
<NMEAView ref="nmeaViewInstance" />
</div>
<div class="text-center w-1/2 ml-auto">
<BaseInfoView ref="baseInfoViewInstance" />
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import SNRView from './components/SNRView.vue'
import NMEAView from './components/NMEAView.vue'
import EarthView from './components/EarthView.vue'
import BaseInfoView from './components/BaseInfoView.vue'
import PlanisphereView from './components/PlanisphereView.vue'
const nmeaViewInstance = ref(null)
const snrViewInstance = ref(null)
const earthViewInstance = ref(null)
const baseInfoViewInstance = ref(null)
const planisphereViewInstance = ref(null)
onMounted(() => {
if ('ipcRenderer' in window) {
ipcRenderer.receive('NMEA_RECEIVED', nmeaViewInstance.value.update)
ipcRenderer.receive('NMEA_HANDLED' , (nmeaStr: string) => {
const nmea = JSON.parse(nmeaStr)
snrViewInstance.value.update(nmea)
earthViewInstance.value.update(nmea)
baseInfoViewInstance.value.update(nmea)
planisphereViewInstance.value.update(nmea)
})
ipcRenderer.send('APP_MOUNTED')
}
})
</script>
<style scoped>
.front-container {
grid-template-rows: 55vh;
background: linear-gradient(to right, rgba(17, 24, 39, 0.8) , transparent, rgba(17, 24, 39, 0.8));
}
</style>

113
src/render/api/util.js Normal file
View File

@@ -0,0 +1,113 @@
import { Helpers } from 'nmea'
const getPosition = (nmea) => {
if (!nmea) return
const { RMC } = nmea
const position = { lng: Helpers.parseLongitude(RMC.lon, RMC.lonPole), lat: Helpers.parseLatitude(RMC.lat, RMC.latPole) }
return position
}
const getBaseInfo = (nmea) => {
if (!nmea) return
const { RMC, GGA } = nmea
try {
const longitude = (!RMC.lon || !RMC.lonPole) ? '' : Helpers.parseLongitude(RMC.lon, RMC.lonPole)
const latitude = (!RMC.lat || !RMC.latPole) ? '' : Helpers.parseLatitude(RMC.lat, RMC.latPole)
const altitude = GGA.alt + GGA.altUnit
const date = (!RMC.date) ? '' : '20' + RMC.date.slice(4, 6) + '-' + RMC.date.slice(2, 4) + '-' + RMC.date.slice(0, 2)
let UTCTime = '', BJTime = ''
if (RMC.timestamp) {
const hour = parseInt(RMC.timestamp.slice(0, 2))
const bjHour = (hour + 8) % 24
UTCTime = hour.toString().padStart(2, '0') + ':' + RMC.timestamp.slice(2, 4) + ':' + RMC.timestamp.slice(4, 6)
BJTime = bjHour.toString().padStart(2, '0') + ':' + RMC.timestamp.slice(2, 4) + ':' + RMC.timestamp.slice(4, 6)
}
const baseInfo = { longitude, latitude, altitude, date, UTCTime, BJTime }
return baseInfo
} catch (error) {
console.error(error);
}
}
const getSNROption = (nmea) => {
if (!nmea) return
const { GSV, GSA } = nmea
const GSVArr = GSV.filter((gsv) => gsv.talker_id === 'BD')
const GSAArr = GSA.filter((gsa) => gsa.talker_id === 'BD')
try {
let yAxis_data = [], series_data = []
const in_positioning_id = get_in_positioning_id(GSAArr)
GSVArr.forEach(({ satellites }) => {
satellites.forEach(({ id, SNRdB }) => {
yAxis_data.push(id)
if (in_positioning_id.indexOf(parseInt(id)) < 0) {
series_data.push([SNRdB, id, false])
} else {
series_data.push([SNRdB, id, true])
}
});
});
const SNROption = {
yAxis: {
data: yAxis_data
},
series: [{
data: series_data
}]
}
return SNROption
} catch (error) {
console.error(error);
}
}
const getPlanisphereOption = (nmea) => {
if (!nmea) return
const { GSV, GSA } = nmea
const GSVArr = GSV.filter((gsv) => gsv.talker_id === 'BD')
const GSAArr = GSA.filter((gsa) => gsa.talker_id === 'BD')
try {
let data = []
const in_positioning_id = get_in_positioning_id(GSAArr)
GSVArr.forEach(({ satellites }) => {
satellites.forEach(({ id, elevationDeg, azimuthTrue }) => {
if (in_positioning_id.indexOf(parseInt(id)) < 0) {
data.push([parseInt(elevationDeg), azimuthTrue, id, false])
} else {
data.push([parseInt(elevationDeg), azimuthTrue, id, true])
}
});
});
const planisphereOption = {
series: [{ data }]
}
return planisphereOption
} catch (error) {
console.error(error);
}
}
const get_in_positioning_id = (GSAArr) => {
let in_positioning_id = []
GSAArr.forEach(({ satellites }) => {
in_positioning_id.push(...satellites)
});
return in_positioning_id
}
export { getPosition, getBaseInfo, getSNROption, getPlanisphereOption }

BIN
src/render/assets/earth.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
src/render/assets/pin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,91 @@
<template>
<div>
<SubTitleView :title="'基础信息'" />
<div class="text-center text-2xl leading-loose text-blue-100">
<div>
<div class="label">经  度:</div>
<div class="content">{{ baseInfo.longitude ? baseInfo.longitude : '' }}</div>
</div>
<div>
<div class="label">纬  度:</div>
<div class="content">{{ baseInfo.latitude ? baseInfo.latitude : '' }}</div>
</div>
<div>
<div class="label">海  拔:</div>
<div class="content">{{ baseInfo.altitude }}</div>
</div>
<div>
<div class="label">日  期:</div>
<div class="content">{{ baseInfo.date }}</div>
</div>
<div>
<div class="label">UTC时间:</div>
<div class="content">{{ baseInfo.UTCTime }}</div>
</div>
<div>
<div class="label">北京时间:</div>
<div class="content">{{ baseInfo.BJTime }}</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import SubTitleView from './SubTitleView.vue'
import { getBaseInfo } from '../api/util'
let baseInfo = reactive({
longitude : '',
latitude : '',
altitude : '',
date : '',
UTCTime : '',
BJTime : '',
})
let longitudeD = ref()
let longitudeM = ref()
let longitudeS = ref()
let latitudeD = ref()
let latitudeM = ref()
let latitudeS = ref()
const update = (nmea) => {
const newBaseInfo = getBaseInfo(nmea)
if (!newBaseInfo) return
Object.assign(baseInfo, newBaseInfo)
longitudeD.value = baseInfo.longitude.substring(0,baseInfo.longitude.indexOf("."))+'°'
longitudeM.value = String((Number('0.'+baseInfo.longitude.substring(baseInfo.longitude.indexOf(".")+1,baseInfo.longitude.length))*60).toFixed(10)).substring(0,String((Number('0.'+baseInfo.longitude.substring(baseInfo.longitude.indexOf(".")+1,baseInfo.longitude.length))*60).toFixed(10)).indexOf("."))+'′'
longitudeS.value = (Number('0.'+(Number('0.'+baseInfo.longitude.substring(baseInfo.longitude.indexOf(".")+1,baseInfo.longitude.length))*60).toFixed(10).substring(String((Number('0.'+baseInfo.longitude.substring(baseInfo.longitude.indexOf(".")+1,baseInfo.longitude.length))*60).toFixed(10)).indexOf(".")+1,((Number('0.'+baseInfo.longitude.substring(baseInfo.longitude.indexOf(".")+1,baseInfo.longitude.length))*60).toFixed(10)).length))*60).toFixed(6)+'″'
latitudeD.value = baseInfo.latitude.substring(0,baseInfo.latitude.indexOf("."))+'°'
latitudeM.value = String((Number('0.'+baseInfo.latitude.substring(baseInfo.latitude.indexOf(".")+1,baseInfo.latitude.length))*60).toFixed(10)).substring(0,String((Number('0.'+baseInfo.latitude.substring(baseInfo.latitude.indexOf(".")+1,baseInfo.latitude.length))*60).toFixed(10)).indexOf("."))+'′'
latitudeS.value = (Number('0.'+(Number('0.'+baseInfo.latitude.substring(baseInfo.latitude.indexOf(".")+1,baseInfo.latitude.length))*60).toFixed(10).substring(String((Number('0.'+baseInfo.latitude.substring(baseInfo.latitude.indexOf(".")+1,baseInfo.latitude.length))*60).toFixed(10)).indexOf(".")+1,((Number('0.'+baseInfo.latitude.substring(baseInfo.latitude.indexOf(".")+1,baseInfo.latitude.length))*60).toFixed(10)).length))*60).toFixed(6)+'″'
}
defineExpose({ update })
</script>
<style scoped>
@layer components {
.label {
@apply float-left text-right w-2/5;
min-width: 120px;
height: 36px;
}
}
@layer components {
.content {
@apply float-left text-left w-3/5;
min-width: 165px;
height: 36px;
/* min-height: 48px; */
}
}
</style>

View File

@@ -0,0 +1,58 @@
<template>
<vc-viewer :shouldAnimate="true" :showCredit="false" :infoBox="false" @ready="onViewerReady">
<vc-layer-imagery>
<vc-provider-imagery-singletile :url="earth" />
</vc-layer-imagery>
<vc-entity :show="show" :position="position">
<vc-graphics-billboard :image="pin" :verticalOrigin="1" />
</vc-entity>
</vc-viewer>
</template>
<script lang="ts" setup>
import { reactive, computed } from 'vue'
import { getPosition } from '../api/util'
import pin from '../assets/pin.png'
import earth from '../assets/earth.jpg'
const position = reactive({ lng: NaN, lat: NaN })
const show = computed(() => {
const { lng, lat } = position
if (isNaN(lng) || lng > 180 || lng < -180) return false
if (isNaN(lat) || lat > 90 || lat < -90 ) return false
return true
})
// 设置地球自转动画效果
const onViewerReady = ({ Cesium, viewer }) => {
const { JulianDate, Cartesian3 } = Cesium
const { clock, scene } = viewer
let prev = clock.currentTime
clock.onTick.addEventListener(() => {
const current = clock.currentTime
const interval = JulianDate.toDate(current) - JulianDate.toDate(prev)
prev = current
scene.camera.rotate(
Cartesian3.UNIT_Z,
(Math.PI / (24 * 60 * 60)) * interval * 1,
);
})
}
const update = (nmea) => {
const newPosition = getPosition(nmea)
if (!newPosition) return
Object.assign(position, newPosition)
}
defineExpose({ update })
</script>

View File

@@ -0,0 +1,20 @@
<template>
<div>
<SubTitleView :title="'NMEA'" />
<div ref="nmeaContainer" class="whitespace-pre-line text-blue-100 text-sm">
{{ nmeaStr }}
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import SubTitleView from './SubTitleView.vue'
const nmeaStr = ref('')
const update = (value) => nmeaStr.value = value
defineExpose({ update })
</script>

View File

@@ -0,0 +1,86 @@
<template>
<SubTitleView :title="'星位视图'" />
<div @dblclick="close" class="h-4/5">
<v-chart :option="option" />
</div>
</template>
<script lang="ts" setup>
import { use } from "echarts/core";
import { CanvasRenderer } from "echarts/renderers";
import { ScatterChart } from "echarts/charts";
import { PolarComponent } from "echarts/components";
import VChart from "vue-echarts";
import { reactive } from "vue";
import SubTitleView from './SubTitleView.vue'
import { getPlanisphereOption } from '../api/util'
use([
CanvasRenderer,
ScatterChart,
PolarComponent
]);
const LOCATED_COLOR = '#e5323e'
const UNLOCATED_COLOR = '#9ca3af'
const option = reactive({
polar: {},
radiusAxis: {
inverse: true,
min: 0,
max: 90,
axisLabel: {
rotate: -25,
showMinLabel: false,
showMaxLabel: false,
verticalAlign: 'bottom'
}
},
angleAxis: {
type: 'value',
min: 0,
max: 360,
axisTick: {show: false},
axisLabel: {
formatter: function (value) {
switch (value)
{
case 0 : return 'N';
case 90 : return 'E';
case 180 : return 'S';
case 270 : return 'W';
}
},
}
},
series: [{
coordinateSystem: 'polar',
type: 'scatter',
symbolSize: 32,
label: {
show: true,
formatter: '{@[2]}'
},
itemStyle: {
color: ({ value }) => {
return value[3] ? LOCATED_COLOR : UNLOCATED_COLOR
}
}
}]
})
const update = (nmea) => {
const newOption = getPlanisphereOption(nmea)
if (!newOption) return
Object.assign(option, newOption)
}
const close = () => {
window.ipcRenderer.send('CLOSE')
}
defineExpose({ update })
</script>

View File

@@ -0,0 +1,78 @@
<template>
<SubTitleView :title="'载噪比'" />
<div class="w-2/3 h-4/5">
<v-chart :option="option" />
</div>
</template>
<script lang="ts" setup>
import { use, graphic } from "echarts/core";
import { CanvasRenderer } from "echarts/renderers";
import { BarChart } from "echarts/charts";
import VChart from "vue-echarts";
import { reactive } from "vue";
import SubTitleView from './SubTitleView.vue'
import { getSNROption } from '../api/util'
use([
CanvasRenderer,
BarChart
]);
const LOCATED_COLOR = new graphic.LinearGradient(
0, 0, 1, 0,
[
{offset: 0.3, color: '#83bff6'},
{offset: 1, color: '#188df0'}
]
)
const UNLOCATED_COLOR = '#9ca3af'
const option = reactive({
grid: {
top: 0,
bottom: 0,
left: 0,
right: 0,
containLabel: true
},
xAxis: {
show: false,
type: 'value',
min: 0,
max: 60,
},
yAxis: {
type: 'category',
inverse: true,
axisLine: {show: false},
axisTick: {show: false},
axisLabel: {color: 'white'}
},
series: [{
type: 'bar',
label: {
show: true,
position: 'insideRight',
formatter: function ({ value }) {
return value[0] <= 5 ? '' : value[0];
},
},
itemStyle: {
color: ({ value }) => {
return value[2] ? LOCATED_COLOR : UNLOCATED_COLOR
}
},
barMaxWidth: 24
}]
})
const update = (nmea) => {
const newOption = getSNROption(nmea)
if (!newOption) return
Object.assign(option, newOption)
}
defineExpose({ update })
</script>

View File

@@ -0,0 +1,9 @@
<template>
<h1 class="text-4xl text-yellow-50 mb-5"> {{ title }} </h1>
</template>
<script lang="ts" setup>
defineProps({
title: String
})
</script>

9
src/render/main.css Normal file
View File

@@ -0,0 +1,9 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.w-h-full {
@apply w-full h-full m-0 p-0;
}
}

15
src/render/main.ts Normal file
View File

@@ -0,0 +1,15 @@
import { createApp } from 'vue'
import App from './App.vue'
import 'vue-cesium/dist/index.css'
import { VcViewer, VcEntity, VcLayerImagery, VcProviderImagerySingletile, VcGraphicsBillboard } from 'vue-cesium'
import './main.css'
const cesiumPath = (process.env.NODE_ENV === 'development' ? './node_modules/cesium/Build/Cesium/Cesium.js': './Cesium/Cesium.js')
const app = createApp(App)
// 局部引入VueCesium
app.use(VcViewer).use(VcEntity).use(VcLayerImagery).use(VcProviderImagerySingletile).use(VcGraphicsBillboard)
app.config.globalProperties.$VueCesium = { cesiumPath }
app.mount('#app')

5
src/render/shims-vue.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}