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