412 lines
17 KiB
JavaScript
412 lines
17 KiB
JavaScript
/**
|
||
* 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();
|
||
}
|
||
|
||
}());
|