1691 lines
63 KiB
PHP
1691 lines
63 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
ini_set('display_errors', '1');
|
||
error_reporting(E_ALL);
|
||
session_start();
|
||
|
||
$loggedIn = isset($_SESSION['user_id']);
|
||
$isAdmin = $loggedIn && (($_SESSION['role'] ?? '') === 'admin');
|
||
|
||
// AJAX-Handler: GRS-Kalibrierung lesen/speichern
|
||
if (isset($_GET['grs_api'])) {
|
||
$dataFile = __DIR__ . '/../config/jupiter_grs.json';
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||
echo file_exists($dataFile)
|
||
? file_get_contents($dataFile)
|
||
: json_encode(['refDate' => null, 'refAngle' => 0, 'calibratedAt' => null, 'calibratedBy' => null]);
|
||
exit;
|
||
}
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
if (!$isAdmin) { http_response_code(403); echo json_encode(['error' => 'Nicht autorisiert']); exit; }
|
||
$body = json_decode(file_get_contents('php://input'), true);
|
||
if (!$body || !isset($body['refDate'], $body['refAngle'])) { http_response_code(400); echo json_encode(['error' => 'Ungültige Daten']); exit; }
|
||
$data = [
|
||
'refDate' => (string) $body['refDate'],
|
||
'refAngle' => (float) $body['refAngle'],
|
||
'calibratedAt' => date('c'),
|
||
'calibratedBy' => $_SESSION['username'] ?? 'admin',
|
||
];
|
||
$written = file_put_contents($dataFile, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||
echo $written !== false ? json_encode(['ok' => true]) : json_encode(['error' => 'Schreibfehler – Dateiberechtigung prüfen']);
|
||
exit;
|
||
}
|
||
http_response_code(405); echo json_encode(['error' => 'Methode nicht erlaubt']); exit;
|
||
}
|
||
|
||
$pageTitle = 'Jupitersystem - AstroTools';
|
||
$bodyClass = 'solarsystem-page jupitersystem-page';
|
||
?>
|
||
<?php require __DIR__ . '/header.php'; ?>
|
||
|
||
<script src="js/astronomy.browser.min.js"></script>
|
||
|
||
<div class="solarsystem-tool">
|
||
<div class="solarsystem-layout">
|
||
<aside class="card solarsystem-sidebar">
|
||
<div class="solarsystem-sidebar-block">
|
||
<h2>Jupitersystem</h2>
|
||
</div>
|
||
|
||
<div class="solarsystem-sidebar-block">
|
||
<div class="solarsystem-label">Datum</div>
|
||
<div class="solarsystem-input-row">
|
||
<input type="date" id="jupiterDatePicker">
|
||
<button type="button" class="btn btn-secondary" id="jupiterBtnToday">Heute</button>
|
||
</div>
|
||
<div class="solarsystem-current-date" id="jupiterCurrentDate">--</div>
|
||
</div>
|
||
|
||
<div class="solarsystem-sidebar-block">
|
||
<div class="solarsystem-label">Wiedergabe</div>
|
||
<div class="solarsystem-button-grid">
|
||
<button type="button" class="btn btn-primary" id="jupiterBtnPlay">Play</button>
|
||
<button type="button" class="btn btn-secondary" id="jupiterBtnReset">Jetzt</button>
|
||
<button type="button" class="btn btn-secondary" id="jupiterBtnMinus5">−5 min</button>
|
||
<button type="button" class="btn btn-secondary" id="jupiterBtnPlus5">+5 min</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="solarsystem-sidebar-block">
|
||
<div class="solarsystem-label">Geschwindigkeit</div>
|
||
<input type="range" id="jupiterSpeedSlider" min="-12" max="12" step="0.01" value="1" list="jupiterSpeedTicks">
|
||
<datalist id="jupiterSpeedTicks">
|
||
<option value="-12"></option>
|
||
<option value="-6"></option>
|
||
<option value="0"></option>
|
||
<option value="6"></option>
|
||
<option value="12"></option>
|
||
</datalist>
|
||
<div class="jupiter-speed-scale">
|
||
<span>−12h</span><span>−6h</span><span>0</span><span>+6h</span><span>+12h</span>
|
||
</div>
|
||
<div class="solarsystem-speed-value" id="jupiterSpeedLabel">1.0 h/s</div>
|
||
<button type="button" class="btn btn-sm btn-secondary mt-md" id="jupiterBtnRealtime">Echtzeit</button>
|
||
</div>
|
||
|
||
<div class="solarsystem-sidebar-block">
|
||
<div class="solarsystem-label">Monatsübersicht</div>
|
||
<button type="button" class="btn btn-secondary btn-full" id="jupiterBtnEphemeris">Mondgrafik erzeugen</button>
|
||
</div>
|
||
|
||
<div class="solarsystem-sidebar-block">
|
||
<div class="solarsystem-label">Ansicht</div>
|
||
<div class="solarsystem-button-grid">
|
||
<button type="button" class="btn btn-secondary" id="jupiterBtnTop">Top</button>
|
||
<button type="button" class="btn btn-secondary" id="jupiterBtnSide">Seite</button>
|
||
<button type="button" class="btn btn-secondary" id="jupiterBtnEarth">Erde</button>
|
||
<button type="button" class="btn btn-secondary is-active" id="jupiterBtnOrbits">Orbits</button>
|
||
<button type="button" class="btn btn-secondary" id="jupiterBtnLabels">Namen</button>
|
||
<button type="button" class="btn btn-secondary" id="jupiterBtnInnerMoons">Zusatzmonde</button>
|
||
</div>
|
||
<label class="form-checkbox mt-sm" title="Vergrößert nur die Darstellung der Monde. Der Schatten auf Jupiter bleibt in der realen Größe.">
|
||
<input type="checkbox" id="jupiterChkMoonScale">
|
||
Monde 4× vergrößert
|
||
</label>
|
||
</div>
|
||
|
||
<div class="solarsystem-sidebar-block">
|
||
<div class="solarsystem-label">Hinweise</div>
|
||
<ul class="solarsystem-notes">
|
||
<li>Maus ziehen: Kamera drehen</li>
|
||
<li>Mausrad: Zoom</li>
|
||
<li>Klick auf Jupiter oder Mond: Fokus setzen</li>
|
||
<li>Erneuter Klick ins Leere: Fokus loesen</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<?php if ($isAdmin): ?>
|
||
<div class="solarsystem-sidebar-block">
|
||
<details id="grsAdminPanel">
|
||
<summary class="solarsystem-label summary-buttonless">▼ GRS Kalibrierung</summary>
|
||
<div class="grs-admin-body">
|
||
<div class="solarsystem-label mt-sm">GRS Transit-Zeitpunkt (UTC)</div>
|
||
<p class="hint mt-2xs"><a href="https://skyandtelescope.org/stargazing-and-observing/interactive-stargazing-tools/transit-times-of-jupiters-great-red-spot/" target="_blank" rel="noopener">↗ Sky&Telescope: GRS-Transitzeiten</a></p>
|
||
<input type="text" id="grsTransitInput" class="w-full mt-2xs" placeholder="03/27/2026 @ 05:00 UT">
|
||
<button class="btn btn-secondary btn-full mt-xs" id="grsBtnSetTime">Auf diese Zeit setzen</button>
|
||
<div id="grsParseError" class="msg-error mt-2xs is-hidden"></div>
|
||
|
||
<div class="solarsystem-label mt-md">Jupiter drehen</div>
|
||
<div class="solarsystem-button-grid mt-2xs">
|
||
<button class="btn btn-secondary" id="grsBtnRot5L">◀◀ 5°</button>
|
||
<button class="btn btn-secondary" id="grsBtnRot1L">◀ 1°</button>
|
||
<button class="btn btn-secondary" id="grsBtnRot1R">1° ▶</button>
|
||
<button class="btn btn-secondary" id="grsBtnRot5R">5° ▶▶</button>
|
||
</div>
|
||
|
||
<button class="btn btn-secondary btn-full mt-md" id="grsBtnShadowRays">Schattenstrahlen anzeigen</button>
|
||
<button class="btn btn-primary btn-full mt-xs" id="grsBtnSave">Kalibrierung speichern</button>
|
||
<div id="grsCalibStatus" class="hint inline-fade mt-sm"></div>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
<?php endif; ?>
|
||
</aside>
|
||
|
||
<section class="card solarsystem-stage-card">
|
||
<div class="solarsystem-stage-wrap" id="jupiterStageWrap">
|
||
<canvas id="jupiterCanvas"></canvas>
|
||
<div id="jupiterLabels" class="solarsystem-label-layer"></div>
|
||
|
||
<div class="solarsystem-info-panel" id="jupiterInfoPanel" hidden>
|
||
<h3 id="jupiterInfoName">--</h3>
|
||
<div class="solarsystem-info-row"><span>Abstand Jupiter</span><strong id="jupiterInfoDistanceRj">--</strong></div>
|
||
<div class="solarsystem-info-row"><span>Abstand km</span><strong id="jupiterInfoDistanceKm">--</strong></div>
|
||
<div class="solarsystem-info-row"><span>Umlaufzeit</span><strong id="jupiterInfoPeriod">--</strong></div>
|
||
<div class="solarsystem-info-row"><span>Helligkeit</span><strong id="jupiterInfoMagnitude">--</strong></div>
|
||
<div class="solarsystem-info-row"><span>Typ</span><strong id="jupiterInfoType">--</strong></div>
|
||
</div>
|
||
|
||
<div class="solarsystem-overlay" id="jupiterOverlay">Initialisiere Visualisierung...</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
|
||
<script type="module">
|
||
const AstronomyEngine = window.Astronomy;
|
||
const AU_KM = 149597870.7;
|
||
const JUPITER_RADIUS_KM = AstronomyEngine.JUPITER_EQUATORIAL_RADIUS_KM;
|
||
const JUPITER_DISPLAY_RADIUS = 5.8;
|
||
// Positions from JupiterMoons() are in AU → convert to display units
|
||
// so that 1 Jupiter radius = JUPITER_DISPLAY_RADIUS display units
|
||
const MOON_DISTANCE_SCALE = (JUPITER_DISPLAY_RADIUS / JUPITER_RADIUS_KM) * AU_KM;
|
||
const KM_TO_DISPLAY = JUPITER_DISPLAY_RADIUS / JUPITER_RADIUS_KM;
|
||
// Jupiter System III (Referenz): 9h 55m 29.7s — System II (GRS): 9h 55m 40.632s
|
||
const GRS_SYSTEM_II_PERIOD_DAYS = 35740.632 / 86400;
|
||
|
||
// GRS-Kalibrierung: Referenzzeitpunkt und Rotationswinkel bei Transit
|
||
let grsRefDate = new Date(0);
|
||
let grsRefAngle = 0;
|
||
|
||
const MOONS = [
|
||
{ key: 'io', label: 'Io', color: '#f4d16b', orbitalPeriodDays: 1.769, radiusKm: AstronomyEngine.IO_RADIUS_KM, texture: 'images/moons/io.jpg', type: 'Galileischer Mond' },
|
||
{ key: 'europa', label: 'Europa', color: '#d8c7a2', orbitalPeriodDays: 3.551, radiusKm: AstronomyEngine.EUROPA_RADIUS_KM, texture: 'images/moons/europa.jpg', type: 'Galileischer Mond' },
|
||
{ key: 'ganymede', label: 'Ganymed', color: '#9f8f7c', orbitalPeriodDays: 7.155, radiusKm: AstronomyEngine.GANYMEDE_RADIUS_KM, texture: 'images/moons/ganymede.jpg', type: 'Galileischer Mond' },
|
||
{ key: 'callisto', label: 'Kallisto', color: '#7f7164', orbitalPeriodDays: 16.689, radiusKm: AstronomyEngine.CALLISTO_RADIUS_KM, texture: 'images/moons/callisto.jpg', type: 'Galileischer Mond' }
|
||
].map(moon => ({ ...moon, size: moon.radiusKm * KM_TO_DISPLAY }));
|
||
|
||
// Innere Monde — Keplerbahnen, IAU-Elemente (Epoche J2000)
|
||
const INNER_MOON_EPOCH = new Date('2000-01-01T12:00:00Z');
|
||
const INNER_MOON_MIN_RADIUS = 0.08; // Mindest-Anzeigeradius für Sichtbarkeit
|
||
const INNER_MOONS = [
|
||
{ key: 'metis', label: 'Metis', color: '#9a9a9a', semiMajorAxisKm: 127690, eccentricity: 0.0002, inclinationDeg: 0.06, orbitalPeriodDays: 0.29478, radiusKm: 21.5, M0deg: 0, type: 'Innerer Mond' },
|
||
{ key: 'amalthea', label: 'Amalthea', color: '#b03010', semiMajorAxisKm: 181366, eccentricity: 0.0032, inclinationDeg: 0.374, orbitalPeriodDays: 0.49818, radiusKm: 83.5, M0deg: 120, type: 'Innerer Mond' },
|
||
{ key: 'thebe', label: 'Thebe', color: '#787878', semiMajorAxisKm: 221900, eccentricity: 0.0177, inclinationDeg: 1.076, orbitalPeriodDays: 0.6745, radiusKm: 49.3, M0deg: 0, type: 'Innerer Mond' },
|
||
].map(m => ({ ...m, size: Math.max(m.radiusKm * KM_TO_DISPLAY, INNER_MOON_MIN_RADIUS) }));
|
||
|
||
const stageWrap = document.getElementById('jupiterStageWrap');
|
||
const canvas = document.getElementById('jupiterCanvas');
|
||
const labelsLayer = document.getElementById('jupiterLabels');
|
||
const overlay = document.getElementById('jupiterOverlay');
|
||
const datePicker = document.getElementById('jupiterDatePicker');
|
||
const currentDateLabel = document.getElementById('jupiterCurrentDate');
|
||
const btnToday = document.getElementById('jupiterBtnToday');
|
||
const btnPlay = document.getElementById('jupiterBtnPlay');
|
||
const btnReset = document.getElementById('jupiterBtnReset');
|
||
const btnTop = document.getElementById('jupiterBtnTop');
|
||
const btnSide = document.getElementById('jupiterBtnSide');
|
||
const btnEarth = document.getElementById('jupiterBtnEarth');
|
||
const btnOrbits = document.getElementById('jupiterBtnOrbits');
|
||
const btnLabels = document.getElementById('jupiterBtnLabels');
|
||
const btnInnerMoons = document.getElementById('jupiterBtnInnerMoons');
|
||
const chkMoonScale = document.getElementById('jupiterChkMoonScale');
|
||
const speedSlider = document.getElementById('jupiterSpeedSlider');
|
||
const speedLabel = document.getElementById('jupiterSpeedLabel');
|
||
const infoPanel = document.getElementById('jupiterInfoPanel');
|
||
|
||
let THREE;
|
||
let rotationEqjToEcl;
|
||
let renderer;
|
||
let scene;
|
||
let camera;
|
||
let textureLoader;
|
||
let moonGroups;
|
||
let moonShadowCasters;
|
||
let moonOrbitLines;
|
||
let innerMoonGroups;
|
||
let innerMoonShadowCasters;
|
||
let innerMoonOrbitLines;
|
||
let labelElements;
|
||
let jupiterGroup;
|
||
let sunLight;
|
||
let meridianLine;
|
||
let shadowRays = [];
|
||
let showShadowRays = false;
|
||
let raycaster;
|
||
let pointer;
|
||
|
||
let showOrbits = true;
|
||
let showLabels = false;
|
||
let moonScaleEnlarged = false;
|
||
let showInnerMoons = false;
|
||
let playing = true;
|
||
let speedDaysPerSecond = 1 / 24;
|
||
let focusedObjectKey = null;
|
||
let simDate = new Date();
|
||
let lastAnimationTime = performance.now();
|
||
let cameraTarget;
|
||
let camTheta = 0.45;
|
||
let camPhi = 1.0;
|
||
let camRadius = 95;
|
||
let camRadiusTarget = 95;
|
||
let isDragging = false;
|
||
let pointerDownMoved = false;
|
||
let lastPointerX = 0;
|
||
let lastPointerY = 0;
|
||
|
||
if (!AstronomyEngine) {
|
||
overlay.textContent = 'Astronomy Engine konnte nicht geladen werden.';
|
||
throw new Error('Astronomy Engine unavailable');
|
||
}
|
||
|
||
function formatDateInput(date) {
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
return `${year}-${month}-${day}`;
|
||
}
|
||
|
||
function formatDisplayDate(date) {
|
||
return date.toLocaleString('de-DE', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit'
|
||
});
|
||
}
|
||
|
||
function normalizedSpeedText(daysPerSecond) {
|
||
const totalMinutes = daysPerSecond * 24 * 60;
|
||
if (Math.abs(totalMinutes) < 0.5) {
|
||
return '0 min/s';
|
||
}
|
||
const sign = totalMinutes < 0 ? '−' : '';
|
||
const abs = Math.abs(totalMinutes);
|
||
if (abs < 60) {
|
||
return `${sign}${Math.round(abs)} min/s`;
|
||
}
|
||
const h = Math.floor(abs / 60);
|
||
const m = Math.round(abs % 60);
|
||
return m > 0 ? `${sign}${h} h ${m} min/s` : `${sign}${h} h/s`;
|
||
}
|
||
|
||
function updateSpeedFromSlider() {
|
||
speedDaysPerSecond = Number(speedSlider.value) / 24;
|
||
speedLabel.textContent = normalizedSpeedText(speedDaysPerSecond);
|
||
}
|
||
|
||
function refreshCurrentDateUi() {
|
||
currentDateLabel.textContent = formatDisplayDate(simDate);
|
||
datePicker.value = formatDateInput(simDate);
|
||
}
|
||
|
||
function resizeRenderer() {
|
||
const width = stageWrap.clientWidth;
|
||
const height = Math.max(520, Math.min(Math.round(width * 0.62), Math.round(window.innerHeight * 0.78)));
|
||
stageWrap.style.height = `${height}px`;
|
||
renderer.setSize(width, height, false);
|
||
camera.aspect = width / height;
|
||
camera.updateProjectionMatrix();
|
||
}
|
||
|
||
function updateCamera() {
|
||
const sinPhi = Math.sin(camPhi);
|
||
const x = camRadius * sinPhi * Math.sin(camTheta);
|
||
const y = camRadius * Math.cos(camPhi);
|
||
const z = camRadius * sinPhi * Math.cos(camTheta);
|
||
|
||
camera.position.set(
|
||
cameraTarget.x + x,
|
||
cameraTarget.y + y,
|
||
cameraTarget.z + z
|
||
);
|
||
camera.lookAt(cameraTarget);
|
||
}
|
||
|
||
function getMoonMeta(key) {
|
||
return MOONS.find((moon) => moon.key === key) || null;
|
||
}
|
||
|
||
function getJupiterMoonStates(date) {
|
||
return AstronomyEngine.JupiterMoons(date);
|
||
}
|
||
|
||
function toDisplayPosition(state) {
|
||
const ecl = rotationEqjToEcl
|
||
? AstronomyEngine.RotateVector(rotationEqjToEcl, state)
|
||
: state;
|
||
return new THREE.Vector3(
|
||
ecl.x * MOON_DISTANCE_SCALE,
|
||
ecl.z * MOON_DISTANCE_SCALE,
|
||
-ecl.y * MOON_DISTANCE_SCALE
|
||
);
|
||
}
|
||
|
||
function getMoonDistanceFromJupiter(state) {
|
||
const distAu = Math.hypot(state.x, state.y, state.z);
|
||
const distKm = distAu * AU_KM;
|
||
return {
|
||
rj: distKm / JUPITER_RADIUS_KM,
|
||
km: distKm,
|
||
};
|
||
}
|
||
|
||
function getJupiterMagnitude(date) {
|
||
try {
|
||
const illumination = AstronomyEngine.Illumination(AstronomyEngine.Body.Jupiter, date);
|
||
return illumination && Number.isFinite(illumination.mag) ? illumination.mag : null;
|
||
} catch (error) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function loadTexture(path, onLoaded, onError) {
|
||
return textureLoader.load(
|
||
path,
|
||
(loadedTexture) => {
|
||
loadedTexture.colorSpace = THREE.SRGBColorSpace;
|
||
loadedTexture.anisotropy = Math.min(renderer.capabilities.getMaxAnisotropy(), 8);
|
||
loadedTexture.needsUpdate = true;
|
||
if (typeof onLoaded === 'function') {
|
||
onLoaded(loadedTexture);
|
||
}
|
||
},
|
||
undefined,
|
||
(error) => {
|
||
console.warn(`Textur konnte nicht geladen werden: ${path}`);
|
||
if (typeof onError === 'function') {
|
||
onError(error);
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|
||
function updateInfoPanel(key) {
|
||
if (key === 'jupiter') {
|
||
const magnitude = getJupiterMagnitude(simDate);
|
||
infoPanel.hidden = false;
|
||
document.getElementById('jupiterInfoName').textContent = 'Jupiter';
|
||
document.getElementById('jupiterInfoName').style.color = '#f0cf8d';
|
||
document.getElementById('jupiterInfoDistanceRj').textContent = '0.0 Rj';
|
||
document.getElementById('jupiterInfoDistanceKm').textContent = '0 km';
|
||
document.getElementById('jupiterInfoPeriod').textContent = `${(AstronomyEngine.PlanetOrbitalPeriod(AstronomyEngine.Body.Jupiter) / 365.25).toFixed(2)} Jahre`;
|
||
document.getElementById('jupiterInfoMagnitude').textContent = magnitude === null ? '--' : `${magnitude.toFixed(1)} mag`;
|
||
document.getElementById('jupiterInfoType').textContent = 'Gasriese';
|
||
return;
|
||
}
|
||
|
||
const moon = getMoonMeta(key);
|
||
if (!moon) {
|
||
infoPanel.hidden = true;
|
||
return;
|
||
}
|
||
|
||
const state = getJupiterMoonStates(simDate)[moon.key];
|
||
const distance = getMoonDistanceFromJupiter(state);
|
||
infoPanel.hidden = false;
|
||
document.getElementById('jupiterInfoName').textContent = moon.label;
|
||
document.getElementById('jupiterInfoName').style.color = moon.color;
|
||
document.getElementById('jupiterInfoDistanceRj').textContent = `${distance.rj.toFixed(2)} Rj`;
|
||
document.getElementById('jupiterInfoDistanceKm').textContent = `${distance.km.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, '.')} km`;
|
||
document.getElementById('jupiterInfoPeriod').textContent = `${moon.orbitalPeriodDays.toFixed(3)} Tage`;
|
||
document.getElementById('jupiterInfoMagnitude').textContent = '--';
|
||
document.getElementById('jupiterInfoType').textContent = moon.type;
|
||
}
|
||
|
||
function clearFocus() {
|
||
focusedObjectKey = null;
|
||
cameraTarget.set(0, 0, 0);
|
||
infoPanel.hidden = true;
|
||
camRadiusTarget = camRadius;
|
||
updateCamera();
|
||
}
|
||
|
||
function buildLabels() {
|
||
const objects = [{ key: 'jupiter', label: 'Jupiter', color: '#f0cf8d' }, ...MOONS, ...INNER_MOONS];
|
||
objects.forEach((entry) => {
|
||
const label = document.createElement('div');
|
||
label.className = 'solarsystem-planet-label';
|
||
label.textContent = entry.label;
|
||
label.style.color = entry.color;
|
||
label.hidden = true;
|
||
labelsLayer.appendChild(label);
|
||
labelElements.set(entry.key, label);
|
||
});
|
||
}
|
||
|
||
function createMoonOrbitLine(moon) {
|
||
const steps = 180;
|
||
const positions = [];
|
||
const referenceDate = new Date(simDate);
|
||
|
||
for (let step = 0; step <= steps; step += 1) {
|
||
const sampleDate = new Date(referenceDate.getTime() + ((moon.orbitalPeriodDays * step) / steps) * 86400000);
|
||
const moonState = getJupiterMoonStates(sampleDate)[moon.key];
|
||
const display = toDisplayPosition(moonState);
|
||
positions.push(display.x, display.y, display.z);
|
||
}
|
||
|
||
const geometry = new THREE.BufferGeometry();
|
||
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||
const material = new THREE.LineBasicMaterial({
|
||
color: new THREE.Color(moon.color).multiplyScalar(0.9),
|
||
transparent: true,
|
||
opacity: 0.55,
|
||
fog: false,
|
||
});
|
||
const line = new THREE.Line(geometry, material);
|
||
line.visible = showOrbits;
|
||
moonOrbitLines.set(moon.key, line);
|
||
scene.add(line);
|
||
}
|
||
|
||
function rebuildMoonOrbitLine(moon) {
|
||
const line = moonOrbitLines.get(moon.key);
|
||
if (!line) {
|
||
return;
|
||
}
|
||
|
||
const steps = 180;
|
||
const positions = [];
|
||
const referenceDate = new Date(simDate);
|
||
|
||
for (let step = 0; step <= steps; step += 1) {
|
||
const sampleDate = new Date(referenceDate.getTime() + ((moon.orbitalPeriodDays * step) / steps) * 86400000);
|
||
const moonState = getJupiterMoonStates(sampleDate)[moon.key];
|
||
const display = toDisplayPosition(moonState);
|
||
positions.push(display.x, display.y, display.z);
|
||
}
|
||
|
||
line.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||
line.geometry.attributes.position.needsUpdate = true;
|
||
}
|
||
|
||
// ── Kepler-Solver für innere Monde ──────────────────────────────────────────
|
||
|
||
function solveKepler(M, e) {
|
||
let E = M % (2 * Math.PI);
|
||
for (let i = 0; i < 10; i++) {
|
||
E -= (E - e * Math.sin(E) - M) / (1 - e * Math.cos(E));
|
||
}
|
||
return E;
|
||
}
|
||
|
||
function innerMoonPosition(moon, date) {
|
||
const tDays = (date - INNER_MOON_EPOCH) / 86400000;
|
||
const M = (moon.M0deg * Math.PI / 180) + (2 * Math.PI / moon.orbitalPeriodDays) * tDays;
|
||
const E = solveKepler(M, moon.eccentricity);
|
||
const nu = 2 * Math.atan2(
|
||
Math.sqrt(1 + moon.eccentricity) * Math.sin(E / 2),
|
||
Math.sqrt(1 - moon.eccentricity) * Math.cos(E / 2)
|
||
);
|
||
const r = moon.semiMajorAxisKm * (1 - moon.eccentricity * Math.cos(E));
|
||
const i = moon.inclinationDeg * Math.PI / 180;
|
||
const x = r * Math.cos(nu);
|
||
const yOrb = r * Math.sin(nu);
|
||
return new THREE.Vector3(
|
||
x * KM_TO_DISPLAY,
|
||
yOrb * Math.sin(i) * KM_TO_DISPLAY,
|
||
-yOrb * Math.cos(i) * KM_TO_DISPLAY
|
||
);
|
||
}
|
||
|
||
function createInnerMoonOrbitLine(moon) {
|
||
const steps = 120;
|
||
const positions = [];
|
||
for (let s = 0; s <= steps; s++) {
|
||
const sampleDate = new Date(simDate.getTime() + (moon.orbitalPeriodDays * s / steps) * 86400000);
|
||
const p = innerMoonPosition(moon, sampleDate);
|
||
positions.push(p.x, p.y, p.z);
|
||
}
|
||
const geo = new THREE.BufferGeometry();
|
||
geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||
const line = new THREE.Line(geo, new THREE.LineBasicMaterial({
|
||
color: new THREE.Color(moon.color).multiplyScalar(0.85),
|
||
transparent: true, opacity: 0.45, fog: false,
|
||
}));
|
||
line.visible = showInnerMoons && showOrbits;
|
||
innerMoonOrbitLines.set(moon.key, line);
|
||
scene.add(line);
|
||
}
|
||
|
||
function rebuildInnerMoonOrbitLine(moon) {
|
||
const line = innerMoonOrbitLines.get(moon.key);
|
||
if (!line) return;
|
||
const steps = 120;
|
||
const positions = [];
|
||
for (let s = 0; s <= steps; s++) {
|
||
const sampleDate = new Date(simDate.getTime() + (moon.orbitalPeriodDays * s / steps) * 86400000);
|
||
const p = innerMoonPosition(moon, sampleDate);
|
||
positions.push(p.x, p.y, p.z);
|
||
}
|
||
line.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||
line.geometry.attributes.position.needsUpdate = true;
|
||
}
|
||
|
||
function createInnerMoons() {
|
||
INNER_MOONS.forEach((moon) => {
|
||
const mesh = new THREE.Mesh(
|
||
new THREE.SphereGeometry(moon.size, 24, 24),
|
||
new THREE.MeshStandardMaterial({ color: new THREE.Color(moon.color), roughness: 0.9, metalness: 0.0, fog: false })
|
||
);
|
||
mesh.userData.objectKey = moon.key;
|
||
mesh.castShadow = false; // Shadow-Caster übernimmt immer
|
||
mesh.receiveShadow = true;
|
||
mesh.visible = showInnerMoons;
|
||
scene.add(mesh);
|
||
|
||
const shadowCaster = new THREE.Mesh(
|
||
new THREE.SphereGeometry(moon.size, 12, 12),
|
||
new THREE.MeshStandardMaterial()
|
||
);
|
||
shadowCaster.castShadow = false;
|
||
shadowCaster.visible = false;
|
||
scene.add(shadowCaster);
|
||
|
||
innerMoonGroups.set(moon.key, mesh);
|
||
innerMoonShadowCasters.set(moon.key, shadowCaster);
|
||
createInnerMoonOrbitLine(moon);
|
||
});
|
||
}
|
||
|
||
function updateInnerMoonPositions() {
|
||
if (!showInnerMoons) return;
|
||
INNER_MOONS.forEach((moon) => {
|
||
const mesh = innerMoonGroups.get(moon.key);
|
||
const shadowCaster = innerMoonShadowCasters.get(moon.key);
|
||
const pos = innerMoonPosition(moon, simDate);
|
||
if (mesh) mesh.position.copy(pos);
|
||
if (shadowCaster) shadowCaster.position.copy(pos);
|
||
});
|
||
}
|
||
|
||
function createJupiterSystemObjects() {
|
||
const jupiterMaterial = new THREE.MeshStandardMaterial({
|
||
color: 0xffffff,
|
||
emissive: new THREE.Color(0x2b1908),
|
||
emissiveIntensity: 0.10,
|
||
roughness: 0.92,
|
||
metalness: 0.00,
|
||
});
|
||
loadTexture('./images/planets/jupiter_2048.jpg', (texture) => {
|
||
jupiterMaterial.map = texture;
|
||
jupiterMaterial.needsUpdate = true;
|
||
});
|
||
jupiterGroup = new THREE.Mesh(
|
||
new THREE.SphereGeometry(5.8, 64, 64),
|
||
jupiterMaterial
|
||
);
|
||
jupiterGroup.userData.objectKey = 'jupiter';
|
||
jupiterGroup.scale.set(1, 66854 / 71492, 1); // Abplattung: Polarradius/Äquatorialradius
|
||
jupiterGroup.receiveShadow = true;
|
||
jupiterGroup.castShadow = true;
|
||
scene.add(jupiterGroup);
|
||
|
||
MOONS.forEach((moon) => {
|
||
const moonMaterial = new THREE.MeshStandardMaterial({
|
||
color: new THREE.Color(moon.color),
|
||
emissive: new THREE.Color(0x0a0a0a),
|
||
emissiveIntensity: 0.10,
|
||
roughness: 0.92,
|
||
metalness: 0.00,
|
||
fog: false,
|
||
});
|
||
loadTexture(`./${moon.texture}`, (texture) => {
|
||
moonMaterial.map = texture;
|
||
moonMaterial.color.set(0xffffff);
|
||
moonMaterial.needsUpdate = true;
|
||
});
|
||
const mesh = new THREE.Mesh(
|
||
new THREE.SphereGeometry(moon.size, 32, 32),
|
||
moonMaterial
|
||
);
|
||
mesh.userData.objectKey = moon.key;
|
||
mesh.castShadow = true;
|
||
mesh.receiveShadow = true;
|
||
scene.add(mesh);
|
||
|
||
// Shadow-Caster in Realgröße — nur aktiv wenn Monde visuell vergrößert sind
|
||
const shadowCaster = new THREE.Mesh(
|
||
new THREE.SphereGeometry(moon.size, 16, 16),
|
||
new THREE.MeshStandardMaterial()
|
||
);
|
||
shadowCaster.castShadow = false;
|
||
shadowCaster.visible = false;
|
||
scene.add(shadowCaster);
|
||
moonShadowCasters.set(moon.key, shadowCaster);
|
||
moonGroups.set(moon.key, mesh);
|
||
createMoonOrbitLine(moon);
|
||
});
|
||
}
|
||
|
||
function updateSunDirection() {
|
||
// Jupiterposition heliozentrisch in EQJ → negiert = Richtung Jupiter→Sonne
|
||
const jupVec = AstronomyEngine.HelioVector(AstronomyEngine.Body.Jupiter, simDate);
|
||
const toSun = { x: -jupVec.x, y: -jupVec.y, z: -jupVec.z };
|
||
const ecl = AstronomyEngine.RotateVector(rotationEqjToEcl, toSun);
|
||
// Nur die Richtung verwenden (normieren), Licht auf feste Distanz von 800 setzen
|
||
const len = Math.hypot(ecl.x, ecl.z, ecl.y);
|
||
sunLight.position.set(
|
||
(ecl.x / len) * 800,
|
||
(ecl.z / len) * 800,
|
||
(-ecl.y / len) * 800
|
||
);
|
||
}
|
||
|
||
function updateMoonPositions(deltaDays) {
|
||
const states = getJupiterMoonStates(simDate);
|
||
MOONS.forEach((moon) => {
|
||
const mesh = moonGroups.get(moon.key);
|
||
if (!mesh) {
|
||
return;
|
||
}
|
||
|
||
const display = toDisplayPosition(states[moon.key]);
|
||
mesh.position.copy(display);
|
||
const shadowCaster = moonShadowCasters.get(moon.key);
|
||
if (shadowCaster) shadowCaster.position.copy(display);
|
||
// Monde sind gezeitengebunden → keine unabhängige Eigenrotation
|
||
});
|
||
|
||
if (jupiterGroup) {
|
||
const elapsedDays = (simDate - grsRefDate) / 86400000;
|
||
jupiterGroup.rotation.y = grsRefAngle + (elapsedDays / GRS_SYSTEM_II_PERIOD_DAYS) * Math.PI * 2;
|
||
}
|
||
}
|
||
|
||
function updateLabels() {
|
||
labelElements.forEach((label, key) => {
|
||
label.hidden = !showLabels;
|
||
if (!showLabels) {
|
||
return;
|
||
}
|
||
|
||
const object = key === 'jupiter' ? jupiterGroup
|
||
: (moonGroups.get(key) || innerMoonGroups.get(key));
|
||
if (!object || !object.visible) {
|
||
label.hidden = true;
|
||
return;
|
||
}
|
||
|
||
const projected = object.position.clone().project(camera);
|
||
if (projected.z > 1) {
|
||
label.style.opacity = '0';
|
||
return;
|
||
}
|
||
|
||
label.style.opacity = '1';
|
||
label.style.left = `${(projected.x * 0.5 + 0.5) * stageWrap.clientWidth}px`;
|
||
label.style.top = `${(-projected.y * 0.5 + 0.5) * stageWrap.clientHeight}px`;
|
||
});
|
||
}
|
||
|
||
function pickObject(clientX, clientY) {
|
||
const rect = canvas.getBoundingClientRect();
|
||
pointer.x = ((clientX - rect.left) / rect.width) * 2 - 1;
|
||
pointer.y = -(((clientY - rect.top) / rect.height) * 2 - 1);
|
||
raycaster.setFromCamera(pointer, camera);
|
||
|
||
const pickables = [jupiterGroup, ...Array.from(moonGroups.values())].filter(Boolean);
|
||
const hits = raycaster.intersectObjects(pickables, false);
|
||
if (hits.length === 0) {
|
||
clearFocus();
|
||
return;
|
||
}
|
||
|
||
const objectKey = hits[0].object.userData.objectKey || null;
|
||
if (!objectKey) {
|
||
clearFocus();
|
||
return;
|
||
}
|
||
|
||
focusedObjectKey = objectKey;
|
||
updateInfoPanel(objectKey);
|
||
}
|
||
|
||
function attachPointerControls() {
|
||
canvas.addEventListener('pointerdown', (event) => {
|
||
isDragging = true;
|
||
pointerDownMoved = false;
|
||
lastPointerX = event.clientX;
|
||
lastPointerY = event.clientY;
|
||
canvas.setPointerCapture(event.pointerId);
|
||
});
|
||
|
||
canvas.addEventListener('pointermove', (event) => {
|
||
if (!isDragging) {
|
||
return;
|
||
}
|
||
|
||
const dx = event.clientX - lastPointerX;
|
||
const dy = event.clientY - lastPointerY;
|
||
pointerDownMoved = pointerDownMoved || Math.abs(dx) > 3 || Math.abs(dy) > 3;
|
||
camTheta -= dx * 0.005;
|
||
camPhi = Math.max(0.18, Math.min(Math.PI - 0.18, camPhi + dy * 0.005));
|
||
lastPointerX = event.clientX;
|
||
lastPointerY = event.clientY;
|
||
updateCamera();
|
||
});
|
||
|
||
canvas.addEventListener('pointerup', (event) => {
|
||
if (!pointerDownMoved) {
|
||
pickObject(event.clientX, event.clientY);
|
||
}
|
||
isDragging = false;
|
||
canvas.releasePointerCapture(event.pointerId);
|
||
});
|
||
|
||
canvas.addEventListener('wheel', (event) => {
|
||
event.preventDefault();
|
||
camRadiusTarget = Math.max(8, Math.min(600, camRadiusTarget * (1 + event.deltaY * 0.001)));
|
||
camRadius = camRadiusTarget;
|
||
updateCamera();
|
||
}, { passive: false });
|
||
}
|
||
|
||
function parseGrsTransitInput(str) {
|
||
// Erwartet: "MM/DD/YYYY @ HH:MM UT" oder "MM/DD/YYYY @ HH:MM"
|
||
const match = str.trim().match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})\s*@\s*(\d{1,2}):(\d{2})(\s*UT)?$/i);
|
||
if (!match) return null;
|
||
const [, mm, dd, yyyy, hh, min] = match;
|
||
const iso = `${yyyy}-${mm.padStart(2,'0')}-${dd.padStart(2,'0')}T${hh.padStart(2,'0')}:${min}:00Z`;
|
||
const d = new Date(iso);
|
||
return isNaN(d.getTime()) ? null : d;
|
||
}
|
||
|
||
function formatGrsStatus(data) {
|
||
if (!data.calibratedAt) return 'Noch keine Kalibrierung gespeichert.';
|
||
const at = new Date(data.calibratedAt).toLocaleString('de-DE', { timeZone: 'UTC', dateStyle: 'short', timeStyle: 'short' });
|
||
const ref = data.refDate ? new Date(data.refDate).toLocaleString('de-DE', { timeZone: 'UTC', dateStyle: 'short', timeStyle: 'short' }) : '–';
|
||
return `Letzter Abgleich: ${at} UT\nTransit-Ref.: ${ref} UT\nGespeichert von: ${data.calibratedBy || '–'}`;
|
||
}
|
||
|
||
async function loadGrsCalibration() {
|
||
try {
|
||
const res = await fetch('jupitersystem.php?grs_api');
|
||
if (!res.ok) return;
|
||
const data = await res.json();
|
||
if (data.refDate) {
|
||
grsRefDate = new Date(data.refDate);
|
||
grsRefAngle = Number(data.refAngle) || 0;
|
||
}
|
||
const statusEl = document.getElementById('grsCalibStatus');
|
||
if (statusEl) statusEl.textContent = formatGrsStatus(data);
|
||
} catch (e) {}
|
||
}
|
||
|
||
async function saveGrsCalibration() {
|
||
const res = await fetch('jupitersystem.php?grs_api', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ refDate: grsRefDate.toISOString(), refAngle: grsRefAngle }),
|
||
});
|
||
const json = await res.json().catch(() => ({}));
|
||
return res.ok ? true : (json.error || 'Unbekannter Fehler');
|
||
}
|
||
|
||
function shiftGrsRotation(angleDeg) {
|
||
// Verschiebt den GRS-Winkel relativ zur aktuellen Simulationszeit
|
||
const elapsedDays = (simDate - grsRefDate) / 86400000;
|
||
const currentAngle = grsRefAngle + (elapsedDays / GRS_SYSTEM_II_PERIOD_DAYS) * Math.PI * 2;
|
||
grsRefDate = new Date(simDate);
|
||
grsRefAngle = currentAngle + (angleDeg * Math.PI / 180);
|
||
}
|
||
|
||
function applyEarthView(targetRadius = 200) {
|
||
const geoVec = AstronomyEngine.GeoVector(AstronomyEngine.Body.Jupiter, simDate, true);
|
||
const toEarth = { x: -geoVec.x, y: -geoVec.y, z: -geoVec.z };
|
||
const ecl = AstronomyEngine.RotateVector(rotationEqjToEcl, toEarth);
|
||
const dx = ecl.x, dy = ecl.z, dz = -ecl.y;
|
||
camPhi = Math.acos(Math.max(-1, Math.min(1, dy / Math.hypot(dx, dy, dz))));
|
||
camTheta = Math.atan2(dx, dz);
|
||
camRadius = targetRadius;
|
||
camRadiusTarget = targetRadius;
|
||
clearFocus();
|
||
updateCamera();
|
||
}
|
||
|
||
function createMeridianLine() {
|
||
const geo = new THREE.BufferGeometry();
|
||
geo.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array(65 * 3), 3));
|
||
meridianLine = new THREE.Line(geo, new THREE.LineBasicMaterial({
|
||
color: 0xffffff, transparent: true, opacity: 0.75, depthTest: false,
|
||
}));
|
||
meridianLine.visible = false;
|
||
meridianLine.renderOrder = 1;
|
||
scene.add(meridianLine);
|
||
}
|
||
|
||
function updateMeridianLine() {
|
||
if (!meridianLine || !meridianLine.visible) return;
|
||
const R = 5.85; // leicht über Jupiters Oberfläche
|
||
const camDir = camera.position.clone().sub(cameraTarget).normalize();
|
||
const up = new THREE.Vector3(0, 1, 0);
|
||
// Projektion der Kamerarichtung auf die Äquatorialebene
|
||
const dPerp = camDir.clone().sub(up.clone().multiplyScalar(camDir.dot(up)));
|
||
const steps = 64;
|
||
const positions = [];
|
||
if (dPerp.length() < 0.001) {
|
||
// Blick von oben: Kreis auf Äquator
|
||
for (let i = 0; i <= steps; i++) {
|
||
const t = (i / steps) * Math.PI * 2;
|
||
positions.push(R * Math.cos(t), 0, R * Math.sin(t));
|
||
}
|
||
} else {
|
||
dPerp.normalize();
|
||
// Meridian: Südpol → Suberdpunkt → Nordpol
|
||
for (let i = 0; i <= steps; i++) {
|
||
const t = -Math.PI / 2 + (i / steps) * Math.PI;
|
||
positions.push(R * dPerp.x * Math.cos(t), R * Math.sin(t), R * dPerp.z * Math.cos(t));
|
||
}
|
||
}
|
||
meridianLine.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||
meridianLine.geometry.attributes.position.needsUpdate = true;
|
||
}
|
||
|
||
function createShadowRays() {
|
||
MOONS.forEach((moon, i) => {
|
||
const geo = new THREE.BufferGeometry();
|
||
geo.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array(6), 3));
|
||
const colors = ['#f4d16b', '#d8c7a2', '#9f8f7c', '#7f7164'];
|
||
const mat = new THREE.LineBasicMaterial({ color: new THREE.Color(colors[i] || '#ffffff'), transparent: true, opacity: 0.7, depthTest: false });
|
||
const line = new THREE.Line(geo, mat);
|
||
line.visible = false;
|
||
line.renderOrder = 2;
|
||
scene.add(line);
|
||
shadowRays.push({ line, key: moon.key });
|
||
});
|
||
}
|
||
|
||
function updateShadowRays() {
|
||
if (!showShadowRays) return;
|
||
const sunDir = sunLight.position.clone().normalize(); // Richtung Licht→Jupiter
|
||
shadowRays.forEach(({ line, key }) => {
|
||
const mesh = moonGroups.get(key);
|
||
if (!mesh) return;
|
||
const moonPos = mesh.position;
|
||
// Strahl: vom Mond entgegen der Sonnenrichtung bis hinter Jupiter
|
||
const start = moonPos.clone().addScaledVector(sunDir, 200); // Seite der Sonne
|
||
const end = moonPos.clone().addScaledVector(sunDir, -200); // Seite Jupiter/dahinter
|
||
const pos = [start.x, start.y, start.z, end.x, end.y, end.z];
|
||
line.geometry.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
|
||
line.geometry.attributes.position.needsUpdate = true;
|
||
});
|
||
}
|
||
|
||
function attachAdminHandlers() {
|
||
const grsTransitInput = document.getElementById('grsTransitInput');
|
||
if (!grsTransitInput) return;
|
||
|
||
const grsBtnSetTime = document.getElementById('grsBtnSetTime');
|
||
const grsBtnRot5L = document.getElementById('grsBtnRot5L');
|
||
const grsBtnRot1L = document.getElementById('grsBtnRot1L');
|
||
const grsBtnRot1R = document.getElementById('grsBtnRot1R');
|
||
const grsBtnRot5R = document.getElementById('grsBtnRot5R');
|
||
const grsBtnSave = document.getElementById('grsBtnSave');
|
||
const grsCalibStatus = document.getElementById('grsCalibStatus');
|
||
|
||
const grsParseError = document.getElementById('grsParseError');
|
||
|
||
grsBtnSetTime.addEventListener('click', () => {
|
||
const parsed = parseGrsTransitInput(grsTransitInput.value);
|
||
if (!parsed) {
|
||
grsParseError.textContent = 'Format ungültig. Erwartet: MM/DD/YYYY @ HH:MM UT';
|
||
grsParseError.classList.remove('is-hidden');
|
||
return;
|
||
}
|
||
grsParseError.classList.add('is-hidden');
|
||
simDate = parsed;
|
||
refreshCurrentDateUi();
|
||
MOONS.forEach(rebuildMoonOrbitLine);
|
||
// Animation anhalten
|
||
playing = false;
|
||
btnPlay.textContent = 'Play';
|
||
btnPlay.classList.remove('is-active');
|
||
// Blick von der Erde, Jupiter füllt die Fläche (~22 = knapp über Oberfläche mit FOV 45°)
|
||
applyEarthView(22);
|
||
// Zentralmeridian einblenden
|
||
if (meridianLine) meridianLine.visible = true;
|
||
});
|
||
|
||
const grsBtnShadowRays = document.getElementById('grsBtnShadowRays');
|
||
grsBtnShadowRays.addEventListener('click', () => {
|
||
showShadowRays = !showShadowRays;
|
||
grsBtnShadowRays.textContent = showShadowRays ? 'Schattenstrahlen ausblenden' : 'Schattenstrahlen anzeigen';
|
||
grsBtnShadowRays.classList.toggle('is-active', showShadowRays);
|
||
shadowRays.forEach(({ line }) => { line.visible = showShadowRays; });
|
||
});
|
||
|
||
grsBtnRot5L.addEventListener('click', () => shiftGrsRotation(-5));
|
||
grsBtnRot1L.addEventListener('click', () => shiftGrsRotation(-1));
|
||
grsBtnRot1R.addEventListener('click', () => shiftGrsRotation(1));
|
||
grsBtnRot5R.addEventListener('click', () => shiftGrsRotation(5));
|
||
|
||
grsBtnSave.addEventListener('click', async () => {
|
||
const elapsedDays = (simDate - grsRefDate) / 86400000;
|
||
grsRefAngle = grsRefAngle + (elapsedDays / GRS_SYSTEM_II_PERIOD_DAYS) * Math.PI * 2;
|
||
grsRefDate = new Date(simDate);
|
||
const ok = await saveGrsCalibration();
|
||
if (ok === true && meridianLine) meridianLine.visible = false;
|
||
grsCalibStatus.textContent = ok === true
|
||
? `Gespeichert: ${simDate.toISOString().slice(0, 16).replace('T', ' ')} UT\nFür alle Nutzer aktiv.`
|
||
: `Fehler: ${ok}`;
|
||
});
|
||
}
|
||
|
||
function attachUiHandlers() {
|
||
btnToday.addEventListener('click', () => {
|
||
simDate = new Date();
|
||
refreshCurrentDateUi();
|
||
});
|
||
|
||
btnReset.addEventListener('click', () => {
|
||
simDate = new Date();
|
||
clearFocus();
|
||
refreshCurrentDateUi();
|
||
});
|
||
|
||
function togglePlay() {
|
||
playing = !playing;
|
||
btnPlay.textContent = playing ? 'Pause' : 'Play';
|
||
btnPlay.classList.toggle('is-active', playing);
|
||
}
|
||
|
||
btnPlay.addEventListener('click', togglePlay);
|
||
|
||
document.getElementById('jupiterBtnMinus5').addEventListener('click', () => {
|
||
simDate = new Date(simDate.getTime() - 5 * 60000);
|
||
refreshCurrentDateUi();
|
||
});
|
||
|
||
document.getElementById('jupiterBtnPlus5').addEventListener('click', () => {
|
||
simDate = new Date(simDate.getTime() + 5 * 60000);
|
||
refreshCurrentDateUi();
|
||
});
|
||
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.code === 'Space' && e.target === document.body) {
|
||
e.preventDefault();
|
||
togglePlay();
|
||
}
|
||
});
|
||
|
||
btnTop.addEventListener('click', () => {
|
||
camPhi = 0.22;
|
||
camTheta = 0;
|
||
camRadius = 120;
|
||
camRadiusTarget = 120;
|
||
clearFocus();
|
||
updateCamera();
|
||
});
|
||
|
||
btnSide.addEventListener('click', () => {
|
||
camPhi = Math.PI / 2;
|
||
camTheta = 0;
|
||
camRadius = 95;
|
||
camRadiusTarget = 95;
|
||
clearFocus();
|
||
updateCamera();
|
||
});
|
||
|
||
btnEarth.addEventListener('click', () => applyEarthView());
|
||
|
||
btnOrbits.addEventListener('click', () => {
|
||
showOrbits = !showOrbits;
|
||
btnOrbits.classList.toggle('is-active', showOrbits);
|
||
moonOrbitLines.forEach((line) => {
|
||
line.visible = showOrbits;
|
||
});
|
||
innerMoonOrbitLines.forEach((line) => {
|
||
line.visible = showInnerMoons && showOrbits;
|
||
});
|
||
});
|
||
|
||
btnLabels.addEventListener('click', () => {
|
||
showLabels = !showLabels;
|
||
btnLabels.classList.toggle('is-active', showLabels);
|
||
labelElements.forEach((label) => {
|
||
label.hidden = !showLabels;
|
||
});
|
||
});
|
||
|
||
btnInnerMoons.addEventListener('click', () => {
|
||
showInnerMoons = !showInnerMoons;
|
||
btnInnerMoons.classList.toggle('is-active', showInnerMoons);
|
||
INNER_MOONS.forEach((moon) => {
|
||
const mesh = innerMoonGroups.get(moon.key);
|
||
const line = innerMoonOrbitLines.get(moon.key);
|
||
if (mesh) mesh.visible = showInnerMoons;
|
||
if (line) line.visible = showInnerMoons && showOrbits;
|
||
});
|
||
});
|
||
|
||
chkMoonScale.addEventListener('change', () => {
|
||
moonScaleEnlarged = chkMoonScale.checked;
|
||
const factor = moonScaleEnlarged ? 4 : 1;
|
||
MOONS.forEach((moon) => {
|
||
const mesh = moonGroups.get(moon.key);
|
||
const shadowCaster = moonShadowCasters.get(moon.key);
|
||
if (mesh) {
|
||
mesh.scale.setScalar(factor);
|
||
mesh.castShadow = !moonScaleEnlarged;
|
||
if (shadowCaster) {
|
||
shadowCaster.castShadow = moonScaleEnlarged;
|
||
shadowCaster.visible = moonScaleEnlarged;
|
||
}
|
||
}
|
||
});
|
||
INNER_MOONS.forEach((moon) => {
|
||
const mesh = innerMoonGroups.get(moon.key);
|
||
if (mesh) mesh.scale.setScalar(factor);
|
||
// Shadow-Caster bleibt immer bei moon.size — keine Änderung nötig
|
||
});
|
||
});
|
||
|
||
speedSlider.addEventListener('input', updateSpeedFromSlider);
|
||
speedSlider.addEventListener('wheel', (e) => {
|
||
e.preventDefault();
|
||
const step = parseFloat(speedSlider.step) || 0.01;
|
||
speedSlider.value = Math.min(speedSlider.max, Math.max(speedSlider.min,
|
||
parseFloat(speedSlider.value) + (e.deltaY < 0 ? step : -step)
|
||
));
|
||
updateSpeedFromSlider();
|
||
}, { passive: false });
|
||
document.getElementById('jupiterBtnRealtime').addEventListener('click', () => {
|
||
speedDaysPerSecond = 1 / 86400; // 1 Sekunde real = 1 Sekunde Simulation
|
||
speedSlider.value = 0;
|
||
speedLabel.textContent = 'Echtzeit';
|
||
if (!playing) {
|
||
playing = true;
|
||
btnPlay.textContent = 'Pause';
|
||
btnPlay.classList.add('is-active');
|
||
}
|
||
});
|
||
|
||
datePicker.addEventListener('change', () => {
|
||
if (!datePicker.value) {
|
||
return;
|
||
}
|
||
const [year, month, day] = datePicker.value.split('-').map(Number);
|
||
const current = new Date(simDate);
|
||
simDate = new Date(year, month - 1, day, current.getHours(), current.getMinutes(), current.getSeconds());
|
||
refreshCurrentDateUi();
|
||
MOONS.forEach(rebuildMoonOrbitLine);
|
||
});
|
||
}
|
||
|
||
function animate() {
|
||
requestAnimationFrame(animate);
|
||
|
||
const now = performance.now();
|
||
const deltaSeconds = (now - lastAnimationTime) / 1000;
|
||
lastAnimationTime = now;
|
||
|
||
const deltaDays = playing ? speedDaysPerSecond * deltaSeconds : 0;
|
||
if (playing) {
|
||
simDate = new Date(simDate.getTime() + deltaDays * 86400000);
|
||
}
|
||
|
||
updateMoonPositions(deltaDays);
|
||
updateInnerMoonPositions();
|
||
updateSunDirection();
|
||
updateMeridianLine();
|
||
updateShadowRays();
|
||
refreshCurrentDateUi();
|
||
|
||
if (focusedObjectKey) {
|
||
const object = focusedObjectKey === 'jupiter' ? jupiterGroup : moonGroups.get(focusedObjectKey);
|
||
if (object) {
|
||
cameraTarget.lerp(object.position, 0.06);
|
||
updateInfoPanel(focusedObjectKey);
|
||
}
|
||
}
|
||
|
||
camRadius += (camRadiusTarget - camRadius) * 0.18;
|
||
updateCamera();
|
||
updateLabels();
|
||
|
||
renderer.render(scene, camera);
|
||
}
|
||
|
||
async function initJupiterSystem() {
|
||
try {
|
||
THREE = await import('./js/three.module.js');
|
||
|
||
renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||
renderer.setClearColor(0x000000, 0);
|
||
renderer.shadowMap.enabled = true;
|
||
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||
|
||
scene = new THREE.Scene();
|
||
scene.fog = new THREE.FogExp2(0x03050c, 0.00015);
|
||
|
||
camera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000);
|
||
textureLoader = new THREE.TextureLoader();
|
||
moonGroups = new Map();
|
||
moonShadowCasters = new Map();
|
||
moonOrbitLines = new Map();
|
||
innerMoonGroups = new Map();
|
||
innerMoonShadowCasters = new Map();
|
||
innerMoonOrbitLines = new Map();
|
||
labelElements = new Map();
|
||
raycaster = new THREE.Raycaster();
|
||
pointer = new THREE.Vector2();
|
||
cameraTarget = new THREE.Vector3(0, 0, 0);
|
||
rotationEqjToEcl = AstronomyEngine.Rotation_EQJ_ECL();
|
||
|
||
const ambientLight = new THREE.AmbientLight(0x334466, 0.8);
|
||
scene.add(ambientLight);
|
||
|
||
const hemiLight = new THREE.HemisphereLight(0x445577, 0x111a2e, 0.6);
|
||
scene.add(hemiLight);
|
||
|
||
sunLight = new THREE.DirectionalLight(0xfff5d0, 3.5);
|
||
sunLight.position.set(550, 0, 0); // wird jede Frame aktualisiert
|
||
sunLight.castShadow = true;
|
||
sunLight.shadow.mapSize.width = 4096;
|
||
sunLight.shadow.mapSize.height = 4096;
|
||
sunLight.shadow.camera.near = 500; // Licht bei 800 → Nahebene bei 300 Einh. von Jupiter (Kallisto max. 153)
|
||
sunLight.shadow.camera.far = 1050; // Licht bei 800 + Kallisto hinter Jupiter (153) + Puffer
|
||
sunLight.shadow.camera.left = -40;
|
||
sunLight.shadow.camera.right = 40;
|
||
sunLight.shadow.camera.top = 40;
|
||
sunLight.shadow.camera.bottom = -40;
|
||
sunLight.shadow.camera.updateProjectionMatrix();
|
||
sunLight.shadow.radius = 0.5;
|
||
scene.add(sunLight);
|
||
scene.add(sunLight.target); // Pflicht: sonst ignoriert Three.js die Shadow-Camera-Ausrichtung
|
||
|
||
// Starfield
|
||
(function createStarfield() {
|
||
const starCount = 2200;
|
||
const positions = new Float32Array(starCount * 3);
|
||
const colors = new Float32Array(starCount * 3);
|
||
const starColors = [[1.0,0.95,0.85],[0.85,0.92,1.0],[1.0,1.0,1.0],[1.0,0.88,0.72],[0.78,0.88,1.0]];
|
||
for (let i = 0; i < starCount; i++) {
|
||
const theta = Math.random() * Math.PI * 2;
|
||
const phi = Math.acos(2 * Math.random() - 1);
|
||
const r = 700 + Math.random() * 80;
|
||
positions[i*3] = r * Math.sin(phi) * Math.cos(theta);
|
||
positions[i*3+1] = r * Math.sin(phi) * Math.sin(theta);
|
||
positions[i*3+2] = r * Math.cos(phi);
|
||
const c = starColors[Math.floor(Math.random() * starColors.length)];
|
||
colors[i*3] = c[0]; colors[i*3+1] = c[1]; colors[i*3+2] = c[2];
|
||
}
|
||
const geo = new THREE.BufferGeometry();
|
||
geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||
geo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
|
||
scene.add(new THREE.Points(geo, new THREE.PointsMaterial({
|
||
vertexColors: true, size: 1.1, sizeAttenuation: false, transparent: true, opacity: 0.88,
|
||
})));
|
||
})();
|
||
|
||
await loadGrsCalibration();
|
||
buildLabels();
|
||
createJupiterSystemObjects();
|
||
createInnerMoons();
|
||
createMeridianLine();
|
||
createShadowRays();
|
||
attachPointerControls();
|
||
attachUiHandlers();
|
||
attachAdminHandlers();
|
||
btnPlay.textContent = 'Pause';
|
||
btnPlay.classList.add('is-active');
|
||
updateSpeedFromSlider();
|
||
refreshCurrentDateUi();
|
||
resizeRenderer();
|
||
updateMoonPositions(0);
|
||
updateCamera();
|
||
overlay.hidden = true;
|
||
animate();
|
||
|
||
if (typeof ResizeObserver === 'function') {
|
||
const resizeObserver = new ResizeObserver(() => {
|
||
resizeRenderer();
|
||
});
|
||
resizeObserver.observe(stageWrap);
|
||
}
|
||
|
||
window.addEventListener('resize', resizeRenderer);
|
||
} catch (error) {
|
||
console.error('Jupitersystem konnte nicht initialisiert werden.', error);
|
||
overlay.hidden = false;
|
||
overlay.textContent = 'Fehler beim Laden der Visualisierung. Bitte Browser-Konsole pruefen.';
|
||
}
|
||
}
|
||
|
||
initJupiterSystem();
|
||
|
||
/* ═══════════════════════════════════════════════════
|
||
MONATSÜBERSICHT — Galileische Monde (DIN A4)
|
||
═══════════════════════════════════════════════════ */
|
||
function generateEphemerisCanvas() {
|
||
const darkMode = false;
|
||
// Monat aus simDate
|
||
const ref = new Date(simDate);
|
||
const year = ref.getFullYear();
|
||
const month = ref.getMonth(); // 0-based
|
||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||
const monthName = ref.toLocaleString('de-DE', { month: 'long', year: 'numeric' });
|
||
|
||
// DIN A4 @ 150 dpi
|
||
const W = 1240;
|
||
const H = 1754;
|
||
|
||
const MARGIN_LEFT = 160; // Platz für Tageszahlen + GRS-Zeiten
|
||
const MARGIN_RIGHT = 50;
|
||
const MARGIN_TOP = 80;
|
||
const MARGIN_BOT = 160;
|
||
|
||
const chartW = W - MARGIN_LEFT - MARGIN_RIGHT;
|
||
const chartH = H - MARGIN_TOP - MARGIN_BOT;
|
||
const rowH = chartH / daysInMonth;
|
||
|
||
// Jupiter-Radius in AU → wir skalieren so dass Kallisto (~0.01257 AU) auf halbe Chartbreite passt
|
||
const MAX_AU = 0.014; // ±0.014 AU ~ knapp über Kallistos max. Elongation
|
||
const scaleX = (chartW / 2) / MAX_AU; // px pro AU
|
||
const centerX = MARGIN_LEFT + chartW / 2;
|
||
const jupiterPxRadius = 7;
|
||
|
||
const MOON_COLORS = {
|
||
io: '#e07000', // kräftiges Orange
|
||
europa: '#1a6abf', // kräftiges Blau
|
||
ganymede: '#2a9d40', // kräftiges Grün
|
||
callisto: '#9030a0', // kräftiges Violett
|
||
};
|
||
const MOON_KEYS = ['io', 'europa', 'ganymede', 'callisto'];
|
||
const MOON_LABELS = { io: 'Io', europa: 'Europa', ganymede: 'Ganymed', callisto: 'Kallisto' };
|
||
|
||
// Positionen berechnen: alle 2 Stunden (12 Punkte/Tag) für glatte Kurven
|
||
const STEPS_PER_DAY = 12;
|
||
const STEP_HOURS = 24 / STEPS_PER_DAY;
|
||
const JUPITER_RADIUS_AU = AstronomyEngine.JUPITER_EQUATORIAL_RADIUS_KM / AU_KM;
|
||
|
||
// Schattenberechnung: Mond wirft Schatten auf Jupiter
|
||
// → Mond steht zwischen Sonne und Jupiter (auf der Sonnenseite)
|
||
// → Schatten des Mondes (entlang Sonne→Jupiter-Achse projiziert) trifft Jupiterscheibe
|
||
function getMoonsNearDisk(date, states) {
|
||
const jupHelio = AstronomyEngine.HelioVector(AstronomyEngine.Body.Jupiter, date);
|
||
const jupHelioDist = Math.hypot(jupHelio.x, jupHelio.y, jupHelio.z);
|
||
// Einheitsvektor Sonne → Jupiter
|
||
const sx = jupHelio.x / jupHelioDist;
|
||
const sy = jupHelio.y / jupHelioDist;
|
||
const sz = jupHelio.z / jupHelioDist;
|
||
|
||
const result = {};
|
||
MOON_KEYS.forEach(key => {
|
||
const m = states[key];
|
||
// Komponente entlang Sonne→Jupiter: negativ = Mond auf Sonnenseite (zwischen Sonne und Jupiter)
|
||
const dot = m.x * sx + m.y * sy + m.z * sz;
|
||
if (dot >= 0) return; // Mond auf Schattenseite → kein Schatten auf Jupiter
|
||
// Senkrechter Abstand des Mondes von der Sonne-Jupiter-Achse
|
||
const pxS = m.x - dot * sx, pyS = m.y - dot * sy, pzS = m.z - dot * sz;
|
||
if (Math.hypot(pxS, pyS, pzS) < JUPITER_RADIUS_AU) {
|
||
result[key] = 'shadow'; // Mondschatten trifft Jupiterscheibe
|
||
}
|
||
});
|
||
return result;
|
||
}
|
||
|
||
const points = [];
|
||
for (let d = 1; d <= daysInMonth; d++) {
|
||
for (let s = 0; s < STEPS_PER_DAY; s++) {
|
||
const date = new Date(Date.UTC(year, month, d, s * STEP_HOURS, 0, 0));
|
||
const states = getJupiterMoonStates(date);
|
||
const moonX = {};
|
||
MOON_KEYS.forEach(key => { moonX[key] = states[key].x; });
|
||
const nearDisk = getMoonsNearDisk(date, states);
|
||
points.push({ d, t: (d - 1) + s / STEPS_PER_DAY, date, moonX, nearDisk });
|
||
}
|
||
}
|
||
|
||
// Doppel-/Dreifach-Ereignisse erkennen + Beginn auf 5 Min verfeinern
|
||
function refineMultiStart(dateA, dateB) {
|
||
// Binäre Suche: finde den frühesten Moment wo >= 2 Monde im Schatten sind
|
||
let lo = dateA.getTime(), hi = dateB.getTime();
|
||
for (let i = 0; i < 8; i++) { // 8 Schritte → ~1 Min Genauigkeit aus 2h
|
||
const mid = (lo + hi) / 2;
|
||
const midDate = new Date(mid);
|
||
const st = getJupiterMoonStates(midDate);
|
||
const n = Object.keys(getMoonsNearDisk(midDate, st)).length;
|
||
if (n >= 2) hi = mid; else lo = mid;
|
||
}
|
||
// Auf 5-Minuten-Raster runden
|
||
const rounded = Math.round(hi / 300000) * 300000;
|
||
return new Date(rounded);
|
||
}
|
||
|
||
const multiEvents = [];
|
||
let lastEventT = -999;
|
||
points.forEach((pt, i) => {
|
||
const inDisk = Object.keys(pt.nearDisk);
|
||
if (inDisk.length >= 2 && pt.t - lastEventT > 0.1) {
|
||
const prev = i > 0 ? points[i - 1] : pt;
|
||
const refinedDate = refineMultiStart(prev.date, pt.date);
|
||
const refinedT = pt.t - (pt.date - refinedDate) / 86400000;
|
||
multiEvents.push({ t: refinedT, date: refinedDate, moons: inDisk });
|
||
lastEventT = pt.t;
|
||
}
|
||
});
|
||
// GRS-Transitzeiten: alle Transits im Monat berechnen
|
||
function getGrsTransitsForDay(d) {
|
||
if (!grsRefDate || grsRefDate.getTime() === 0) return [];
|
||
const dayStart = new Date(Date.UTC(year, month, d, 0, 0, 0)).getTime();
|
||
const dayEnd = dayStart + 86400000;
|
||
// Erster Transit nach Epoch: finde nächsten Transit ab dayStart
|
||
const refMs = grsRefDate.getTime();
|
||
const periodMs = GRS_SYSTEM_II_PERIOD_DAYS * 86400000;
|
||
// Wie viele Perioden seit refDate bis dayStart?
|
||
const n = Math.ceil((dayStart - refMs) / periodMs);
|
||
const transits = [];
|
||
for (let i = n - 1; i <= n + 3; i++) {
|
||
const t = refMs + i * periodMs;
|
||
if (t >= dayStart && t < dayEnd) transits.push(new Date(t));
|
||
}
|
||
return transits;
|
||
}
|
||
|
||
// rows für Jupiter-Kreise und Tageszahlen (einmal pro Tag, Mittag)
|
||
const rows = [];
|
||
for (let d = 1; d <= daysInMonth; d++) {
|
||
const date = new Date(Date.UTC(year, month, d, 12, 0, 0));
|
||
const states = getJupiterMoonStates(date);
|
||
const moonX = {};
|
||
MOON_KEYS.forEach(key => { moonX[key] = states[key].x; });
|
||
const grsTransits = getGrsTransitsForDay(d);
|
||
rows.push({ d, moonX, grsTransits });
|
||
}
|
||
|
||
// Canvas aufbauen
|
||
const cvs = document.createElement('canvas');
|
||
cvs.width = W;
|
||
cvs.height = H;
|
||
const c = cvs.getContext('2d');
|
||
|
||
// ── Farben je nach Modus ──
|
||
const COL = darkMode ? {
|
||
bg0: '#06080f', bg1: '#03030d',
|
||
text: '#eef4ff', textDim: 'rgba(200,215,240,0.35)',
|
||
grid: 'rgba(255,255,255,0.03)',
|
||
rowAlt: 'rgba(255,255,255,0.018)',
|
||
sep: 'rgba(201,168,76,0.08)',
|
||
gold: '#c9a84c', goldLine: 'rgba(201,168,76,0.5)',
|
||
axisGlow: 'rgba(232,160,32,0.12)', axisCore: 'rgba(232,160,32,0.35)',
|
||
jupRim: 'rgba(255,200,100,0.6)',
|
||
moonRim: 'rgba(255,255,255,0.3)',
|
||
legBg: 'rgba(6,10,26,0.7)', legBorder: 'rgba(201,168,76,0.3)',
|
||
legText: 'rgba(200,215,240,0.65)',
|
||
grsCol: '#ff9930',
|
||
footText: 'rgba(200,215,240,0.35)',
|
||
titleLine: 'rgba(201,168,76,0.5)',
|
||
} : {
|
||
bg0: '#ffffff', bg1: '#f5f5f8',
|
||
text: '#1a1a2e', textDim: 'rgba(30,30,60,0.5)',
|
||
grid: 'rgba(0,0,0,0.04)',
|
||
rowAlt: 'rgba(0,0,0,0.03)',
|
||
sep: 'rgba(100,80,20,0.15)',
|
||
gold: '#8a6010', goldLine: 'rgba(120,90,20,0.4)',
|
||
axisGlow: 'rgba(180,100,0,0.08)', axisCore: 'rgba(180,100,0,0.5)',
|
||
jupRim: 'rgba(180,100,0,0.5)',
|
||
moonRim: 'rgba(0,0,0,0.2)',
|
||
legBg: 'rgba(240,240,245,0.9)', legBorder: 'rgba(100,80,20,0.3)',
|
||
legText: 'rgba(40,40,80,0.8)',
|
||
grsCol: '#c05000',
|
||
footText: 'rgba(60,60,100,0.5)',
|
||
titleLine: 'rgba(120,90,20,0.4)',
|
||
};
|
||
|
||
// ── Hintergrund mit leichtem Verlauf ──
|
||
const bgGrad = c.createLinearGradient(0, 0, 0, H);
|
||
bgGrad.addColorStop(0, COL.bg0);
|
||
bgGrad.addColorStop(1, COL.bg1);
|
||
c.fillStyle = bgGrad;
|
||
c.fillRect(0, 0, W, H);
|
||
|
||
// ── Subtile Rasterlinien im Chartbereich ──
|
||
c.strokeStyle = COL.grid;
|
||
c.lineWidth = 1;
|
||
for (let x = centerX - Math.round(MAX_AU / 0.002) * 0.002 * scaleX;
|
||
x < MARGIN_LEFT + chartW; x += 0.002 * scaleX) {
|
||
c.beginPath(); c.moveTo(x, MARGIN_TOP); c.lineTo(x, MARGIN_TOP + chartH); c.stroke();
|
||
}
|
||
|
||
// ── Titel ──
|
||
c.textBaseline = 'alphabetic';
|
||
c.fillStyle = COL.gold;
|
||
c.font = 'bold 42px monospace';
|
||
c.textAlign = 'center';
|
||
c.fillText(`Jupitermonde — ${monthName}`, W / 2, 56);
|
||
|
||
// Goldene Trennlinie unter Titel
|
||
const tl = c.createLinearGradient(MARGIN_LEFT, 0, MARGIN_LEFT + chartW, 0);
|
||
tl.addColorStop(0, 'transparent');
|
||
tl.addColorStop(0.2, COL.titleLine);
|
||
tl.addColorStop(0.8, COL.titleLine);
|
||
tl.addColorStop(1, 'transparent');
|
||
c.strokeStyle = tl; c.lineWidth = 1;
|
||
c.beginPath(); c.moveTo(MARGIN_LEFT, 66); c.lineTo(MARGIN_LEFT + chartW, 66); c.stroke();
|
||
|
||
// ── Mittellinie (Jupiter-Achse) mit Glühen ──
|
||
c.strokeStyle = COL.axisGlow;
|
||
c.lineWidth = 6;
|
||
c.beginPath(); c.moveTo(centerX, MARGIN_TOP); c.lineTo(centerX, MARGIN_TOP + chartH); c.stroke();
|
||
c.strokeStyle = COL.axisCore;
|
||
c.lineWidth = 1;
|
||
c.beginPath(); c.moveTo(centerX, MARGIN_TOP); c.lineTo(centerX, MARGIN_TOP + chartH); c.stroke();
|
||
|
||
// ── Mondlinien (hochauflösend) ──
|
||
MOON_KEYS.forEach(key => {
|
||
// Glow-Pass
|
||
c.beginPath();
|
||
c.strokeStyle = MOON_COLORS[key] + '30'; // hex alpha ~19%
|
||
c.lineWidth = 6;
|
||
c.lineJoin = 'round';
|
||
points.forEach((pt, i) => {
|
||
const px = centerX + pt.moonX[key] * scaleX;
|
||
const py = MARGIN_TOP + pt.t * rowH;
|
||
if (i === 0) c.moveTo(px, py); else c.lineTo(px, py);
|
||
});
|
||
c.stroke();
|
||
// Hauptlinie
|
||
c.beginPath();
|
||
c.strokeStyle = MOON_COLORS[key];
|
||
c.lineWidth = 1.8;
|
||
points.forEach((pt, i) => {
|
||
const px = centerX + pt.moonX[key] * scaleX;
|
||
const py = MARGIN_TOP + pt.t * rowH;
|
||
if (i === 0) c.moveTo(px, py); else c.lineTo(px, py);
|
||
});
|
||
c.stroke();
|
||
});
|
||
|
||
// ── Tageszeilen ──
|
||
const fontSize = Math.round(rowH * 0.48);
|
||
rows.forEach((row, ri) => {
|
||
const py = MARGIN_TOP + (ri + 0.5) * rowH;
|
||
|
||
// Alternierende Zeilentönung
|
||
if (ri % 2 === 0) {
|
||
c.fillStyle = COL.rowAlt;
|
||
c.fillRect(MARGIN_LEFT, MARGIN_TOP + ri * rowH, chartW, rowH);
|
||
}
|
||
|
||
// Trennlinie
|
||
if (ri > 0) {
|
||
c.strokeStyle = COL.sep;
|
||
c.lineWidth = 0.5;
|
||
c.beginPath();
|
||
c.moveTo(MARGIN_LEFT, MARGIN_TOP + ri * rowH);
|
||
c.lineTo(W - MARGIN_RIGHT, MARGIN_TOP + ri * rowH);
|
||
c.stroke();
|
||
}
|
||
|
||
// Tageszahl
|
||
c.font = `bold ${fontSize}px monospace`;
|
||
c.fillStyle = COL.text;
|
||
c.textAlign = 'left';
|
||
c.textBaseline = 'middle';
|
||
c.fillText(String(row.d).padStart(2, ' '), 4, py);
|
||
|
||
// GRS-Transitzeiten
|
||
if (row.grsTransits.length > 0) {
|
||
const grsSize = Math.round(rowH * 0.33);
|
||
c.font = `${grsSize}px monospace`;
|
||
c.fillStyle = COL.grsCol;
|
||
const timeStr = row.grsTransits.map(t => t.toISOString().slice(11, 16)).join(' ');
|
||
c.fillText(timeStr, 4 + fontSize * 1.5, py);
|
||
}
|
||
|
||
// Jupiter-Kreis mit Glühen
|
||
const jg = c.createRadialGradient(centerX, py, 0, centerX, py, jupiterPxRadius * 2.5);
|
||
jg.addColorStop(0, 'rgba(232,160,32,0.25)');
|
||
jg.addColorStop(1, 'transparent');
|
||
c.beginPath(); c.arc(centerX, py, jupiterPxRadius * 2.5, 0, Math.PI * 2);
|
||
c.fillStyle = jg; c.fill();
|
||
c.beginPath(); c.arc(centerX, py, jupiterPxRadius, 0, Math.PI * 2);
|
||
c.fillStyle = '#e8a020'; c.fill();
|
||
c.strokeStyle = COL.jupRim; c.lineWidth = 0.8;
|
||
c.stroke();
|
||
|
||
// Mondpunkte
|
||
MOON_KEYS.forEach(key => {
|
||
const px = centerX + row.moonX[key] * scaleX;
|
||
c.beginPath(); c.arc(px, py, 4, 0, Math.PI * 2);
|
||
c.fillStyle = MOON_COLORS[key]; c.fill();
|
||
c.strokeStyle = COL.moonRim; c.lineWidth = 0.5; c.stroke();
|
||
});
|
||
});
|
||
|
||
// ── Schattenstriche rechts: nur Beginn jeder Finsternis ──
|
||
const TICK_X = W - MARGIN_RIGHT + 10;
|
||
const TICK_W = 22;
|
||
const TICK_H = 3;
|
||
const TICK_GAP = 3;
|
||
points.forEach((pt, i) => {
|
||
const prev = i > 0 ? points[i - 1].nearDisk : {};
|
||
MOON_KEYS.forEach((key, ki) => {
|
||
// Strich nur beim Übergang: vorher nicht im Schatten, jetzt schon
|
||
if (pt.nearDisk[key] && !prev[key]) {
|
||
const py = MARGIN_TOP + pt.t * rowH;
|
||
c.fillStyle = MOON_COLORS[key];
|
||
c.fillRect(TICK_X, py - TICK_H / 2, TICK_W, TICK_H);
|
||
}
|
||
});
|
||
});
|
||
|
||
// ── Doppel-/Dreifach-Ereignisse ──
|
||
multiEvents.forEach(ev => {
|
||
const py = MARGIN_TOP + ev.t * rowH;
|
||
const isTriple = ev.moons.length >= 3;
|
||
c.save();
|
||
c.strokeStyle = isTriple ? 'rgba(255,80,80,0.5)' : 'rgba(255,220,80,0.35)';
|
||
c.lineWidth = isTriple ? 2 : 1;
|
||
c.setLineDash([3, 5]);
|
||
c.beginPath(); c.moveTo(MARGIN_LEFT, py); c.lineTo(W - MARGIN_RIGHT - 36, py); c.stroke();
|
||
c.setLineDash([]);
|
||
c.restore();
|
||
const evLabel = (isTriple ? '◆ 3× ' : '◆ 2× ') +
|
||
ev.moons.map(k => MOON_LABELS[k]).join('+') +
|
||
' ' + ev.date.toISOString().slice(11, 16) + ' UT';
|
||
c.font = `bold ${Math.round(rowH * 0.34)}px monospace`;
|
||
c.fillStyle = isTriple ? 'rgba(255,110,110,0.95)' : 'rgba(255,220,80,0.9)';
|
||
c.textAlign = 'right';
|
||
c.textBaseline = 'bottom';
|
||
c.fillText(evLabel, W - MARGIN_RIGHT - 38, py - 2);
|
||
});
|
||
|
||
// ── Legende + Fußzeile ──
|
||
// Layout: Legendenbox links, Logo rechts daneben, Fußtext ganz unten
|
||
const logoH = 64;
|
||
const logoW_est = 64; // wird beim Zeichnen korrigiert
|
||
const LEG_PAD = 20;
|
||
const LEG_X = MARGIN_LEFT;
|
||
const LEG_W = chartW - logoW_est - 16;
|
||
const LEG_H = 120;
|
||
const LEG_Y = H - MARGIN_BOT + 10;
|
||
const fontSize2 = 19;
|
||
const lineH = 30;
|
||
|
||
// Rahmen
|
||
c.save();
|
||
c.strokeStyle = COL.legBorder;
|
||
c.lineWidth = 1;
|
||
c.beginPath();
|
||
c.roundRect(LEG_X, LEG_Y, LEG_W, LEG_H, 8);
|
||
c.fillStyle = COL.legBg;
|
||
c.fill(); c.stroke();
|
||
c.restore();
|
||
|
||
c.textBaseline = 'middle';
|
||
c.textAlign = 'left';
|
||
|
||
// Zeile 1: Monde (4 Einträge gleichmäßig verteilt)
|
||
const row1Y = LEG_Y + LEG_PAD + lineH * 0;
|
||
const moonColW = (LEG_W - LEG_PAD * 2) / 4;
|
||
MOON_KEYS.forEach((key, i) => {
|
||
const x = LEG_X + LEG_PAD + i * moonColW;
|
||
c.beginPath(); c.arc(x + 8, row1Y, 6, 0, Math.PI * 2);
|
||
c.fillStyle = MOON_COLORS[key]; c.fill();
|
||
c.font = `bold ${fontSize2}px monospace`;
|
||
c.fillStyle = MOON_COLORS[key];
|
||
c.fillText(MOON_LABELS[key], x + 22, row1Y);
|
||
});
|
||
|
||
// Zeile 2: Schattenstriche + GRS + Mehrfach
|
||
const row2Y = LEG_Y + LEG_PAD + lineH * 1.6;
|
||
const col2W = (LEG_W - LEG_PAD * 2) / 3;
|
||
c.font = `${fontSize2}px monospace`;
|
||
c.fillStyle = COL.legText;
|
||
|
||
// Schatten
|
||
let cx2 = LEG_X + LEG_PAD;
|
||
c.fillStyle = MOON_COLORS['io'];
|
||
c.fillRect(cx2, row2Y - 2, 28, 4);
|
||
c.fillStyle = COL.legText;
|
||
c.fillText('Mondschatten auf Jupiter', cx2 + 36, row2Y);
|
||
|
||
// GRS
|
||
cx2 = LEG_X + LEG_PAD + col2W;
|
||
c.font = `${fontSize2}px monospace`;
|
||
c.fillStyle = COL.grsCol;
|
||
c.fillText('03:42', cx2, row2Y);
|
||
c.fillStyle = COL.legText;
|
||
c.fillText(' GRS-Transit', cx2 + c.measureText('03:42').width, row2Y);
|
||
|
||
// Mehrfach
|
||
cx2 = LEG_X + LEG_PAD + col2W * 2;
|
||
c.font = `bold ${fontSize2}px monospace`;
|
||
c.fillStyle = '#c08000';
|
||
c.fillText('◆ Mehrfach-Schatten', cx2, row2Y);
|
||
|
||
// Fußlinie + Text
|
||
const footY = H - 14;
|
||
const footLineY = H - 34;
|
||
const footGrad = c.createLinearGradient(MARGIN_LEFT, 0, MARGIN_LEFT + chartW, 0);
|
||
footGrad.addColorStop(0, 'transparent');
|
||
footGrad.addColorStop(0.15, COL.goldLine);
|
||
footGrad.addColorStop(0.85, COL.goldLine);
|
||
footGrad.addColorStop(1, 'transparent');
|
||
c.strokeStyle = footGrad; c.lineWidth = 1;
|
||
c.beginPath(); c.moveTo(MARGIN_LEFT, footLineY); c.lineTo(MARGIN_LEFT + chartW, footLineY); c.stroke();
|
||
c.fillStyle = COL.footText;
|
||
c.font = '17px monospace';
|
||
c.textAlign = 'center';
|
||
c.textBaseline = 'alphabetic';
|
||
c.fillText('AstroTools · Astronomiemuseum der Sternwarte Sonneberg · IAU 031 · Alle Zeiten in UT', W / 2, footY);
|
||
|
||
return new Promise(resolve => {
|
||
const logo = new Image();
|
||
logo.onload = () => {
|
||
const logoH = 64;
|
||
const logoW = Math.round(logo.naturalWidth * (logoH / logo.naturalHeight));
|
||
const lx = W - MARGIN_RIGHT - logoW;
|
||
const ly = LEG_Y + (LEG_H - logoH) / 2;
|
||
c.drawImage(logo, lx, ly, logoW, logoH);
|
||
resolve({ cvs, year, month });
|
||
};
|
||
logo.onerror = () => resolve({ cvs, year, month }); // kein Logo → trotzdem fortfahren
|
||
logo.src = 'images/logoAM.png';
|
||
});
|
||
}
|
||
|
||
document.getElementById('jupiterBtnEphemeris').addEventListener('click', async () => {
|
||
const { cvs, year, month } = await generateEphemerisCanvas();
|
||
cvs.toBlob(blob => {
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `jupitermonde_${year}_${String(month + 1).padStart(2, '0')}.png`;
|
||
a.click();
|
||
setTimeout(() => URL.revokeObjectURL(url), 5000);
|
||
}, 'image/png');
|
||
});
|
||
</script>
|
||
|
||
<?php require __DIR__ . '/footer.php'; ?>
|