/** * 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); } }()); }());