Geocron ist drin
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
}());
|
||||
Reference in New Issue
Block a user