Files
skyview.astronomiemuseum.de/public/geocron/earthquake.js
T
2026-04-02 15:40:06 +02:00

412 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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();
}
}());