diff --git a/public/header.php b/public/header.php index 5dcbcc7..7b25786 100644 --- a/public/header.php +++ b/public/header.php @@ -117,6 +117,25 @@ if ($hasFullAdminAccess && $pdoHeader !== null) { $pendingApprovals = (int)$stmtPendingUsers->fetchColumn(); } +// Favoriten-Kometen des eingeloggten Users für den Sternenhimmel laden +$favCometsForCanvas = []; +if ($loggedIn && isset($_SESSION['user_id']) && $pdoHeader !== null) { + try { + $stmtFavComets = $pdoHeader->prepare( + 'SELECT c.designation_and_name + FROM app_user_comets uc + JOIN comets_mpc c ON c.id = uc.comet_id + WHERE uc.user_id = ? AND uc.is_favorite = 1 + ORDER BY c.designation_and_name + LIMIT 8' + ); + $stmtFavComets->execute([(int)$_SESSION['user_id']]); + $favCometsForCanvas = $stmtFavComets->fetchAll(PDO::FETCH_COLUMN); + } catch (Throwable $e) { + $favCometsForCanvas = []; + } +} + $menuGroups = [ [ 'title' => 'Himmel & Planeten', @@ -281,6 +300,109 @@ if ($loggedIn && !$hasFullAdminAccess && $pdoHeader !== null) { })(); + + + +
diff --git a/public/sternenhimmel.php b/public/sternenhimmel.php index 4b49e1a..e9991e8 100644 --- a/public/sternenhimmel.php +++ b/public/sternenhimmel.php @@ -186,6 +186,39 @@ try { ]; } } + + // Favoritenkometen des Users laden – Bahnelemente für JS-seitige Berechnung + $favCometData = []; + if ($loggedIn && isset($_SESSION['user_id'])) { + $favCometStmt = $planetariumPdo->prepare( + "SELECT c.designation_and_name, + c.year_of_perihelion, c.month_of_perihelion, c.day_of_perihelion, + c.perihelion_dist_au, c.eccentricity, + c.arg_perihelion_deg, c.ascending_node_deg, c.inclination_deg, + c.absolute_magnitude_h, c.slope_parameter_g + FROM app_user_comets uc + JOIN comets_mpc c ON c.id = uc.comet_id + WHERE uc.user_id = ? AND uc.is_favorite = 1 + ORDER BY c.designation_and_name + LIMIT 8" + ); + $favCometStmt->execute([(int)$_SESSION['user_id']]); + foreach ($favCometStmt->fetchAll() as $r) { + $favCometData[] = [ + 'name' => (string)$r['designation_and_name'], + 'q' => (float)$r['perihelion_dist_au'], + 'e' => (float)$r['eccentricity'], + 'w' => (float)$r['arg_perihelion_deg'], + 'Om' => (float)$r['ascending_node_deg'], + 'inc' => (float)$r['inclination_deg'], + 'Tp_y' => (int)$r['year_of_perihelion'], + 'Tp_m' => (int)$r['month_of_perihelion'], + 'Tp_d' => (float)$r['day_of_perihelion'], + 'H' => $r['absolute_magnitude_h'] !== null ? (float)$r['absolute_magnitude_h'] : null, + 'G' => $r['slope_parameter_g'] !== null ? (float)$r['slope_parameter_g'] : null, + ]; + } + } } catch (Throwable $e) { if ($starsApiRequested) { header('Content-Type: application/json; charset=utf-8'); @@ -201,6 +234,11 @@ try { $messierDataError = $e->getMessage(); } +$favCometDataJson = json_encode( + $favCometData ?? [], + JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES +); + $constellationDataJson = json_encode( $constellationData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES @@ -368,6 +406,16 @@ if (!$availableLocations) {
+ +
+ Fav.-Kometen +
+ + +
+
+ +
Sonne / Mond
@@ -550,6 +598,7 @@ let showSunMoon = true; let showEclip = true; let showMessier = true; let showConst = true; +let showComets = true; // Label-Sichtbarkeit (separat von Layer-Sichtbarkeit) let labelEq = true; @@ -559,6 +608,7 @@ let labelStars = true; let labelPlanets = true; let labelSunMoon = true; let labelMessier = true; +let labelComets = true; let showStars = true; // ═══════════════════════════════════════════════════════════════════════════════ @@ -569,6 +619,7 @@ const EMBEDDED_STAR_ERROR = ; const EMBEDDED_MESSIER_DATA = ; const EMBEDDED_MESSIER_ERROR = ; const MESSIER = Array.isArray(EMBEDDED_MESSIER_DATA) ? EMBEDDED_MESSIER_DATA : []; +const FAV_COMETS = ; let constellationData = EMBEDDED_CONSTELLATION_DATA; let hipMap = new Map(); @@ -1507,6 +1558,184 @@ function drawMessier(date) { } +// ── Kometenposition: Bahnmechanik (Port aus comets.py) ─────────────────────── +const COMET_K = 0.01720209895; // Gauß'sche Gravitationskonstante +const COMET_OBLIQ = 23.439279444444445 * Math.PI / 180; // J2000-Schiefe + +function cometSolveElliptic(M, e) { + let E = e < 0.8 ? M : (M >= 0 ? Math.PI : -Math.PI); + for (let i = 0; i < 30; i++) { + const d = (E - e * Math.sin(E) - M) / (1 - e * Math.cos(E)); + E -= d; + if (Math.abs(d) < 1e-12) break; + } + return E; +} + +function cometSolveHyperbolic(M, e) { + let F = M === 0 ? 0 : Math.asinh(M / e); + for (let i = 0; i < 40; i++) { + const d = (e * Math.sinh(F) - F - M) / (e * Math.cosh(F) - 1); + F -= d; + if (Math.abs(d) < 1e-12) break; + } + return F; +} + +function cometSolveParabolic(deltaDays, q) { + const s = COMET_K * deltaDays / Math.sqrt(2 * q * q * q); + let W = s; + for (let i = 0; i < 40; i++) { + const d = (W + W*W*W/3 - s) / (1 + W*W); + W -= d; + if (Math.abs(d) < 1e-12) break; + } + return W; +} + +function cometNuR(deltaDays, q, e) { + const TOL = 1e-6; + if (e < 1 - TOL) { + const a = q / (1 - e); + const n = COMET_K / Math.pow(a, 1.5); + const M = ((n * deltaDays) % (2 * Math.PI)); + const E = cometSolveElliptic(M, e); + const r = a * (1 - e * Math.cos(E)); + const nu = 2 * Math.atan2(Math.sqrt(1+e)*Math.sin(E/2), Math.sqrt(1-e)*Math.cos(E/2)); + return [nu, r]; + } + if (e > 1 + TOL) { + const a = q / (e - 1); + const M = COMET_K * deltaDays / Math.pow(a, 1.5); + const F = cometSolveHyperbolic(M, e); + const r = a * (e * Math.cosh(F) - 1); + const nu = 2 * Math.atan2(Math.sqrt(e+1)*Math.sinh(F/2), Math.sqrt(e-1)*Math.cosh(F/2)); + return [nu, r]; + } + const W = cometSolveParabolic(deltaDays, q); + return [2 * Math.atan(W), q * (1 + W*W)]; +} + +function cometRaDec(comet, date) { + // Periheldatum in ms + const dayFloor = Math.floor(comet.Tp_d); + const fracMs = (comet.Tp_d - dayFloor) * 86400000; + const tpMs = Date.UTC(comet.Tp_y, comet.Tp_m - 1, dayFloor) + fracMs; + const deltaDays = (date.getTime() - tpMs) / 86400000; + + const [nu, r] = cometNuR(deltaDays, comet.q, comet.e); + + const w = comet.w * Math.PI / 180; + const Om = comet.Om * Math.PI / 180; + const inc = comet.inc * Math.PI / 180; + const u = w + nu; + + const cOs = Math.cos(Om), sOs = Math.sin(Om); + const cI = Math.cos(inc), sI = Math.sin(inc); + const cU = Math.cos(u), sU = Math.sin(u); + + const xE = r * (cOs*cU - sOs*sU*cI); + const yE = r * (sOs*cU + cOs*sU*cI); + const zE = r * (sU * sI); + + // Ekliptik → Äquatorial (J2000) + const cosEps = Math.cos(COMET_OBLIQ), sinEps = Math.sin(COMET_OBLIQ); + const xEq = xE; + const yEq = yE*cosEps - zE*sinEps; + const zEq = yE*sinEps + zE*cosEps; + + // Erdvektor (Astronomy Engine) subtrahieren → geozentrisch + const astTime = Astronomy.MakeTime(date); + const earth = Astronomy.HelioVector(Astronomy.Body.Earth, astTime); + const gx = xEq - earth.x; + const gy = yEq - earth.y; + const gz = zEq - earth.z; + + const dist = Math.sqrt(gx*gx + gy*gy + gz*gz); + if (dist === 0) return null; + + let ra = Math.atan2(gy, gx) * (180 / Math.PI) / 15; + if (ra < 0) ra += 24; + const dec = Math.asin(gz / dist) * (180 / Math.PI); + + // Helligkeit schätzen (HG-Modell wie in comets.py) + let mag = null; + if (comet.H !== null && comet.G !== null) { + const rH = Math.sqrt(xEq*xEq + yEq*yEq + zEq*zEq); + if (rH > 0 && dist > 0) { + mag = comet.H + 5*Math.log10(dist) + 2.5*comet.G*Math.log10(rH); + } + } + + return { ra, dec, mag }; +} + +function drawComets(date) { + if (!FAV_COMETS || FAV_COMETS.length === 0) return; + for (const comet of FAV_COMETS) { + let pos; + try { pos = cometRaDec(comet, date); } catch(e) { comet._xy = null; continue; } + if (!pos) { comet._xy = null; continue; } + + const hor = eqHorizon(pos.ra, pos.dec, date); + if (hor.altitude < 0) { comet._xy = null; continue; } + const pt = project(hor.altitude, hor.azimuth); + if (!pt) { comet._xy = null; continue; } + + comet._xy = pt; + comet._pos = pos; + + // Schweifrichtung: vom Sonnenzentrum weg (Näherung über Sonnenazimut) + let tailAngle = -Math.PI * 0.65; + if (sunCache && Number.isFinite(sunCache.az)) { + const sunPt = project(sunCache.alt, sunCache.az); + if (sunPt) { + tailAngle = Math.atan2(pt.y - sunPt.y, pt.x - sunPt.x); + } + } + const tailLen = 22; + const tx = pt.x + Math.cos(tailAngle) * tailLen; + const ty = pt.y + Math.sin(tailAngle) * tailLen; + + const grd = ctx.createLinearGradient(pt.x, pt.y, tx, ty); + grd.addColorStop(0, 'rgba(201,168,76,0.80)'); + grd.addColorStop(0.5, 'rgba(240,217,144,0.35)'); + grd.addColorStop(1, 'rgba(201,168,76,0)'); + ctx.beginPath(); + ctx.moveTo(pt.x, pt.y); + ctx.lineTo(tx, ty); + ctx.strokeStyle = grd; + ctx.lineWidth = 2.5; + ctx.lineCap = 'round'; + ctx.stroke(); + + const comaR = 4; + const glow = createSafeRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, comaR * 2.5); + if (glow) { + glow.addColorStop(0, 'rgba(255,245,200,0.95)'); + glow.addColorStop(0.4, 'rgba(201,168,76,0.60)'); + glow.addColorStop(1, 'rgba(201,168,76,0)'); + ctx.beginPath(); + ctx.arc(pt.x, pt.y, comaR * 2.5, 0, Math.PI * 2); + ctx.fillStyle = glow; + ctx.fill(); + } + ctx.beginPath(); + ctx.arc(pt.x, pt.y, comaR * 0.7, 0, Math.PI * 2); + ctx.fillStyle = 'rgba(255,250,220,0.95)'; + ctx.fill(); + + if (labelComets) { + const magStr = pos.mag !== null && isFinite(pos.mag) ? ' (' + pos.mag.toFixed(1) + 'mag)' : ''; + ctx.font = '10px Rajdhani,sans-serif'; + ctx.fillStyle = 'rgba(240,217,144,0.90)'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + ctx.fillText(comet.name + magStr, pt.x + comaR * 2.5 + 4, pt.y - 6); + } + } +} + function render() { const date = getTime(); ctx.clearRect(0, 0, W, H); @@ -1596,6 +1825,9 @@ function render() { } } + // ── Favoriten-Kometen ──────────────────────────────────────────────────── + if (showComets) drawComets(date); + ctx.restore(); // Ende Clip // Horizontring @@ -1858,6 +2090,8 @@ document.querySelectorAll('input[name="gridMode"]').forEach(radio => { spBind('cfgEqLabels', () => labelEq, v => { labelEq = v; labelAltaz = v; }); spBind('cfgConstShow', () => showConst, v => showConst = v); spBind('cfgPlanetsShow', () => showPlanets, v => showPlanets = v); +spBind('cfgCometsShow', () => showComets, v => showComets = v); +spBind('cfgCometsLabels', () => labelComets, v => labelComets = v); spBind('cfgEclipShow', () => showEclip, v => showEclip = v); spBind('cfgMessierShow', () => showMessier, v => showMessier = v); spBind('cfgSunMoonShow', () => showSunMoon, v => showSunMoon = v); @@ -2147,6 +2381,26 @@ canvas.addEventListener('click', function(e) { tip.className = 'visible'; positionTooltip(e); return; } + // Favoriten-Kometen + if (showComets) { + for (const comet of FAV_COMETS) { + if (!comet._xy) continue; + if (Math.hypot(mx - comet._xy.x, my - comet._xy.y) < 14) { + const pos = comet._pos || {}; + const hor = eqHorizon(pos.ra ?? 0, pos.dec ?? 0, getTime()); + const magStr = pos.mag !== null && pos.mag !== undefined && isFinite(pos.mag) ? pos.mag.toFixed(1) : '–'; + tip.innerHTML = + '☄ ' + comet.name + '' + + 'Mag: ' + magStr + '
' + + 'Alt: ' + hor.altitude.toFixed(2) + '°
' + + 'Az: ' + hor.azimuth.toFixed(2) + '°
' + + 'RA: ' + (pos.ra ?? 0).toFixed(4) + ' h
' + + 'Dec: ' + (pos.dec ?? 0).toFixed(3) + '°'; + tip.className = 'visible'; positionTooltip(e); return; + } + } + } + // Messier-Objekte if (showMessier) { for (const obj of MESSIER) {