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