Sat updates
This commit is contained in:
@@ -3,6 +3,7 @@ import { AirTrafficLayer } from '@/services/adsb.ts'
|
||||
import { AISLayer } from '@/services/ais.ts'
|
||||
import { api } from '@/services/api.ts'
|
||||
import { RangeLayer } from '@/services/range.ts'
|
||||
import {SatsLayer} from '@/services/sats.ts';
|
||||
import VectorTileLayer from 'ol/layer/VectorTile'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import Map from 'ol/Map'
|
||||
@@ -24,7 +25,6 @@ const current = ref({ latitude: 0, longitude: 0 })
|
||||
const props = defineProps<{ dark: boolean }>()
|
||||
const mapEl = ref<HTMLDivElement>()
|
||||
const showOverlays = ref(false)
|
||||
const atActive = ref(true)
|
||||
|
||||
const NM = 1852
|
||||
|
||||
@@ -38,7 +38,7 @@ const OVERLAYS = [
|
||||
{ id: 'wind', label: 'Wind', icon: '💨' },
|
||||
]
|
||||
|
||||
const activeOverlays = ref<Set<string>>(new Set(['traffic', 'rain']))
|
||||
const activeOverlays = ref<Set<string>>(new Set(['traffic', 'sats', 'rain']))
|
||||
const overlayLayers: { [key: string]: any } = {}
|
||||
|
||||
let map: Map
|
||||
@@ -47,6 +47,7 @@ let radarInterval: ReturnType<typeof setInterval>
|
||||
let airTraffic: AirTrafficLayer
|
||||
let aisLayer: AISLayer
|
||||
let range: RangeLayer
|
||||
let sats: SatsLayer
|
||||
|
||||
function buildWindSrc(lat: number, lon: number, zoom: number) {
|
||||
return `https://embed.windy.com/embed2.html?lat=${lat.toFixed(4)}&lon=${lon.toFixed(4)}&detailLat=${lat.toFixed(4)}&detailLon=${lon.toFixed(4)}&zoom=${Math.round(zoom)}&level=surface&overlay=wind&product=ecmwf&menu=&message=&marker=&calendar=now&pressure=&type=map&location=coordinates&detail=&metricWind=kt&metricTemp=%C2%B0C&radarRange=-1`
|
||||
@@ -123,9 +124,15 @@ function toggleOverlay(id: string) {
|
||||
airTraffic.show()
|
||||
range.show()
|
||||
}
|
||||
}
|
||||
|
||||
if (id === 'wind') {
|
||||
} else if (id === 'sats') {
|
||||
if (activeOverlays.value.has('sats')) {
|
||||
activeOverlays.value.delete('sats')
|
||||
sats.hide()
|
||||
} else {
|
||||
activeOverlays.value.add('sats')
|
||||
sats.show()
|
||||
}
|
||||
} else if (id === 'wind') {
|
||||
if (activeOverlays.value.has('wind')) {
|
||||
activeOverlays.value.delete('wind')
|
||||
hideWindOverlay()
|
||||
@@ -133,10 +140,7 @@ function toggleOverlay(id: string) {
|
||||
activeOverlays.value.add('wind')
|
||||
showWindOverlay()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (id === 'rain') {
|
||||
} else if (id === 'rain') {
|
||||
if (activeOverlays.value.has('rain')) {
|
||||
activeOverlays.value.delete('rain')
|
||||
if (overlayLayers['rain']) { map.removeLayer(overlayLayers['rain']); delete overlayLayers['rain'] }
|
||||
@@ -144,7 +148,6 @@ function toggleOverlay(id: string) {
|
||||
activeOverlays.value.add('rain')
|
||||
refreshRain()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,6 +205,8 @@ onMounted(async () => {
|
||||
aisLayer.show()
|
||||
airTraffic = new AirTrafficLayer(map)
|
||||
airTraffic.show()
|
||||
sats = new SatsLayer(map)
|
||||
sats.show();
|
||||
range = new RangeLayer(map)
|
||||
range.show()
|
||||
|
||||
|
||||
@@ -1,48 +1,228 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
history: any[]
|
||||
range: { altitude: number; slantRange: number; horizonRadius: number; groundDist: number } | null
|
||||
eta: number | null
|
||||
position: { x: number; y: number }
|
||||
}>()
|
||||
const emit = defineEmits<{ (e: 'close'): void; (e: 'bringToFront'): void }>()
|
||||
|
||||
const emit = defineEmits(['bringToFront']);
|
||||
const props = defineProps<{ history: any[]; position: { x: number, y: number }; onClose: () => void }>()
|
||||
const latest = computed(() => props.history[props.history.length - 1])
|
||||
const rangeVal = computed(() => props.range ? Math.round(props.range.slantRange) : null)
|
||||
const altVal = computed(() => props.range ? Math.round(props.range.altitude) : null)
|
||||
|
||||
// ── Drag ──────────────────────────────────────────────────────────────────────
|
||||
const pos = ref({ ...props.position })
|
||||
const isDragging = ref(false)
|
||||
const dragStart = ref({ mx: 0, my: 0, ex: 0, ey: 0 })
|
||||
const longPressTimer = ref<number | null>(null)
|
||||
const header = ref<HTMLElement | null>(null)
|
||||
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
if ((e.target as HTMLElement).classList.contains('tp-close')) return
|
||||
emit('bringToFront')
|
||||
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) {
|
||||
if (!isDragging.value) return
|
||||
pos.value.x = dragStart.value.ex + (e.clientX - dragStart.value.mx)
|
||||
pos.value.y = dragStart.value.ey + (e.clientY - dragStart.value.my)
|
||||
}
|
||||
function onMouseUp() { isDragging.value = false }
|
||||
|
||||
function onTouchStart(e: TouchEvent) {
|
||||
if ((e.target as HTMLElement).classList.contains('tp-close')) return
|
||||
const touch: any = e.touches[0]
|
||||
const sx = touch.clientX, sy = touch.clientY
|
||||
longPressTimer.value = window.setTimeout(() => {
|
||||
emit('bringToFront')
|
||||
isDragging.value = true
|
||||
dragStart.value = { mx: sx, my: sy, ex: pos.value.x, ey: pos.value.y }
|
||||
if (header.value) header.value.style.opacity = '0.8'
|
||||
}, 500)
|
||||
}
|
||||
function onTouchMove(e: TouchEvent) {
|
||||
if (!isDragging.value) return
|
||||
const touch: any = e.touches[0]
|
||||
pos.value.x = dragStart.value.ex + (touch.clientX - dragStart.value.mx)
|
||||
pos.value.y = dragStart.value.ey + (touch.clientY - dragStart.value.my)
|
||||
e.preventDefault()
|
||||
}
|
||||
function onTouchEnd() {
|
||||
if (longPressTimer.value) { clearTimeout(longPressTimer.value); longPressTimer.value = null }
|
||||
if (isDragging.value && header.value) header.value.style.opacity = '1'
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('mousemove', onMouseMove)
|
||||
document.addEventListener('mouseup', onMouseUp)
|
||||
document.addEventListener('touchmove', onTouchMove, { passive: false })
|
||||
document.addEventListener('touchend', onTouchEnd)
|
||||
document.addEventListener('touchcancel', onTouchEnd)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousemove', onMouseMove)
|
||||
document.removeEventListener('mouseup', onMouseUp)
|
||||
document.removeEventListener('touchmove', onTouchMove)
|
||||
document.removeEventListener('touchend', onTouchEnd)
|
||||
document.removeEventListener('touchcancel', onTouchEnd)
|
||||
})
|
||||
|
||||
// ── Radar HUD ─────────────────────────────────────────────────────────────────
|
||||
/** Center = zenith (el 90), edge = horizon (el 0). */
|
||||
function polarPoint(az: number, el: number, radius = 100) {
|
||||
const r = radius * (1 - el / 90)
|
||||
const rad = (az - 90) * Math.PI / 180
|
||||
return { x: 120 + r * Math.cos(rad), y: 120 + r * Math.sin(rad) }
|
||||
}
|
||||
|
||||
const trackPoints = computed(() => props.history.map(r => polarPoint(r.az, r.el)).map(p => `${p.x},${p.y}`).join(' '))
|
||||
const currentPoint = computed(() => latest.value ? polarPoint(latest.value.az, latest.value.el) : null)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tinygs-popup { position: fixed; width: 340px; background: var(--surface); border-radius: 12px; padding: 12px; box-shadow: 0 4px 20px rgba(0,0,0,.3); z-index: 1000; }
|
||||
.close { position: absolute; top: 8px; right: 8px; background: none; border: none; cursor: pointer; }
|
||||
.fields { width: 100%; font-size: 13px; margin-bottom: 8px; td { padding: 2px 4px; } }
|
||||
.ok { color: #3fdb6d; }
|
||||
.bad { color: #e0475a; }
|
||||
.radar { width: 100%; }
|
||||
.ring, .axis { fill: none; stroke: var(--border); stroke-width: 1; }
|
||||
<style scoped>
|
||||
.sat-popup {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
pointer-events: auto;
|
||||
background: rgba(0,0,0,0.88);
|
||||
border: 1px solid rgba(212,164,255,0.3);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.5);
|
||||
overflow: hidden;
|
||||
font-family: monospace;
|
||||
color: #ccc;
|
||||
min-width: 320px;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.tp-header {
|
||||
position: relative;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid rgba(212,164,255,0.2);
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
.tp-name { margin: 0 24px 0 0; color: #d4a4ff; font-size: 16px; }
|
||||
|
||||
.tp-close {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: rgba(255,255,255,0.2);
|
||||
border: none;
|
||||
color: white;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.tp-close:hover { background: rgba(255,255,255,0.35); }
|
||||
|
||||
.tp-meta {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid rgba(212,164,255,0.2);
|
||||
}
|
||||
|
||||
.tp-body { display: flex; gap: 16px; padding: 12px 16px; }
|
||||
|
||||
.tp-gauges { flex: 0 0 110px; }
|
||||
.tp-gauge-wrap { margin-bottom: 12px; }
|
||||
.tp-gauge-label { color: #888; font-size: 11px; font-weight: bold; margin-bottom: 3px; }
|
||||
.tp-gauge {
|
||||
padding: 6px;
|
||||
background: rgba(0,0,0,0.95);
|
||||
border: 2px solid #d4a4ff;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.tp-gauge-val { font-size: 20px; font-weight: bold; color: #d4a4ff; }
|
||||
.tp-gauge-unit { font-size: 11px; color: #d4a4ff; margin-bottom: 0.75em; }
|
||||
|
||||
.tp-radar { flex: 1; }
|
||||
.ring, .axis { fill: none; stroke: rgba(212,164,255,0.3); stroke-width: 1; }
|
||||
.dir { fill: #d4a4ff; font-size: 10px; font-weight: bold; }
|
||||
.track { fill: none; stroke: #d4a4ff; stroke-width: 1.5; }
|
||||
.current { fill: #d4a4ff; }
|
||||
|
||||
.tp-fields { width: 100%; font-size: 11px; padding: 0 16px 12px; border-collapse: collapse; }
|
||||
.tp-fields td { padding: 2px 4px; color: #888; }
|
||||
.tp-fields td:last-child { color: #ccc; text-align: right; }
|
||||
.ok { color: #3fdb6d !important; }
|
||||
.bad { color: #e0475a !important; }
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="tinygs-popup" :style="{ left: position.x + 'px', top: position.y + 'px' }" @mousedown="emit('bringToFront')">
|
||||
<button class="close" @click="onClose">✕</button>
|
||||
<h3>{{ latest?.satellite || 'Unknown' }}</h3>
|
||||
<table class="fields">
|
||||
<tr><td>Frequency</td><td>{{ latest?.freqMHz }} MHz</td></tr>
|
||||
<tr><td>Packet RSSI</td><td>{{ latest?.packetRssi }} dBm</td></tr>
|
||||
<tr><td>SNR</td><td>{{ latest?.packetSnr }} dB</td></tr>
|
||||
<tr><td>Freq error</td><td>{{ latest?.freqError }} Hz</td></tr>
|
||||
<tr><td>CRC</td><td :class="latest?.crcOk ? 'ok' : 'bad'">{{ latest?.crcOk ? 'OK' : 'ERROR' }}</td></tr>
|
||||
</table>
|
||||
<svg viewBox="0 0 240 240" class="radar">
|
||||
<circle cx="120" cy="120" r="100" class="ring" /><circle cx="120" cy="120" r="66" class="ring" /><circle cx="120" cy="120" r="33" class="ring" />
|
||||
<line x1="120" y1="20" x2="120" y2="220" class="axis" /><line x1="20" y1="120" x2="220" y2="120" class="axis" />
|
||||
<polyline :points="trackPoints" class="track" />
|
||||
<circle v-if="latest" v-bind="polarPoint(latest.az, latest.el)" r="4" class="current" />
|
||||
</svg>
|
||||
<div class="sat-popup" :style="{ left: pos.x + 'px', top: pos.y + 'px' }">
|
||||
|
||||
<div ref="header" class="tp-header" @mousedown="onMouseDown" @touchstart.passive="onTouchStart">
|
||||
<h3 class="tp-name">🛰️ {{ latest?.satellite || 'Unknown' }}</h3>
|
||||
<button class="tp-close" @click.stop="emit('close')">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="tp-meta">
|
||||
<div class="flex-c">
|
||||
<span>{{ latest?.freqMHz }} MHz</span>
|
||||
<span>Range: {{ rangeVal != null ? rangeVal + ' km' : 'Range —' }}</span>
|
||||
</div>
|
||||
<div class="flex-c align-x-end">
|
||||
<span>RSSI: {{ latest?.packetRssi }} dBm</span>
|
||||
<span>SNR: {{ latest?.packetSnr }} dB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tp-body">
|
||||
<div class="tp-gauges">
|
||||
<div class="tp-gauge-wrap">
|
||||
<div class="tp-gauge-label">Altitude (±10%)</div>
|
||||
<div class="tp-gauge">
|
||||
<span class="tp-gauge-val">{{ altVal != null ? altVal + ' km' : '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tp-gauge-wrap">
|
||||
<div class="tp-gauge-label">Azimuth</div>
|
||||
<div class="tp-gauge">
|
||||
<span class="tp-gauge-val">{{ latest?.az?.toFixed(2) ?? '—' }}</span>
|
||||
<span class="tp-gauge-unit">°</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tp-gauge-wrap">
|
||||
<div class="tp-gauge-label">Elevation</div>
|
||||
<div class="tp-gauge">
|
||||
<span class="tp-gauge-val">{{ latest?.el?.toFixed(2) ?? '—' }}</span>
|
||||
<span class="tp-gauge-unit">°</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svg viewBox="0 0 240 240" class="tp-radar">
|
||||
<circle cx="120" cy="120" r="100" class="ring" /><circle cx="120" cy="120" r="66" class="ring" /><circle cx="120" cy="120" r="33" class="ring" />
|
||||
<line x1="120" y1="20" x2="120" y2="220" class="axis" /><line x1="20" y1="120" x2="220" y2="120" class="axis" />
|
||||
<text x="120" y="14" text-anchor="middle" class="dir">N</text>
|
||||
<text x="228" y="124" text-anchor="middle" class="dir">E</text>
|
||||
<text x="120" y="234" text-anchor="middle" class="dir">S</text>
|
||||
<text x="12" y="124" text-anchor="middle" class="dir">W</text>
|
||||
<polyline :points="trackPoints" class="track" />
|
||||
<circle v-if="currentPoint" :cx="currentPoint.x" :cy="currentPoint.y" r="5" class="current" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user