Files
skyview.astronomiemuseum.de/public/geocron/plugin-satellites.js
T

382 lines
15 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.
/**
* 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);
function createSatrec(row) {
if (typeof satellite.json2satrec === "function") {
return satellite.json2satrec({
OBJECT_NAME: row.object_name ?? String(row.norad_cat_id),
OBJECT_ID: row.object_id ?? "",
EPOCH: row.epoch,
MEAN_MOTION: Number(row.mean_motion ?? 0),
ECCENTRICITY: Number(row.eccentricity ?? 0),
INCLINATION: Number(row.inclination ?? 0),
RA_OF_ASC_NODE: Number(row.ra_of_asc_node ?? 0),
ARG_OF_PERICENTER: Number(row.arg_of_pericenter ?? 0),
MEAN_ANOMALY: Number(row.mean_anomaly ?? 0),
EPHEMERIS_TYPE: Number(row.ephemeris_type ?? 0),
CLASSIFICATION_TYPE: row.classification_type ?? "U",
NORAD_CAT_ID: Number(row.norad_cat_id),
ELEMENT_SET_NO: Number(row.element_set_no ?? 0),
REV_AT_EPOCH: Number(row.rev_at_epoch ?? 0),
BSTAR: Number(row.bstar ?? 0),
MEAN_MOTION_DOT: Number(row.mean_motion_dot ?? 0),
MEAN_MOTION_DDOT: Number(row.mean_motion_ddot ?? 0),
});
}
return satellite.twoline2satrec(row.tle_line1, row.tle_line2);
}
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: createSatrec(r),
}));
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 || !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);
}
}());
}());