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
+411
View File
@@ -0,0 +1,411 @@
/**
* earthquake.js – Erdbeben-Plugin für die Erd-Visualisierung
* =============================================================
* Einbinden: <script src="earthquake.js"></script>
* (einfach am Ende des <body>, nach dem bestehenden <script>-Block)
*
* Voraussetzung: Das Element #earth-wrapper existiert im DOM.
*/
(function () {
// ═══════════════════════════════════════════════════════════════
// KONFIGURATION
// ═══════════════════════════════════════════════════════════════
const EQ_CFG = {
// ── Datenquelle ────────────────────────────────────────────
// USGS GeoJSON-Feeds (wähle einen aus oder eigene URL):
// all_hour = alle letzten 60 Minuten
// all_day = alle letzten 24 Stunden ← Standard
// all_week = alle letzten 7 Tage
// 2.5_day = nur M ≥ 2.5 letzte 24 h
DATA_URL: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson",
REFRESH_MS: 5 * 60 * 1000, // Aktualisierung alle 5 Minuten
// ── Mindestmagnitude zum Anzeigen ──────────────────────────
MIN_MAG: 0, // 0 = alles anzeigen
// ── Marker-Darstellung ─────────────────────────────────────
// Radius = BASE_RADIUS_PX + MAG_SCALE_PX × magnitude
BASE_RADIUS_PX: 2,
MAG_SCALE_PX: 2.2,
MIN_RADIUS_PX: 2,
// Farbe nach Magnitude (Grenzen aufsteigend)
MAG_COLORS: [
{ min: 0, color: "rgba( 80, 200, 80, 0.85)" }, // grün < 2
{ min: 2, color: "rgba( 180, 220, 60, 0.85)" }, // gelb-grün
{ min: 3, color: "rgba( 255, 210, 0, 0.85)" }, // gelb
{ min: 4, color: "rgba( 255, 140, 0, 0.85)" }, // orange
{ min: 5, color: "rgba( 230, 50, 30, 0.85)" }, // rot
{ min: 6, color: "rgba( 180, 0, 200, 0.85)" }, // lila
{ min: 7, color: "rgba( 255, 255, 255, 0.95)" }, // weiß
],
BORDER_COLOR: "rgba(255, 255, 255, 0.35)",
BORDER_WIDTH_PX: 0.8,
GLOW_BLUR_PX: 6,
// ── Pulsierender Ring für starke Beben ─────────────────────
PULSE_MIN_MAG: 5.0, // ab dieser Magnitude pulsiert ein Ring
PULSE_DURATION_MS: 2000, // Dauer einer Pulsation in ms
// ── Canvas z-index ─────────────────────────────────────────
Z_INDEX: 9999,
// ── Popup (Klick auf Marker) ───────────────────────────────
POPUP_BG: "rgba(10, 15, 30, 0.92)",
POPUP_BORDER: "rgba(120, 180, 255, 0.6)",
POPUP_TEXT: "#e8f0ff",
POPUP_FONT: "13px system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif",
// ── Legende ────────────────────────────────────────────────
SHOW_LEGEND: true,
LEGEND_FONT: "12px system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif",
};
// ═══════════════════════════════════════════════════════════════
// INTERNE VARIABLEN
// ═══════════════════════════════════════════════════════════════
let earthquakes = []; // aktuelle Erdbebendaten
let canvas, ctx;
let popup = null; // { x, y, quake } oder null
let animFrame = null;
let lastFetch = 0;
// ── Koordinaten-Helpers (gleiche Logik wie Haupt-Skript) ──────
const toX = (lon, w) => ((lon + 180) / 360) * w;
const toY = (lat, h) => ((90 - lat) / 180) * h;
// ═══════════════════════════════════════════════════════════════
// CANVAS EINRICHTEN
// ═══════════════════════════════════════════════════════════════
function initCanvas() {
const viewport = document.getElementById("earth-viewport");
if (!viewport) {
console.error("earthquake.js: #earth-viewport nicht gefunden.");
return false;
}
canvas = document.createElement("canvas");
canvas.id = "earthquakeCanvas";
canvas.style.position = "absolute";
canvas.style.top = "0";
canvas.style.left = "0";
canvas.style.display = "block";
canvas.style.zIndex = EQ_CFG.Z_INDEX;
canvas.style.pointerEvents = "none"; // Klicks standard-mäßig durchlassen
viewport.appendChild(canvas);
ctx = canvas.getContext("2d");
// Größe an vorhandenes Erdbild anpassen
syncSize();
// ResizeObserver auf das earth-wrapper-Element
const ro = new ResizeObserver(() => { syncSize(); draw(); });
ro.observe(viewport);
// Klick-Handler für Popups
canvas.style.pointerEvents = "auto";
canvas.addEventListener("click", onCanvasClick);
canvas.addEventListener("mousemove", onCanvasMove);
return true;
}
function syncSize() {
// Referenz: erstes Canvas-Kind (oder dayImg) im wrapper
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 fetchEarthquakes() {
try {
const res = await fetch(EQ_CFG.DATA_URL, { cache: "no-store" });
if (!res.ok) throw new Error("HTTP " + res.status);
const json = await res.json();
earthquakes = (json.features || [])
.filter(f => {
const mag = f.properties?.mag;
return mag !== null && mag >= EQ_CFG.MIN_MAG;
})
.map(f => ({
mag: f.properties.mag,
place: f.properties.place || "Unbekannt",
time: new Date(f.properties.time),
depth: f.geometry?.coordinates?.[2] ?? 0,
lon: f.geometry?.coordinates?.[0] ?? 0,
lat: f.geometry?.coordinates?.[1] ?? 0,
}));
lastFetch = Date.now();
} catch (e) {
console.warn("earthquake.js: Fetch-Fehler –", e.message);
}
}
// ═══════════════════════════════════════════════════════════════
// ZEICHNEN
// ═══════════════════════════════════════════════════════════════
function magColor(mag) {
let color = EQ_CFG.MAG_COLORS[0].color;
for (const entry of EQ_CFG.MAG_COLORS) {
if (mag >= entry.min) color = entry.color;
}
return color;
}
function markerRadius(mag) {
return Math.max(
EQ_CFG.MIN_RADIUS_PX,
EQ_CFG.BASE_RADIUS_PX + EQ_CFG.MAG_SCALE_PX * mag
);
}
function draw() {
const w = canvas.width, h = canvas.height;
if (!w || !h) return;
ctx.clearRect(0, 0, w, h);
const now = Date.now();
// ── Erdbeben-Marker ─────────────────────────────────────────
for (const q of earthquakes) {
const x = toX(q.lon, w);
const y = toY(q.lat, h);
const r = markerRadius(q.mag/2);
const c = magColor(q.mag);
// Pulsierender Ring für starke Beben
if (q.mag >= EQ_CFG.PULSE_MIN_MAG) {
const age = (now % EQ_CFG.PULSE_DURATION_MS) / EQ_CFG.PULSE_DURATION_MS;
const pr = r + age * r * 2.5;
const pa = (1 - age) * 0.6;
ctx.save();
ctx.beginPath();
ctx.arc(x, y, pr, 0, Math.PI * 2);
ctx.strokeStyle = c.replace(/[\d.]+\)$/, pa.toFixed(2) + ")");
ctx.lineWidth = 1.2;
ctx.stroke();
ctx.restore();
}
// Hauptmarker
ctx.save();
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fillStyle = c;
ctx.shadowColor = c;
ctx.shadowBlur = EQ_CFG.GLOW_BLUR_PX;
ctx.fill();
ctx.strokeStyle = EQ_CFG.BORDER_COLOR;
ctx.lineWidth = EQ_CFG.BORDER_WIDTH_PX;
ctx.stroke();
ctx.restore();
}
// ── Legende ─────────────────────────────────────────────────
if (EQ_CFG.SHOW_LEGEND) drawLegend(w, h);
// ── Popup ────────────────────────────────────────────────────
if (popup) drawPopup(popup, w, h);
}
function drawLegend(w, h) {
const items = [
{ label: "M < 2", color: EQ_CFG.MAG_COLORS[0].color },
{ label: "M 2–3", color: EQ_CFG.MAG_COLORS[1].color },
{ label: "M 3–4", color: EQ_CFG.MAG_COLORS[2].color },
{ label: "M 4–5", color: EQ_CFG.MAG_COLORS[3].color },
{ label: "M 5–6", color: EQ_CFG.MAG_COLORS[4].color },
{ label: "M 6–7", color: EQ_CFG.MAG_COLORS[5].color },
{ label: "M ≥ 7", color: EQ_CFG.MAG_COLORS[6].color },
];
const padX = 10, padY = 6;
const lineH = 18;
const dotR = 5;
const legW = 90;
const legH = padY * 2 + items.length * lineH;
const lx = w - legW - 10;
const ly = h - legH - 10;
// Hintergrund
ctx.save();
ctx.fillStyle = "rgba(10, 15, 30, 0.75)";
ctx.strokeStyle = "rgba(100, 160, 255, 0.35)";
ctx.lineWidth = 1;
roundRect(ctx, lx, ly, legW, legH, 6);
ctx.fill(); ctx.stroke();
// Titel
ctx.font = "bold " + EQ_CFG.LEGEND_FONT;
ctx.fillStyle = "rgba(180, 210, 255, 0.95)";
ctx.textBaseline = "middle";
ctx.textAlign = "left";
// Zeilen
ctx.font = EQ_CFG.LEGEND_FONT;
items.forEach((item, i) => {
const iy = ly + padY + dotR + i * lineH;
ctx.beginPath();
ctx.arc(lx + padX + dotR, iy, dotR, 0, Math.PI * 2);
ctx.fillStyle = item.color;
ctx.shadowColor = item.color;
ctx.shadowBlur = 4;
ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = "rgba(220, 230, 255, 0.9)";
ctx.fillText(item.label, lx + padX + dotR * 2 + 4, iy);
});
// Zähler unten
ctx.font = "11px " + EQ_CFG.LEGEND_FONT;
ctx.fillStyle = "rgba(140, 170, 220, 0.75)";
ctx.textAlign = "center";
const countText = earthquakes.length + " Beben (24 h)";
ctx.fillText(countText, lx + legW / 2, ly + legH - padY);
ctx.restore();
}
function drawPopup(pop, w, h) {
const q = pop.quake;
const magStr = q.mag !== null ? "M " + q.mag.toFixed(1) : "M ?";
const timeStr =
q.time.toLocaleString("de-DE", {
day: "2-digit", month: "2-digit", year: "numeric",
hour: "2-digit", minute: "2-digit",
timeZone: "UTC"
}) + " UTC";
const depthStr = q.depth.toFixed(1) + " km Tiefe";
const lines = [
magStr + " – " + depthStr,
q.place,
timeStr,
];
ctx.save();
ctx.font = EQ_CFG.POPUP_FONT;
ctx.textBaseline = "top";
const lh = 18, padX = 10, padY = 8;
const bw = Math.max(...lines.map(l => ctx.measureText(l).width)) + padX * 2;
const bh = lines.length * lh + padY * 2;
let px = pop.x + 12, py = pop.y - bh / 2;
if (px + bw > w - 5) px = pop.x - bw - 12;
if (py < 5) py = 5;
if (py + bh > h - 5) py = h - bh - 5;
roundRect(ctx, px, py, bw, bh, 7);
ctx.fillStyle = EQ_CFG.POPUP_BG;
ctx.fill();
ctx.strokeStyle = EQ_CFG.POPUP_BORDER;
ctx.lineWidth = 1;
ctx.stroke();
lines.forEach((line, i) => {
ctx.fillStyle = i === 0 ? magColor(q.mag) : EQ_CFG.POPUP_TEXT;
if (i === 0) {
ctx.font = "bold " + EQ_CFG.POPUP_FONT;
} else {
ctx.font = EQ_CFG.POPUP_FONT;
}
ctx.fillText(line, px + padX, py + padY + i * lh);
});
ctx.restore();
}
// ═══════════════════════════════════════════════════════════════
// INTERAKTION
// ═══════════════════════════════════════════════════════════════
function hitTest(cx, cy) {
const w = canvas.width, h = canvas.height;
let best = null, bestDist = Infinity;
for (const q of earthquakes) {
const x = toX(q.lon, w);
const y = toY(q.lat, h);
const r = markerRadius(q.mag) + 3; // etwas größere Klickfläche
const d = Math.hypot(cx - x, cy - y);
if (d <= r && d < bestDist) { best = q; bestDist = d; }
}
return best;
}
function onCanvasClick(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const mx = (e.clientX - rect.left) * scaleX;
const my = (e.clientY - rect.top) * scaleY;
const q = hitTest(mx, my);
popup = q ? { x: mx, y: my, quake: q } : null;
draw();
}
function onCanvasMove(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const mx = (e.clientX - rect.left) * scaleX;
const my = (e.clientY - rect.top) * scaleY;
canvas.style.cursor = hitTest(mx, my) ? "pointer" : "default";
}
// ═══════════════════════════════════════════════════════════════
// ANIMATIONSSCHLEIFE
// ═══════════════════════════════════════════════════════════════
function loop() {
// Daten bei Bedarf nachladen
if (Date.now() - lastFetch >= EQ_CFG.REFRESH_MS) fetchEarthquakes();
draw();
animFrame = requestAnimationFrame(loop);
}
// ═══════════════════════════════════════════════════════════════
// 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();
}
// ═══════════════════════════════════════════════════════════════
// START
// ═══════════════════════════════════════════════════════════════
function init() {
if (!initCanvas()) return;
fetchEarthquakes().then(() => { loop(); });
}
// Warten bis DOM bereit ist
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
}());