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
+355
View File
@@ -0,0 +1,355 @@
/**
* plugin-satellites.js – Satelliten & Orbits
* ==============================================
* Abhängigkeit: earth-core.js, satellite.js (unpkg CDN)
*
* Erstellt: #orbitCanvas (z-index 4)
*
* Ortsmarker (z.B. Museum) → plugin-markers.js
*/
(function () {
// ═══════════════════════════════════════════════════════════════
// KONFIGURATION
// ═══════════════════════════════════════════════════════════════
const CFG = {
// ── TLE-Endpunkt ───────────────────────────────────────────
TLE_ENDPOINT: "getTLE.php", // ?ids=25544,20580,… → JSON aus DB
// ── Satelliten-Icons ───────────────────────────────────────
// Zuerst ../images/satpics/{id}.png versucht, dann Fallback
SAT_ICON_FALLBACK: "../images/satpics/satellit.png",
// ── Marker-Größe ───────────────────────────────────────────
SAT_MARKER_SIZE_PX: 36,
SAT_MARKER_MIN_PX: 18,
SAT_SCALE: 1.5,
ISS_SCALE: 3, // zusätzlicher Faktor nur für ISS (ID 25544)
// ── Labels ─────────────────────────────────────────────────
SHOW_SAT_LABELS: true,
LABEL_FONT_PX: 14,
LABEL_FONT_FAMILY: "system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif",
LABEL_OFFSET_X_PX: 10,
LABEL_OFFSET_Y_PX: -10,
LABEL_TEXT_COLOR: "rgba(255, 255, 255, 0.95)",
LABEL_BG_COLOR: "rgba(0, 0, 0, 0.55)",
LABEL_PADDING_X_PX: 6,
LABEL_PADDING_Y_PX: 3,
LABEL_RADIUS_PX: 6,
// ── Orbit-Vorschau ─────────────────────────────────────────
ORBIT_MINUTES: 90, // Vorschau-Zeitraum ab jetzt
ORBIT_STEP_SEC: 10, // Zeitschritt
ORBIT_COLORS: [
"rgba(120, 200, 255, 0.95)",
"rgba(255, 160, 120, 0.95)",
"rgba(160, 255, 160, 0.95)",
"rgba(220, 180, 255, 0.95)",
"rgba(255, 220, 120, 0.95)",
],
ORBIT_LINE_WIDTH_DIV: 1500,
ORBIT_SHADOW_BLUR: 0,
};
// ── Kurz-Referenz auf EarthCore-Helfer ────────────────────────
const { toX, toY, createCanvas, syncCanvas, roundRectPath } = EarthCore;
// ── Canvas & Context ──────────────────────────────────────────
const orbitCanvas = createCanvas("orbitCanvas", 10000); // pointerEvents: none → Klicks via window
const ctx = orbitCanvas.getContext("2d");
// ── Icons laden ───────────────────────────────────────────────
const satIconMap = new Map();
const satFallbackIcon = new Image();
satFallbackIcon.src = CFG.SAT_ICON_FALLBACK;
function loadSatIcon(id) {
return new Promise(resolve => {
const img = new Image();
img.onload = () => { satIconMap.set(id, img); resolve(); };
img.onerror = () => { satIconMap.set(id, satFallbackIcon); resolve(); };
img.src = `../images/satpics/${id}.png`;
});
}
// ── TLE-State ─────────────────────────────────────────────────
let satrecs = [];
// ── Popup-State ───────────────────────────────────────────────
let popup = null; // { x, y, sat: { name, altKm, speedKms, lat, lon } }
let lastPositions = []; // [{ id, name, x, y, altKm, speedKms }] nach jedem Render
// ═══════════════════════════════════════════════════════════════
// TLE AUS DB LADEN & SATRECS AUFBAUEN
// ═══════════════════════════════════════════════════════════════
async function buildSatRecs() {
const url = CFG.TLE_ENDPOINT;
const res = await fetch(url, { cache: "no-store" });
if (!res.ok) throw new Error("TLE-Endpunkt Fehler: " + res.status);
const rows = await res.json();
if (rows.error) throw new Error("TLE-DB-Fehler: " + rows.error);
satrecs = rows.map(r => ({
id: Number(r.norad_cat_id),
name: r.object_name ?? String(r.norad_cat_id),
isFavorite: Boolean(r.is_favorite),
satrec: satellite.twoline2satrec(r.tle_line1, r.tle_line2),
}));
await Promise.all(satrecs.map(s => loadSatIcon(s.id)));
}
// ═══════════════════════════════════════════════════════════════
// SATELLITEN-POSITION
// ═══════════════════════════════════════════════════════════════
const wrapLon = lon => ((lon + 180) % 360 + 360) % 360 - 180;
function currentLatLon(satrec, date) {
const pv = satellite.propagate(satrec, date);
if (!pv.position) return null;
const gmst = satellite.gstime(date);
const gd = satellite.eciToGeodetic(pv.position, gmst);
const v = pv.velocity;
const speedKms = v ? Math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z) : null;
return {
lat: satellite.degreesLat(gd.latitude),
lon: wrapLon(satellite.degreesLong(gd.longitude)),
altKm: gd.height, // km über Ellipsoid
speedKms, // km/s
};
}
function groundTrackFuture(satrec, startDate) {
const steps = Math.floor((CFG.ORBIT_MINUTES * 60) / CFG.ORBIT_STEP_SEC);
const pts = [];
for (let i = 0; i <= steps; i++) {
const d = new Date(startDate.getTime() + i * CFG.ORBIT_STEP_SEC * 1000);
const ll = currentLatLon(satrec, d);
if (ll) pts.push(ll);
}
return pts;
}
// ═══════════════════════════════════════════════════════════════
// ZEICHNEN
// ═══════════════════════════════════════════════════════════════
function drawOrbitLine(points, baseColor) {
if (points.length < 2) return;
const total = points.length - 1;
const match = baseColor.match(/[\d.]+/g);
const r = match[0], g = match[1], b = match[2];
for (let i = 0; i < total; i++) {
const p0 = points[i], p1 = points[i + 1];
if (Math.abs(p1.lon - p0.lon) > 180) continue;
const t = i / total;
const alpha = (1 - t) * (1 - t);
ctx.beginPath();
ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${alpha.toFixed(3)})`;
ctx.moveTo(toX(p0.lon, orbitCanvas.width), toY(p0.lat, orbitCanvas.height));
ctx.lineTo(toX(p1.lon, orbitCanvas.width), toY(p1.lat, orbitCanvas.height));
ctx.stroke();
}
}
function getMarkerSize(id) {
const base = Math.max(CFG.SAT_MARKER_MIN_PX, CFG.SAT_MARKER_SIZE_PX) * CFG.SAT_SCALE;
return id === 25544 ? base * CFG.ISS_SCALE : base;
}
function drawMarker(id, lat, lon) {
const w = orbitCanvas.width, h = orbitCanvas.height;
const x = toX(lon, w), y = toY(lat, h);
const img = satIconMap.get(id) ?? satFallbackIcon;
const size = getMarkerSize(id);
const half = size / 2;
if (img.complete && img.naturalWidth > 0) {
ctx.drawImage(img, x - half, y - half, size, size);
if (x - half < 0) ctx.drawImage(img, x - half + w, y - half, size, size);
if (x + half > w) ctx.drawImage(img, x - half - w, y - half, size, size);
} else {
ctx.save();
ctx.beginPath();
ctx.arc(x, y, Math.max(4, size / 6), 0, 2 * Math.PI);
ctx.fillStyle = "rgba(255, 255, 255, 0.9)";
ctx.shadowColor = "rgba(255, 255, 255, 0.6)";
ctx.shadowBlur = 6;
ctx.fill();
ctx.restore();
}
}
function drawLabel(x, y, text) {
if (!CFG.SHOW_SAT_LABELS || !text) return;
ctx.save();
ctx.font = `${CFG.LABEL_FONT_PX}px ${CFG.LABEL_FONT_FAMILY}`;
ctx.textBaseline = "middle";
ctx.textAlign = "left";
const tx = x + CFG.LABEL_OFFSET_X_PX;
const ty = y + CFG.LABEL_OFFSET_Y_PX;
const padX = CFG.LABEL_PADDING_X_PX;
const padY = CFG.LABEL_PADDING_Y_PX;
const boxW = ctx.measureText(text).width + padX * 2;
const boxH = CFG.LABEL_FONT_PX + padY * 2;
ctx.fillStyle = CFG.LABEL_BG_COLOR;
roundRectPath(ctx, tx, ty - boxH / 2, boxW, boxH, CFG.LABEL_RADIUS_PX);
ctx.fill();
ctx.fillStyle = CFG.LABEL_TEXT_COLOR;
ctx.fillText(text, tx + padX, ty);
ctx.restore();
}
// ── Popup zeichnen ────────────────────────────────────────────
function drawPopup(px, py, sat, w, h) {
const lines = [
sat.name,
"Höhe: " + sat.altKm.toFixed(0) + " km",
"Geschw.: " + (sat.speedKms * 3600).toFixed(0) + " km/h",
];
ctx.save();
ctx.font = "13px " + CFG.LABEL_FONT_FAMILY;
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 bx = px + 14, by = py - bh / 2;
if (bx + bw > w - 5) bx = px - bw - 14;
if (by < 5) by = 5;
if (by + bh > h - 5) by = h - bh - 5;
roundRectPath(ctx, bx, by, bw, bh, 7);
ctx.fillStyle = "rgba(10, 15, 30, 0.92)";
ctx.fill();
ctx.strokeStyle = "rgba(120, 180, 255, 0.6)";
ctx.lineWidth = 1;
ctx.stroke();
lines.forEach((line, i) => {
ctx.fillStyle = i === 0 ? "rgba(120, 200, 255, 0.95)" : "rgba(220, 230, 255, 0.9)";
if (i === 0) ctx.font = "bold 13px " + CFG.LABEL_FONT_FAMILY;
else ctx.font = "13px " + CFG.LABEL_FONT_FAMILY;
ctx.fillText(line, bx + padX, by + padY + i * lh);
});
ctx.restore();
}
function render({ w, h }) {
if (!w || !h) return;
syncCanvas(orbitCanvas, w, h);
ctx.clearRect(0, 0, w, h);
const now = new Date();
lastPositions = [];
for (let i = 0; i < satrecs.length; i++) {
const s = satrecs[i];
const color = CFG.ORBIT_COLORS[i % CFG.ORBIT_COLORS.length];
const pts = groundTrackFuture(s.satrec, now);
ctx.save();
ctx.lineWidth = Math.max(1, w / CFG.ORBIT_LINE_WIDTH_DIV);
ctx.shadowBlur = CFG.ORBIT_SHADOW_BLUR;
drawOrbitLine(pts, color);
ctx.restore();
if (pts.length > 0) {
const p0 = pts[0];
drawMarker(s.id, p0.lat, p0.lon);
const sx = toX(p0.lon, w), sy = toY(p0.lat, h);
drawLabel(sx, sy, s.name || String(s.id));
lastPositions.push({
id: s.id, name: s.name || String(s.id),
x: sx, y: sy,
altKm: p0.altKm ?? 0,
speedKms: p0.speedKms ?? 0,
});
}
}
// Popup über allem zeichnen
if (popup) {
drawPopup(popup.x, popup.y, popup.sat, w, h);
}
}
// ── Klick & Hover via window (orbitCanvas hat pointerEvents:none) ──
function canvasCoords(e) {
const rect = orbitCanvas.getBoundingClientRect();
if (!rect.width || !rect.height) return null;
const scaleX = orbitCanvas.width / rect.width;
const scaleY = orbitCanvas.height / rect.height;
return {
mx: (e.clientX - rect.left) * scaleX,
my: (e.clientY - rect.top) * scaleY,
};
}
function hitSatellite(mx, my) {
let best = null, bestDist = Infinity;
for (const p of lastPositions) {
const d = Math.hypot(mx - p.x, my - p.y);
const r = getMarkerSize(p.id) / 2 + 8;
if (d <= r && d < bestDist) { best = p; bestDist = d; }
}
return best;
}
window.addEventListener("click", e => {
const c = canvasCoords(e);
if (!c) return;
const hit = hitSatellite(c.mx, c.my);
if (hit) {
popup = { x: c.mx, y: c.my, sat: hit };
render(EarthCore.size);
} else if (popup) {
popup = null;
render(EarthCore.size);
}
});
window.addEventListener("mousemove", e => {
const c = canvasCoords(e);
if (!c) return;
const hit = hitSatellite(c.mx, c.my);
document.body.style.cursor = hit ? "pointer" : "";
});
// ── Events ────────────────────────────────────────────────────
window.addEventListener("earth:fast", e => render(e.detail));
window.addEventListener("earth:resize", e => render(e.detail));
// ── Abhängigkeit laden & initialisieren ───────────────────────
function loadScript(url) {
return new Promise((resolve, reject) => {
if (document.querySelector(`script[src="${url}"]`)) { resolve(); return; }
const s = document.createElement("script");
s.src = url;
s.onload = resolve;
s.onerror = () => reject(new Error("Konnte nicht laden: " + url));
document.head.appendChild(s);
});
}
(async function init() {
try {
await loadScript("https://unpkg.com/satellite.js@6.0.2/dist/satellite.min.js");
await buildSatRecs();
render(EarthCore.size);
} catch (e) {
console.warn("plugin-satellites:", e.message);
}
}());
}());