Geocron ist drin

This commit is contained in:
Eskimue
2026-04-02 15:40:06 +02:00
parent 4adb5a6065
commit 1156cb66dc
39 changed files with 3195 additions and 0 deletions
+385
View File
@@ -0,0 +1,385 @@
/**
* aurora.js – Polarlicht-Plugin für die Erd-Visualisierung
* ===========================================================
* Einbinden: <script src="aurora.js"></script>
* (am Ende des <body>, nach dem bestehenden <script>-Block)
*
* Datenquelle: NOAA SWPC Ovation Prime Aurora Forecast
* Aktualisierung: alle ~5 Minuten (entspricht der NOAA-Auflösung)
*/
(function () {
// ═══════════════════════════════════════════════════════════════
// KONFIGURATION
// ═══════════════════════════════════════════════════════════════
const AU_CFG = {
// ── Datenquelle ────────────────────────────────────────────
// NOAA SWPC Ovation Prime – globale Aurora-Wahrscheinlichkeit
DATA_URL: "https://services.swpc.noaa.gov/json/ovation_aurora_latest.json",
REFRESH_MS: 5 * 60 * 1000, // alle 5 Minuten (NOAA aktualisiert ~alle 5 min)
// ── Darstellung ────────────────────────────────────────────
// Mindest-Wahrscheinlichkeit zum Zeichnen (0–100)
// Werte darunter werden ignoriert (= unsichtbar)
MIN_PROBABILITY: 4,
// Farbverlauf nach Wahrscheinlichkeit:
// [ { prob: Schwellwert, r, g, b, a } ]
// prob=0 ist immer transparent, höhere Werte heller/farbiger
COLOR_STOPS: [
{ prob: 0, r: 0, g: 255, b: 120, a: 0.5 }, // transparent
{ prob: 5, r: 0, g: 230, b: 100, a: 1.0 }, // zartes Grün
{ prob: 20, r: 30, g: 255, b: 140, a: 1.0 }, // helles Grün
{ prob: 40, r: 100, g: 255, b: 200, a: 1.0 }, // Grün-Cyan
{ prob: 60, r: 140, g: 200, b: 255, a: 1.0 }, // Blau-Weiß
{ prob: 80, r: 200, g: 150, b: 255, a: 1.0 }, // Lila-Weiß
{ prob:100, r: 255, g: 255, b: 255, a: 1.0 }, // Weiß
],
// Globale Deckkraft des Aurora-Layers (0.0 = unsichtbar, 1.0 = voll sichtbar)
// Dieser Wert multipliziert die Deckkraft aller COLOR_STOPS – schnelle Einstellung
// ohne die einzelnen Farbwerte anfassen zu müssen
OPACITY: 1.0,
// Weichzeichner über die fertige Aurora-Maske (in Pixeln)
// Höher = weichere, diffusere Darstellung
BLUR_PX: 6,
// Auflösung der internen Render-Karte
// NOAA liefert 1°-Gitter (360×181 Punkte) – wir rendern auf dieser Größe
// und skalieren dann auf Canvas-Größe hoch (mit blur geglättet)
RENDER_W: 720,
RENDER_H: 361,
// ── Canvas z-index ─────────────────────────────────────────
// Zwischen Nachtkarte (z:2) und Wolken (z:3) oder darüber?
// 2.5 wäre ideal, aber CSS z-index ist ganzzahlig →
// wir setzen 3 und legen aurora UNTER die Wolken
Z_INDEX: 3, // über Nacht (2), unter Wolken (3) ← empfohlen
// ── Info-Anzeige (oben links) ──────────────────────────────
SHOW_INFO: true,
INFO_FONT: "12px system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif",
// ── Kp-Index Anzeige ───────────────────────────────────────
// Zusätzlich den aktuellen Kp-Index laden und anzeigen
SHOW_KP: true,
KP_URL: "https://services.swpc.noaa.gov/products/noaa-planetary-k-index.json",
};
// ═══════════════════════════════════════════════════════════════
// INTERNE VARIABLEN
// ═══════════════════════════════════════════════════════════════
let auroraPoints = []; // [{ lon, lat, aurora }] aurora = 0–100
let forecastTime = null; // Zeitstempel der Vorhersage
let currentKp = null; // aktueller Kp-Index
let canvas, ctx;
let offscreen = null; // vorberechnete Aurora-Textur
let lastFetch = 0;
let animFrame = null;
// ═══════════════════════════════════════════════════════════════
// CANVAS EINRICHTEN
// ═══════════════════════════════════════════════════════════════
function initCanvas() {
const viewport = document.getElementById("earth-viewport");
if (!viewport) {
console.error("aurora.js: #earth-viewport nicht gefunden.");
return false;
}
canvas = document.createElement("canvas");
canvas.id = "auroraCanvas";
canvas.style.position = "absolute";
canvas.style.top = "0";
canvas.style.left = "0";
canvas.style.display = "block";
canvas.style.zIndex = AU_CFG.Z_INDEX;
canvas.style.pointerEvents = "none";
viewport.appendChild(canvas);
ctx = canvas.getContext("2d");
syncSize();
const ro = new ResizeObserver(() => { syncSize(); renderOffscreen(); draw(); });
ro.observe(viewport);
return true;
}
function syncSize() {
const ref = document.getElementById("nightCanvas")
|| document.getElementById("earthDay");
if (!ref) return;
const w = ref.offsetWidth || ref.width || 0;
const h = ref.offsetHeight || ref.height || 0;
if (w && h) {
canvas.style.width = w + "px";
canvas.style.height = h + "px";
canvas.width = w;
canvas.height = h;
}
}
// ═══════════════════════════════════════════════════════════════
// DATEN LADEN
// ═══════════════════════════════════════════════════════════════
async function fetchAurora() {
try {
const res = await fetch(AU_CFG.DATA_URL, { cache: "no-store" });
if (!res.ok) throw new Error("HTTP " + res.status);
const json = await res.json();
// Ovation-Format: { "Forecast Time": "...", "coordinates": [[lon, lat, aurora], ...] }
forecastTime = json["Forecast Time"] ? new Date(json["Forecast Time"]) : null;
auroraPoints = (json.coordinates || []).map(c => ({
lon: c[0],
lat: c[1],
aurora: c[2],
}));
lastFetch = Date.now();
renderOffscreen();
} catch (e) {
console.warn("aurora.js: Aurora-Fetch-Fehler –", e.message);
}
}
async function fetchKp() {
if (!AU_CFG.SHOW_KP) return;
try {
const res = await fetch(AU_CFG.KP_URL, { cache: "no-store" });
if (!res.ok) return;
const json = await res.json();
// Format: [["time", "Kp", ...], ["2024-...", "3.33", ...], ...]
// Letzter Datenpunkt ist aktuellster Wert
if (json.length > 1) {
const last = json[json.length - 1];
currentKp = parseFloat(last[1]);
}
} catch (e) { /* ignorieren */ }
}
// ═══════════════════════════════════════════════════════════════
// AURORA-TEXTUR VORBERECHNEN (Offscreen-Canvas)
//
// Die NOAA-Daten liefern ~65160 Punkte (360 × ~181).
// Wir rendern sie einmalig auf ein kleines Offscreen-Canvas
// und skalieren es dann auf den Haupt-Canvas hoch.
// Das vermeidet teure per-Frame-Berechnungen.
// ═══════════════════════════════════════════════════════════════
function interpolateColor(prob) {
const stops = AU_CFG.COLOR_STOPS;
if (prob <= stops[0].prob) return stops[0];
if (prob >= stops[stops.length - 1].prob) return stops[stops.length - 1];
for (let i = 0; i < stops.length - 1; i++) {
const s0 = stops[i], s1 = stops[i + 1];
if (prob >= s0.prob && prob <= s1.prob) {
const t = (prob - s0.prob) / (s1.prob - s0.prob);
return {
r: Math.round(s0.r + t * (s1.r - s0.r)),
g: Math.round(s0.g + t * (s1.g - s0.g)),
b: Math.round(s0.b + t * (s1.b - s0.b)),
a: s0.a + t * (s1.a - s0.a),
};
}
}
return stops[stops.length - 1];
}
function renderOffscreen() {
if (!auroraPoints.length) return;
const RW = AU_CFG.RENDER_W;
const RH = AU_CFG.RENDER_H;
if (!offscreen) {
offscreen = document.createElement("canvas");
offscreen.width = RW;
offscreen.height = RH;
}
const octx = offscreen.getContext("2d");
const imgData = octx.createImageData(RW, RH);
const d = imgData.data;
// Gitter aufbauen: lon -180…180, lat 90…-90
// NOAA gibt Lon 0-360, wir müssen auf -180…180 normalisieren
const grid = new Float32Array(RW * RH); // initialisiert mit 0
for (const pt of auroraPoints) {
if (pt.aurora < AU_CFG.MIN_PROBABILITY) continue;
// NOAA: lon 0…360
const lon = pt.lon > 180 ? pt.lon - 360 : pt.lon;
const lat = pt.lat;
// Pixel-Koordinaten
const px = Math.round(((lon + 180) / 360) * (RW - 1));
const py = Math.round(((90 - lat) / 180) * (RH - 1));
if (px >= 0 && px < RW && py >= 0 && py < RH) {
grid[py * RW + px] = Math.max(grid[py * RW + px], pt.aurora);
}
}
// Einfacher 3×3-Mittelwert-Blur über das Gitter (Kantenglätte)
const blurred = new Float32Array(RW * RH);
for (let y = 1; y < RH - 1; y++) {
for (let x = 1; x < RW - 1; x++) {
let sum = 0;
for (let dy = -1; dy <= 1; dy++)
for (let dx = -1; dx <= 1; dx++)
sum += grid[(y + dy) * RW + (x + dx)];
blurred[y * RW + x] = sum / 9;
}
}
// Auf ImageData schreiben
for (let i = 0; i < RW * RH; i++) {
const v = blurred[i];
if (v < AU_CFG.MIN_PROBABILITY) continue;
const c = interpolateColor(v);
d[i * 4] = c.r;
d[i * 4 + 1] = c.g;
d[i * 4 + 2] = c.b;
d[i * 4 + 3] = Math.round(c.a * 255);
}
octx.putImageData(imgData, 0, 0);
}
// ═══════════════════════════════════════════════════════════════
// ZEICHNEN (pro Frame)
// ═══════════════════════════════════════════════════════════════
function draw() {
const w = canvas.width, h = canvas.height;
if (!w || !h) return;
ctx.clearRect(0, 0, w, h);
if (offscreen) {
ctx.save();
ctx.globalAlpha = Math.max(0, Math.min(1, AU_CFG.OPACITY));
ctx.filter = `blur(${AU_CFG.BLUR_PX}px)`;
ctx.drawImage(offscreen, 0, 0, w, h);
ctx.filter = "none";
ctx.globalAlpha = 1;
ctx.restore();
}
if (AU_CFG.SHOW_INFO) drawInfo(w, h);
}
// ─── Info-Box (Vorschauzeit + Kp-Index) ───────────────────────
function drawInfo(w, h) {
const lines = [];
if (forecastTime) {
lines.push("🌌 Aurora Forecast");
lines.push(forecastTime.toLocaleString("de-DE", {
day: "2-digit", month: "2-digit", year: "numeric",
hour: "2-digit", minute: "2-digit", timeZoneName: "short"
}));
}
if (currentKp !== null) {
const kpLabel = kpDescription(currentKp);
lines.push(`Kp-Index: ${currentKp.toFixed(1)} ${kpLabel}`);
}
if (!lines.length) return;
ctx.save();
ctx.font = AU_CFG.INFO_FONT;
ctx.textBaseline = "top";
const lh = 17, padX = 10, padY = 7;
const bw = Math.max(...lines.map(l => ctx.measureText(l).width)) + padX * 2;
const bh = lines.length * lh + padY * 2;
const ix = 10, iy = 10;
// Hintergrund
ctx.fillStyle = "rgba(5, 10, 25, 0.75)";
ctx.strokeStyle = "rgba(80, 200, 160, 0.4)";
ctx.lineWidth = 1;
roundRect(ctx, ix, iy, bw, bh, 6);
ctx.fill(); ctx.stroke();
lines.forEach((line, i) => {
if (i === 0) {
ctx.font = "bold " + AU_CFG.INFO_FONT;
ctx.fillStyle = "rgba(100, 255, 180, 0.95)";
} else if (line.startsWith("Kp")) {
ctx.font = AU_CFG.INFO_FONT;
ctx.fillStyle = kpColor(currentKp);
} else {
ctx.font = AU_CFG.INFO_FONT;
ctx.fillStyle = "rgba(180, 230, 210, 0.85)";
}
ctx.fillText(line, ix + padX, iy + padY + i * lh);
});
ctx.restore();
}
function kpDescription(kp) {
if (kp < 1) return "(ruhig)";
if (kp < 2) return "(ruhig)";
if (kp < 3) return "(unsicher)";
if (kp < 4) return "(leicht aktiv)";
if (kp < 5) return "(aktiv)";
if (kp < 6) return "⚡ G1 – Kleiner Sturm";
if (kp < 7) return "⚡ G2 – Mäßiger Sturm";
if (kp < 8) return "⚡ G3 – Starker Sturm";
if (kp < 9) return "⚡ G4 – Sehr starker Sturm";
return "⚡ G5 – Extremer Sturm";
}
function kpColor(kp) {
if (kp < 4) return "rgba(100, 230, 150, 0.9)";
if (kp < 5) return "rgba(200, 220, 60, 0.9)";
if (kp < 6) return "rgba(255, 160, 30, 0.9)";
if (kp < 7) return "rgba(255, 80, 40, 0.9)";
return "rgba(255, 100, 255, 0.9)";
}
// ═══════════════════════════════════════════════════════════════
// CANVAS-HELFER
// ═══════════════════════════════════════════════════════════════
function roundRect(ctx, x, y, w, h, r) {
const rr = Math.min(r, w / 2, h / 2);
ctx.beginPath();
ctx.moveTo(x + rr, y);
ctx.arcTo(x + w, y, x + w, y + h, rr);
ctx.arcTo(x + w, y + h, x, y + h, rr);
ctx.arcTo(x, y + h, x, y, rr);
ctx.arcTo(x, y, x + w, y, rr);
ctx.closePath();
}
// ═══════════════════════════════════════════════════════════════
// ANIMATIONSSCHLEIFE
// ═══════════════════════════════════════════════════════════════
function loop() {
if (Date.now() - lastFetch >= AU_CFG.REFRESH_MS) {
fetchAurora();
fetchKp();
}
draw();
animFrame = requestAnimationFrame(loop);
}
// ═══════════════════════════════════════════════════════════════
// START
// ═══════════════════════════════════════════════════════════════
function init() {
if (!initCanvas()) return;
Promise.all([fetchAurora(), fetchKp()]).then(() => loop());
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
}());