Sats
This commit is contained in:
162
client/src/services/sats.ts
Normal file
162
client/src/services/sats.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { BASE } from '@/services/api.ts'
|
||||
import { createApp, ref } from 'vue'
|
||||
import Map from 'ol/Map'
|
||||
import { Feature } from 'ol'
|
||||
import { LineString, Point } from 'ol/geom'
|
||||
import { fromLonLat } from 'ol/proj'
|
||||
import { Style, Stroke, Fill, Circle as CircleStyle, Text } from 'ol/style'
|
||||
import { Vector as VectorLayer } from 'ol/layer'
|
||||
import { Vector as VectorSource } from 'ol/source'
|
||||
import { adjustedInterval } from '@ztimson/utils'
|
||||
import TinyGSPopup from '@/components/Satellite.vue'
|
||||
import { bringToFront } from './zindex'
|
||||
|
||||
const API = BASE + '/api'
|
||||
|
||||
interface TinyGSReading {
|
||||
timestamp: number
|
||||
freqMHz: number
|
||||
satellite: string
|
||||
latitude: number
|
||||
longitude: number
|
||||
az: number
|
||||
el: number
|
||||
packetRssi: number
|
||||
packetSnr: number
|
||||
freqError: number
|
||||
crcOk: boolean
|
||||
}
|
||||
|
||||
interface TinyGSSatellite extends TinyGSReading {
|
||||
history: TinyGSReading[]
|
||||
}
|
||||
|
||||
export class TinyGSLayer {
|
||||
private map: Map
|
||||
private layer!: VectorLayer<VectorSource>
|
||||
private popups: any = {}
|
||||
private data: TinyGSSatellite[] = []
|
||||
private clickHandler: ((e: any) => void) | null = null
|
||||
private refreshTimer: any = null
|
||||
visible = false
|
||||
|
||||
constructor(map: Map) { this.map = map }
|
||||
|
||||
async show() {
|
||||
if (this.visible) return
|
||||
this.visible = true
|
||||
this.layer = new VectorLayer({ source: new VectorSource(), zIndex: 105 })
|
||||
this.map.addLayer(this.layer)
|
||||
await this._fetch()
|
||||
this._draw()
|
||||
this._attachClick()
|
||||
this.refreshTimer = adjustedInterval(async () => {
|
||||
try {
|
||||
await this._fetch()
|
||||
this._draw()
|
||||
this._refreshPopups()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}, 5_000)
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (!this.visible) return
|
||||
this.visible = false
|
||||
if (this.refreshTimer) { clearInterval(this.refreshTimer); this.refreshTimer = null }
|
||||
if (this.clickHandler) { this.map.un('singleclick', this.clickHandler); this.clickHandler = null }
|
||||
for (const name of Object.keys(this.popups)) this._closePopup(name)
|
||||
this.map.removeLayer(this.layer)
|
||||
}
|
||||
|
||||
private async _fetch() {
|
||||
this.data = await fetch(`${API}/tinygs`).then(r => r.json()) || []
|
||||
}
|
||||
|
||||
private _draw() {
|
||||
const source = this.layer.getSource()!
|
||||
source.clear()
|
||||
|
||||
for (const sat of this.data) {
|
||||
if (sat.latitude == null || sat.longitude == null) continue
|
||||
|
||||
const points = <any>[...sat.history, sat].map(r => fromLonLat([r.longitude, r.latitude]))
|
||||
if (points.length > 1) {
|
||||
const trail = new Feature({ geometry: new LineString(points) })
|
||||
trail.setStyle(new Style({ stroke: new Stroke({ color: 'rgba(212,164,255,0.5)', width: 2, lineDash: [6, 4] }) }))
|
||||
source.addFeature(trail)
|
||||
}
|
||||
|
||||
const marker = new Feature({ geometry: new Point(points[points.length - 1]) })
|
||||
marker.set('satellite', sat.satellite)
|
||||
marker.set('satData', sat)
|
||||
marker.setStyle(new Style({
|
||||
image: new CircleStyle({
|
||||
radius: 6,
|
||||
fill: new Fill({ color: sat.crcOk ? '#3fdb6d' : '#e0475a' }),
|
||||
stroke: new Stroke({ color: '#000', width: 1.5 }),
|
||||
}),
|
||||
text: new Text({
|
||||
text: sat.satellite,
|
||||
font: '12px sans-serif',
|
||||
fill: new Fill({ color: '#d4a4ff' }),
|
||||
stroke: new Stroke({ color: '#000000', width: 2 }),
|
||||
offsetY: -14,
|
||||
}),
|
||||
}))
|
||||
source.addFeature(marker)
|
||||
}
|
||||
}
|
||||
|
||||
private _calcPopupPos(sat: TinyGSSatellite): { x: number; y: number } {
|
||||
const pixel: any = this.map.getPixelFromCoordinate(fromLonLat([sat.longitude, sat.latitude]))
|
||||
if (!pixel) return { x: 10, y: 60 }
|
||||
const rect = (this.map.getTargetElement() as HTMLElement).getBoundingClientRect()
|
||||
return { x: rect.left + pixel[0] + 16, y: rect.top + pixel[1] - 16 }
|
||||
}
|
||||
|
||||
private _openPopup(sat: TinyGSSatellite) {
|
||||
if (this.popups[sat.satellite]) return
|
||||
|
||||
const satRef = ref(sat)
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
|
||||
const mobile = window.innerWidth <= 768
|
||||
|
||||
const app = createApp(TinyGSPopup, {
|
||||
satellite: satRef.value,
|
||||
position: mobile ? { x: window.innerWidth / 2 - 180, y: window.innerHeight - 540 - 16 } : { x: window.innerWidth - 360 - 16, y: 16 },
|
||||
onClose: () => this._closePopup(sat.satellite),
|
||||
onBringToFront: () => bringToFront(container),
|
||||
})
|
||||
|
||||
app.mount(container)
|
||||
this.popups[sat.satellite] = { satRef, unmount: () => { app.unmount(); container.remove() } }
|
||||
}
|
||||
|
||||
private _closePopup(name: string) {
|
||||
const popup = this.popups[name]
|
||||
if (popup) { popup.unmount(); delete this.popups[name] }
|
||||
}
|
||||
|
||||
private _refreshPopups() {
|
||||
for (const name of Object.keys(this.popups)) {
|
||||
const sat = this.data.find(s => s.satellite === name)
|
||||
if (!sat) { this._closePopup(name); continue }
|
||||
this.popups[name].satRef.value = sat
|
||||
}
|
||||
}
|
||||
|
||||
private _attachClick() {
|
||||
this.clickHandler = (evt) => {
|
||||
const f = this.map.forEachFeatureAtPixel(evt.pixel, f => f.get('satData') ? f : null)
|
||||
if (!f) return
|
||||
const sat = f.get('satData') as TinyGSSatellite
|
||||
if (this.popups[sat.satellite]) { this._closePopup(sat.satellite); return }
|
||||
this._openPopup(sat)
|
||||
}
|
||||
this.map.on('singleclick', this.clickHandler)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user