Radiosonde + sat track updates
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import {BASE} from '@/services/api.ts';
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import AircraftAltitude from '@/components/Altitude.vue'
|
||||
|
||||
const BASE_SPEED_UNITS = {
|
||||
knots: { label: 'KTS', convert: (v: number) => Math.round(v) },
|
||||
@@ -18,50 +19,24 @@ const BASE_VERTICAL_UNITS = {
|
||||
|
||||
const photoError = ref(false);
|
||||
const UNIT_KEY = 'at_units'
|
||||
|
||||
function loadUnits() {
|
||||
const s = localStorage.getItem(UNIT_KEY)
|
||||
return s ? JSON.parse(s) : { speed: 'knots', altitude: 'meters', vertical: 'mps' }
|
||||
}
|
||||
|
||||
const props = defineProps<{ plane: any; position: { x: number; y: number } }>()
|
||||
const props = defineProps<{ plane: any; position: { x: number; y: number }; history?: any[] }>()
|
||||
const emit = defineEmits<{ (e: 'close'): void; (e: 'bringToFront'): void }>()
|
||||
|
||||
const prefs = ref(loadUnits())
|
||||
|
||||
function saveAndSet(p: any) {
|
||||
prefs.value = { ...p }
|
||||
localStorage.setItem(UNIT_KEY, JSON.stringify(prefs.value))
|
||||
}
|
||||
function cycleSpeed() { const keys = Object.keys(BASE_SPEED_UNITS); const p = loadUnits(); p.speed = keys[(keys.indexOf(p.speed) + 1) % keys.length]; saveAndSet(p) }
|
||||
function cycleAltitude() { const keys = Object.keys(BASE_ALTITUDE_UNITS); const p = loadUnits(); p.altitude = keys[(keys.indexOf(p.altitude) + 1) % keys.length]; p.vertical = p.altitude === 'meters' ? 'mps' : 'fps'; saveAndSet(p) }
|
||||
function cycleVertical() { const keys = Object.keys(BASE_VERTICAL_UNITS); const p = loadUnits(); p.vertical = keys[(keys.indexOf(p.vertical) + 1) % keys.length]; p.altitude = p.vertical === 'mps' ? 'meters' : 'feet'; saveAndSet(p) }
|
||||
|
||||
function onUnitsChanged() {
|
||||
prefs.value = loadUnits()
|
||||
}
|
||||
|
||||
function cycleSpeed() {
|
||||
const keys = Object.keys(BASE_SPEED_UNITS)
|
||||
const p = loadUnits()
|
||||
p.speed = keys[(keys.indexOf(p.speed) + 1) % keys.length]
|
||||
saveAndSet(p)
|
||||
}
|
||||
|
||||
function cycleAltitude() {
|
||||
const keys = Object.keys(BASE_ALTITUDE_UNITS)
|
||||
const p = loadUnits()
|
||||
p.altitude = keys[(keys.indexOf(p.altitude) + 1) % keys.length]
|
||||
p.vertical = p.altitude === 'meters' ? 'mps' : 'fps'
|
||||
saveAndSet(p)
|
||||
}
|
||||
|
||||
function cycleVertical() {
|
||||
const keys = Object.keys(BASE_VERTICAL_UNITS)
|
||||
const p = loadUnits()
|
||||
p.vertical = keys[(keys.indexOf(p.vertical) + 1) % keys.length]
|
||||
p.altitude = p.vertical === 'mps' ? 'meters' : 'feet'
|
||||
saveAndSet(p)
|
||||
}
|
||||
|
||||
const speed = computed(() => BASE_SPEED_UNITS[prefs.value.speed as keyof typeof BASE_SPEED_UNITS])
|
||||
const speed = computed(() => BASE_SPEED_UNITS[prefs.value.speed as keyof typeof BASE_SPEED_UNITS])
|
||||
const altitude = computed(() => BASE_ALTITUDE_UNITS[prefs.value.altitude as keyof typeof BASE_ALTITUDE_UNITS])
|
||||
const vertical = computed(() => BASE_VERTICAL_UNITS[prefs.value.vertical as keyof typeof BASE_VERTICAL_UNITS])
|
||||
|
||||
@@ -70,13 +45,16 @@ const altitudeVal = computed(() => props.plane.alt_baro ?? props.plane.alt_geom
|
||||
const speedVal = computed(() => speed.value.convert(props.plane.speed || props.plane.gs || 0))
|
||||
const altVal = computed(() => props.plane.landed ? 'LANDED' : altitude.value.convert(altitudeVal.value))
|
||||
const altUnit = computed(() => props.plane.landed ? '' : altitude.value.label)
|
||||
const climbVal = computed(() => {
|
||||
const v = vertical.value.convert(props.plane.climb || props.plane.baro_rate || 0)
|
||||
return v >= 0 ? `+${v}` : String(v)
|
||||
})
|
||||
const climbVal = computed(() => { const v = vertical.value.convert(props.plane.climb || props.plane.baro_rate || 0); return v >= 0 ? `+${v}` : String(v) })
|
||||
const description = computed(() => props.plane.desc || [props.plane.manufacturer, props.plane.model].filter(Boolean).join(' ') || '')
|
||||
const operator = computed(() => props.plane.operator || props.plane.owner || '')
|
||||
|
||||
const altitudeHistory = computed(() => (props.history || [])
|
||||
.filter(p => !p.live && p.ts != null && p.altitude != null)
|
||||
.map(p => ({...p, altitude: altitude.value.convert(Number(p.altitude))})))
|
||||
|
||||
const currentAltitude = computed(() => props.plane.landed ? null : altitude.value.convert(Number(altitudeVal.value)))
|
||||
|
||||
const pos = ref({ ...props.position })
|
||||
const isDragging = ref(false)
|
||||
const dragStart = ref({ mx: 0, my: 0, ex: 0, ey: 0 })
|
||||
@@ -86,8 +64,8 @@ const header = ref<HTMLElement | null>(null)
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
if ((e.target as HTMLElement).classList.contains('ap-close')) return
|
||||
emit('bringToFront')
|
||||
isDragging.value = true
|
||||
dragStart.value = { mx: e.clientX, my: e.clientY, ex: pos.value.x, ey: pos.value.y }
|
||||
isDragging.value = true
|
||||
dragStart.value = { mx: e.clientX, my: e.clientY, ex: pos.value.x, ey: pos.value.y }
|
||||
e.preventDefault()
|
||||
}
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
@@ -123,19 +101,17 @@ function onTouchEnd() {
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('mousemove', onMouseMove)
|
||||
document.addEventListener('mouseup', onMouseUp)
|
||||
document.addEventListener('mouseup', onMouseUp)
|
||||
document.addEventListener('touchmove', onTouchMove, { passive: false })
|
||||
document.addEventListener('touchend', onTouchEnd)
|
||||
document.addEventListener('touchend', onTouchEnd)
|
||||
document.addEventListener('touchcancel', onTouchEnd)
|
||||
window.addEventListener('storage', onUnitsChanged)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousemove', onMouseMove)
|
||||
document.removeEventListener('mouseup', onMouseUp)
|
||||
document.removeEventListener('mouseup', onMouseUp)
|
||||
document.removeEventListener('touchmove', onTouchMove)
|
||||
document.removeEventListener('touchend', onTouchEnd)
|
||||
document.removeEventListener('touchend', onTouchEnd)
|
||||
document.removeEventListener('touchcancel', onTouchEnd)
|
||||
window.removeEventListener('storage', onUnitsChanged)
|
||||
})
|
||||
|
||||
const navball = computed(() => {
|
||||
@@ -150,7 +126,7 @@ const navball = computed(() => {
|
||||
const pitchLines: string[] = []
|
||||
for (let p = -30; p <= 30; p += 5) {
|
||||
if (p === 0) continue
|
||||
const isMajor = p % 10 === 0
|
||||
const isMajor = p % 10 === 0
|
||||
const [start, end] = [85, 115]
|
||||
if (isMajor) {
|
||||
pitchLines.push(`
|
||||
@@ -166,21 +142,21 @@ const navball = computed(() => {
|
||||
const rollTicks: string[] = []
|
||||
for (const angle of [-90, -60, -45, -30, -20, -10, 0, 10, 20, 30, 45, 60, 90]) {
|
||||
const rad = angle * Math.PI / 180
|
||||
const a = Math.abs(angle)
|
||||
const a = Math.abs(angle)
|
||||
if (a === 45) {
|
||||
rollTicks.push(`<circle cx="${100 + 78 * Math.sin(rad)}" cy="${100 - 78 * Math.cos(rad)}" r="2.5" fill="#fff"/>`)
|
||||
} else {
|
||||
const inner = (a === 0 || a % 30 === 0) ? 72 : 77
|
||||
const sw = (a === 0 || a % 30 === 0) ? 2.5 : 1.5
|
||||
const sw = (a === 0 || a % 30 === 0) ? 2.5 : 1.5
|
||||
rollTicks.push(`<line x1="${100 + inner * Math.sin(rad)}" y1="${100 - inner * Math.cos(rad)}" x2="${100 + 85 * Math.sin(rad)}" y2="${100 - 85 * Math.cos(rad)}" stroke="#fff" stroke-width="${sw}"/>`)
|
||||
}
|
||||
}
|
||||
|
||||
const compassLabels = [-60, -45, -30, -15, 0, 15, 30, 45, 60].map(offset => {
|
||||
const rad = offset * Math.PI / 180
|
||||
const x = 100 + 70 * Math.sin(rad)
|
||||
const y = 100 - 70 * Math.cos(rad)
|
||||
const h = Math.round((offset + 360) % 360)
|
||||
const x = 100 + 70 * Math.sin(rad)
|
||||
const y = 100 - 70 * Math.cos(rad)
|
||||
const h = Math.round((offset + 360) % 360)
|
||||
const lbl = ({ 0: 'N', 90: 'E', 180: 'S', 270: 'W' } as any)[h] || ''
|
||||
return lbl ? `<text x="${x}" y="${y + 4}" text-anchor="middle" fill="#0f0" font-size="13" font-weight="bold">${lbl}</text>` : ''
|
||||
}).join('')
|
||||
@@ -190,11 +166,11 @@ const navball = computed(() => {
|
||||
<defs>
|
||||
<clipPath id="navballClip"><circle cx="100" cy="100" r="85"/></clipPath>
|
||||
<linearGradient id="skyGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#1e90ff"/>
|
||||
<stop offset="0%" style="stop-color:#1e90ff"/>
|
||||
<stop offset="100%" style="stop-color:#4169e1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="groundGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#8b4513"/>
|
||||
<stop offset="0%" style="stop-color:#8b4513"/>
|
||||
<stop offset="100%" style="stop-color:#654321"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
@@ -208,7 +184,7 @@ const navball = computed(() => {
|
||||
</g>
|
||||
<g transform="translate(0,25)">${rollTicks.join('')}</g>
|
||||
<circle cx="100" cy="125" r="85" fill="none" stroke="#0f0" stroke-width="2.5"/>
|
||||
<line x1="40" y1="125" x2="80" y2="125" stroke="#ff0" stroke-width="3.5"/>
|
||||
<line x1="40" y1="125" x2="80" y2="125" stroke="#ff0" stroke-width="3.5"/>
|
||||
<line x1="120" y1="125" x2="160" y2="125" stroke="#ff0" stroke-width="3.5"/>
|
||||
<circle cx="100" cy="125" r="4" fill="none" stroke="#ff0" stroke-width="2.5"/>
|
||||
<g transform="rotate(${-heading} 100 125)">${compassLabels}</g>
|
||||
@@ -321,7 +297,6 @@ const navball = computed(() => {
|
||||
gap: 4px;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.ap-gauge:hover { border-color: #0ff; }
|
||||
.ap-gauge-val { font-size: 20px; font-weight: bold; color: #0f0; }
|
||||
.ap-gauge-unit { font-size: 11px; color: #0f0; margin-bottom: 2px; }
|
||||
@@ -330,6 +305,7 @@ const navball = computed(() => {
|
||||
|
||||
<template>
|
||||
<div class="aircraft-popup" :style="{ left: pos.x + 'px', top: pos.y + 'px' }">
|
||||
|
||||
<div ref="header" class="ap-header" @mousedown="onMouseDown" @touchstart.passive="onTouchStart">
|
||||
<h3 class="ap-callsign">
|
||||
✈️
|
||||
@@ -341,11 +317,11 @@ const navball = computed(() => {
|
||||
</div>
|
||||
|
||||
<div class="ap-meta flex-r justify-between">
|
||||
<div class="flex-c">
|
||||
<div class="flex-c flex-fill">
|
||||
<span>{{ operator || 'Unknown Owner' }}</span>
|
||||
<span>{{ plane.country || 'Unknown Country' }}</span>
|
||||
</div>
|
||||
<div class="flex-c align-x-end">
|
||||
<div class="flex-c flex-fill align-x-end">
|
||||
<span>{{ description || 'Unknown Aircraft' }}</span>
|
||||
<span style="text-transform: capitalize">{{ plane.class || 'Unknown' }} • {{ plane.type || 'Unknown' }}</span>
|
||||
</div>
|
||||
@@ -381,5 +357,12 @@ const navball = computed(() => {
|
||||
</div>
|
||||
<div class="ap-navball" v-html="navball" />
|
||||
</div>
|
||||
|
||||
<AircraftAltitude
|
||||
:history="altitudeHistory"
|
||||
:current-altitude="currentAltitude"
|
||||
:unit="altitude.label"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user