Fav Komet im Sternenhimmel angezeigt
This commit is contained in:
@@ -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) {
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php if (!empty($favCometsForCanvas)): ?>
|
||||
<script>
|
||||
(function () {
|
||||
const COMET_NAMES = <?= json_encode(array_values($favCometsForCanvas), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||
if (!COMET_NAMES.length) return;
|
||||
|
||||
const canvas = document.getElementById('starfield');
|
||||
// Warten bis das Sternfeld-Canvas bereit ist
|
||||
function initComets() {
|
||||
if (!canvas.width) { setTimeout(initComets, 100); return; }
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Jeden Kometen mit Startposition und Geschwindigkeit initialisieren
|
||||
const comets = COMET_NAMES.map(function (name, i) {
|
||||
return makeComet(name, i, COMET_NAMES.length, canvas.width, canvas.height);
|
||||
});
|
||||
|
||||
function makeComet(name, index, total, w, h) {
|
||||
// Kometen gleichmäßig über die Höhe verteilt, aber mit leichtem Versatz
|
||||
const seedY = (index + 0.5) / total;
|
||||
return {
|
||||
name: name,
|
||||
x: Math.random() * w,
|
||||
y: seedY * h * 0.85 + h * 0.05,
|
||||
// Langsame Bewegung leicht schräg nach rechts-unten
|
||||
vx: 0.12 + Math.random() * 0.10,
|
||||
vy: (Math.random() - 0.5) * 0.04,
|
||||
// Schweiflänge und Coma-Größe
|
||||
tailLen: 60 + Math.random() * 50,
|
||||
comaR: 2.5 + Math.random() * 1.5,
|
||||
// Leichtes Funkeln
|
||||
phase: Math.random() * Math.PI * 2,
|
||||
speed: 0.0004 + Math.random() * 0.0003,
|
||||
};
|
||||
}
|
||||
|
||||
let t = 0;
|
||||
function drawComets() {
|
||||
t++;
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
|
||||
for (const c of comets) {
|
||||
c.x += c.vx;
|
||||
c.y += c.vy;
|
||||
|
||||
// Wenn rechts raus → links neu eintreten, y leicht variieren
|
||||
if (c.x - c.tailLen > w) {
|
||||
c.x = -c.tailLen;
|
||||
c.y = Math.random() * h * 0.85 + h * 0.05;
|
||||
}
|
||||
|
||||
const alpha = 0.55 + 0.18 * Math.sin(c.phase + t * c.speed * Math.PI * 2);
|
||||
|
||||
// --- Schweif (Gradient nach links) ---
|
||||
const grd = ctx.createLinearGradient(c.x, c.y, c.x - c.tailLen, c.y);
|
||||
grd.addColorStop(0, `rgba(201,168,76,${(alpha * 0.7).toFixed(3)})`);
|
||||
grd.addColorStop(0.4, `rgba(240,217,144,${(alpha * 0.25).toFixed(3)})`);
|
||||
grd.addColorStop(1, 'rgba(201,168,76,0)');
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(c.x, c.y);
|
||||
ctx.lineTo(c.x - c.tailLen, c.y - c.tailLen * 0.06);
|
||||
ctx.strokeStyle = grd;
|
||||
ctx.lineWidth = c.comaR * 0.9;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.stroke();
|
||||
|
||||
// --- Coma (Leuchtkern) ---
|
||||
const glow = ctx.createRadialGradient(c.x, c.y, 0, c.x, c.y, c.comaR * 3);
|
||||
glow.addColorStop(0, `rgba(255,245,200,${alpha.toFixed(3)})`);
|
||||
glow.addColorStop(0.4, `rgba(201,168,76,${(alpha * 0.6).toFixed(3)})`);
|
||||
glow.addColorStop(1, 'rgba(201,168,76,0)');
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(c.x, c.y, c.comaR * 3, 0, Math.PI * 2);
|
||||
ctx.fillStyle = glow;
|
||||
ctx.fill();
|
||||
|
||||
// --- Namens-Label ---
|
||||
ctx.font = '11px sans-serif';
|
||||
ctx.fillStyle = `rgba(240,217,144,${(alpha * 0.85).toFixed(3)})`;
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(c.name, c.x + c.comaR * 3 + 4, c.y - 8);
|
||||
}
|
||||
}
|
||||
|
||||
// In den bestehenden Animation-Loop einhängen
|
||||
const origRAF = window.requestAnimationFrame;
|
||||
(function loop() {
|
||||
drawComets();
|
||||
origRAF(loop);
|
||||
})();
|
||||
}
|
||||
|
||||
// Kurze Verzögerung damit der Stern-Canvas zuerst initialisiert wird
|
||||
setTimeout(initComets, 200);
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<header>
|
||||
<div class="container">
|
||||
<div class="header-inner">
|
||||
|
||||
@@ -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) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($favCometData)): ?>
|
||||
<div class="sp-row">
|
||||
<span class="sp-label">Fav.-Kometen</span>
|
||||
<div class="sp-toggles">
|
||||
<label class="sp-toggle"><input type="checkbox" id="cfgCometsShow" checked><span class="sp-pill"></span><span class="sp-text">Objekte</span></label>
|
||||
<label class="sp-toggle"><input type="checkbox" id="cfgCometsLabels" checked><span class="sp-pill"></span><span class="sp-text">Namen</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="sp-row">
|
||||
<span class="sp-label">Sonne / Mond</span>
|
||||
<div class="sp-toggles">
|
||||
@@ -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 = <?php echo $starDataErrorJson ?: 'null'; ?>;
|
||||
const EMBEDDED_MESSIER_DATA = <?php echo $messierDataJson ?: '[]'; ?>;
|
||||
const EMBEDDED_MESSIER_ERROR = <?php echo $messierDataErrorJson ?: 'null'; ?>;
|
||||
const MESSIER = Array.isArray(EMBEDDED_MESSIER_DATA) ? EMBEDDED_MESSIER_DATA : [];
|
||||
const FAV_COMETS = <?php echo $favCometDataJson ?: '[]'; ?>;
|
||||
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 =
|
||||
'<span class="tt-name tt-planet" style="color:#f0d990">☄ ' + comet.name + '</span>' +
|
||||
'Mag: ' + magStr + '<br>' +
|
||||
'Alt: ' + hor.altitude.toFixed(2) + '°<br>' +
|
||||
'Az: ' + hor.azimuth.toFixed(2) + '°<br>' +
|
||||
'RA: ' + (pos.ra ?? 0).toFixed(4) + ' h<br>' +
|
||||
'Dec: ' + (pos.dec ?? 0).toFixed(3) + '°';
|
||||
tip.className = 'visible'; positionTooltip(e); return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Messier-Objekte
|
||||
if (showMessier) {
|
||||
for (const obj of MESSIER) {
|
||||
|
||||
Reference in New Issue
Block a user