Hooked up auroras

This commit is contained in:
2026-09-11 22:08:14 -04:00
parent fc306f277a
commit 6bcc5bec19
6 changed files with 132 additions and 16 deletions

View File

@@ -219,7 +219,7 @@ const navball = computed(() => {
<!-- Photo -->
<div v-if="!photoError" class="ap-photo-wrap">
<img class="ap-photo" :src="BASE + '/api/adsb-image/' + plane.icao" :alt="callsign" @error="photoError = true" />
<img class="ap-photo" :src="BASE + '/api/adsb/' + plane.icao + '/image'" :alt="callsign" @error="photoError = true" />
</div>
<!-- Body -->

View File

@@ -19,6 +19,7 @@ import { defaults as defaultInteractions } from 'ol/interaction'
import { fromLonLat, toLonLat } from 'ol/proj'
import { Style, Fill, Stroke, Circle as CircleStyle } from 'ol/style'
import { applyStyle } from 'ol-mapbox-style'
import { Aurora } from '@/services/aurora.ts'
import 'ol/ol.css'
const current = ref({ latitude: 0, longitude: 0 })
@@ -35,6 +36,7 @@ const OVERLAYS = [
{ id: 'traffic', label: 'Traffic', icon: '✈️' },
{ id: 'sats', label: 'Sats', icon: '🛰️' },
{ id: 'rain', label: 'Rain', icon: '🌧️' },
{ id: 'aurora', label: 'Aurora', icon: '🌌' },
{ id: 'wind', label: 'Wind', icon: '💨' },
]
@@ -48,6 +50,7 @@ let airTraffic: AirTrafficLayer
let aisLayer: AISLayer
let range: RangeLayer
let sats: SatsLayer
let aurora: Aurora
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`
@@ -124,6 +127,14 @@ function toggleOverlay(id: string) {
airTraffic.show()
range.show()
}
} else if (id === 'aurora') {
if (activeOverlays.value.has('aurora')) {
activeOverlays.value.delete('aurora')
aurora.hide()
} else {
activeOverlays.value.add('aurora')
aurora.show()
}
} else if (id === 'sats') {
if (activeOverlays.value.has('sats')) {
activeOverlays.value.delete('sats')
@@ -207,6 +218,7 @@ onMounted(async () => {
airTraffic.show()
sats = new SatsLayer(map)
sats.show();
aurora = new Aurora(map)
range = new RangeLayer(map)
range.show()

View File

@@ -213,7 +213,7 @@ const compass = computed(() => {
<!-- Photo -->
<div v-if="!photoError" class="sp-photo-wrap">
<img class="sp-photo" :src="BASE + '/api/ais-iamge/' + boat.mmsi" :alt="name" @error="photoError = true" />
<img class="sp-photo" :src="BASE + '/api/ais/' + boat.mmsi + '/image'" :alt="name" @error="photoError = true" />
</div>
<div class="sp-body">

View File

@@ -0,0 +1,104 @@
import 'ol/ol.css';
import {BASE} from '@/services/api.ts';
import {Feature} from 'ol';
import Point from 'ol/geom/Point';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import {Style} from 'ol/style';
export function normalize(coordinates: number | {lon: number} | {long: number} | {longitude: number}) {
const convert = (n) => {
if (n > 180) return n - 360;
if (n < -180) return n + 360;
return n;
}
if(typeof coordinates == 'number') return convert(coordinates);
if(typeof coordinates['lon'] == 'number') return convert(coordinates['lon']);
if(typeof coordinates['long'] == 'number') return convert(coordinates['long']);
if(typeof coordinates['longitude'] == 'number') return convert(coordinates['longitude']);
}
export class Aurora {
data: any = null;
layer;
map;
name = 'aurora';
refresh;
refreshRate = 60_000 * 60;
timestamp;
visible = false;
constructor(map) {
this.map = map;
}
private style(intensity: number) {
return new Style({renderer: (coordinates, state) => {
const ctx = state.context as CanvasRenderingContext2D;
const [x, y] = <[number, number]>coordinates;
const previousComposite = ctx.globalCompositeOperation;
ctx.globalCompositeOperation = 'screen';
const zoom = this.map.getView().getZoom() || 1;
const baseRadius = 50;
const radius = baseRadius * Math.pow(2, zoom - 3);
const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius);
const alpha = intensity / 50;
gradient.addColorStop(0, `rgba(0, 255, 0, ${alpha})`);
gradient.addColorStop(1, 'rgba(0, 255, 0, 0)');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI);
ctx.fill();
ctx.globalCompositeOperation = previousComposite;
}});
};
async update() {
const d = await fetch(BASE + '/api/aurora').then(resp => resp.json());
this.timestamp = new Date(d.timestamp).getTime();
this.data = d.data;
}
async redraw() {
const threshold = 5;
const features = (this.data?.coordinates || [])
.filter(([lon, lat, intensity]: [number, number, number], i) => i % 4 == 0 && lat != 0 && intensity >= threshold)
.map(([lon, lat, intensity]: [number, number, number]) => {
const feature = new Feature({geometry: new Point([normalize(lon), lat])});
const geom = feature.getGeometry() as Point;
geom.transform('EPSG:4326', this.map.getView().getProjection());
feature.setStyle(this.style(intensity));
return feature;
});
if(this.layer) this.map.removeLayer(this.layer);
if(!features.length) return;
this.layer = new VectorLayer({source: new VectorSource({features, wrapX: true}), zIndex: 5030, opacity: 0.4});
this.layer.set('name', this.name);
this.map.addLayer(this.layer);
}
async show() {
if(this.visible) return;
this.visible = true;
if(!this.data || Date.now() - this.timestamp > this.refreshRate) await this.update();
if(!this.visible) return;
await this.redraw();
if(!this.refresh) {
this.refresh = setInterval(async () => {
await this.update();
if(!this.visible) return;
await this.redraw();
}, this.refreshRate);
}
}
hide() {
if(!this.visible) return;
this.visible = false;
if(this.refresh) this.refresh = clearInterval(this.refresh);
this.map.getLayers().forEach(layer => {
if(layer?.get('name') == this.name) this.map.removeLayer(layer);
});
}
}