1379 lines
50 KiB
PHP
1379 lines
50 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
ini_set('display_errors', '1');
|
|
error_reporting(E_ALL);
|
|
session_start();
|
|
|
|
$loggedIn = isset($_SESSION['user_id']);
|
|
$trackedMinorplanetsForJs = [];
|
|
|
|
function h(?string $value): string
|
|
{
|
|
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
|
}
|
|
|
|
function decodeMpcPackedDateChar(string $value): ?int
|
|
{
|
|
if ($value >= '0' && $value <= '9') {
|
|
return (int) $value;
|
|
}
|
|
|
|
$upper = strtoupper($value);
|
|
if ($upper >= 'A' && $upper <= 'V') {
|
|
return ord($upper) - ord('A') + 10;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function decodeMpcPackedDate(?string $packed): ?string
|
|
{
|
|
if ($packed === null || strlen($packed) !== 5) {
|
|
return null;
|
|
}
|
|
|
|
$centuryMap = [
|
|
'I' => 1800,
|
|
'J' => 1900,
|
|
'K' => 2000,
|
|
];
|
|
|
|
$centuryCode = strtoupper($packed[0]);
|
|
if (!isset($centuryMap[$centuryCode])) {
|
|
return null;
|
|
}
|
|
|
|
$yearPart = substr($packed, 1, 2);
|
|
if (!ctype_digit($yearPart)) {
|
|
return null;
|
|
}
|
|
|
|
$month = decodeMpcPackedDateChar($packed[3]);
|
|
$day = decodeMpcPackedDateChar($packed[4]);
|
|
if ($month === null || $day === null || $month < 1 || $month > 12 || $day < 1 || $day > 31) {
|
|
return null;
|
|
}
|
|
|
|
return sprintf('%04d-%02d-%02d', $centuryMap[$centuryCode] + (int) $yearPart, $month, $day);
|
|
}
|
|
|
|
function formatMinorplanetDisplayName(array $minorplanet): string
|
|
{
|
|
$designation = trim((string) ($minorplanet['designation'] ?? $minorplanet['designation_text'] ?? ''));
|
|
$designation = preg_replace('/^\(\d+\)\s*/', '', $designation) ?? $designation;
|
|
$number = $minorplanet['number'] ?? $minorplanet['mp_number'] ?? null;
|
|
|
|
if ($number !== null && $number !== '') {
|
|
return '(' . (int) $number . ') ' . $designation;
|
|
}
|
|
|
|
return $designation !== '' ? $designation : (string) ($minorplanet['packedDesignation'] ?? $minorplanet['desig_packed'] ?? '--');
|
|
}
|
|
|
|
if ($loggedIn) {
|
|
try {
|
|
$config = require __DIR__ . '/../config/database.php';
|
|
$dsn = sprintf(
|
|
'mysql:host=%s;dbname=%s;charset=%s',
|
|
$config['host'],
|
|
$config['dbname'],
|
|
$config['charset'] ?? 'utf8mb4'
|
|
);
|
|
|
|
$pdo = new PDO(
|
|
$dsn,
|
|
$config['user'],
|
|
$config['pass'],
|
|
[
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
]
|
|
);
|
|
|
|
$stmtTrackedMinorplanets = $pdo->prepare("
|
|
SELECT
|
|
mp.`id`,
|
|
mp.`mp_number`,
|
|
mp.`designation_text`,
|
|
mp.`desig_packed`,
|
|
mp.`epoch_packed`,
|
|
mp.`mean_anomaly_deg`,
|
|
mp.`arg_perihelion_deg`,
|
|
mp.`ascending_node_deg`,
|
|
mp.`inclination_deg`,
|
|
mp.`eccentricity`,
|
|
mp.`mean_motion_deg_per_day`,
|
|
mp.`semimajor_axis_au`,
|
|
mp.`absolute_magnitude_h`,
|
|
mp.`slope_g`,
|
|
aum.`is_favorite`
|
|
FROM `app_user_minorplanets` aum
|
|
INNER JOIN `minorplanets_mpc` mp
|
|
ON mp.`id` = aum.`minorplanet_id`
|
|
WHERE aum.`user_id` = :user_id
|
|
ORDER BY aum.`is_favorite` DESC, mp.`mp_number` IS NULL ASC, mp.`mp_number` ASC, mp.`designation_text` ASC
|
|
");
|
|
$stmtTrackedMinorplanets->execute([
|
|
':user_id' => (int) $_SESSION['user_id'],
|
|
]);
|
|
|
|
foreach ($stmtTrackedMinorplanets->fetchAll() as $minorplanet) {
|
|
$trackedMinorplanetsForJs[] = [
|
|
'id' => (int) $minorplanet['id'],
|
|
'number' => $minorplanet['mp_number'] !== null ? (int) $minorplanet['mp_number'] : null,
|
|
'designation' => (string) $minorplanet['designation_text'],
|
|
'packedDesignation' => (string) $minorplanet['desig_packed'],
|
|
'epochPacked' => (string) $minorplanet['epoch_packed'],
|
|
'epochDateIso' => decodeMpcPackedDate($minorplanet['epoch_packed']),
|
|
'meanAnomalyDeg' => (float) $minorplanet['mean_anomaly_deg'],
|
|
'argPerihelionDeg' => (float) $minorplanet['arg_perihelion_deg'],
|
|
'ascendingNodeDeg' => (float) $minorplanet['ascending_node_deg'],
|
|
'inclinationDeg' => (float) $minorplanet['inclination_deg'],
|
|
'eccentricity' => (float) $minorplanet['eccentricity'],
|
|
'meanMotionDegPerDay' => (float) $minorplanet['mean_motion_deg_per_day'],
|
|
'semimajorAxisAu' => (float) $minorplanet['semimajor_axis_au'],
|
|
'absoluteMagnitudeH' => $minorplanet['absolute_magnitude_h'] !== null ? (float) $minorplanet['absolute_magnitude_h'] : null,
|
|
'slopeG' => $minorplanet['slope_g'] !== null ? (float) $minorplanet['slope_g'] : null,
|
|
'isFavorite' => (bool) $minorplanet['is_favorite'],
|
|
];
|
|
}
|
|
} catch (Throwable $e) {
|
|
$trackedMinorplanetsForJs = [];
|
|
}
|
|
}
|
|
|
|
$pageTitle = 'Sonnensystem - AstroTools';
|
|
$bodyClass = 'solarsystem-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>Sonnensystem</h2>
|
|
</div>
|
|
|
|
<div class="solarsystem-sidebar-block">
|
|
<div class="solarsystem-label">Datum</div>
|
|
<div class="solarsystem-input-row">
|
|
<input type="date" id="solarDatePicker">
|
|
<button type="button" class="btn btn-secondary" id="solarBtnToday">Heute</button>
|
|
</div>
|
|
<div class="solarsystem-current-date" id="solarCurrentDate">--</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="solarBtnPlay">Play</button>
|
|
<button type="button" class="btn btn-secondary" id="solarBtnReset">Jetzt</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="solarsystem-sidebar-block">
|
|
<div class="solarsystem-label">Geschwindigkeit</div>
|
|
<input type="range" id="solarSpeedSlider" min="-100" max="100" step="0.1" value="1">
|
|
<div class="solarsystem-speed-value" id="solarSpeedLabel">1 d/s</div>
|
|
</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="solarBtnTop">Top</button>
|
|
<button type="button" class="btn btn-secondary" id="solarBtnSide">Seite</button>
|
|
<button type="button" class="btn btn-secondary is-active" id="solarBtnOrbits">Orbits</button>
|
|
<button type="button" class="btn btn-secondary" id="solarBtnLabels">Namen</button>
|
|
<button type="button" class="btn btn-secondary" id="solarBtnEcliptic">Ekliptik</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="solarsystem-sidebar-block">
|
|
<div class="solarsystem-label">Kleinplanet</div>
|
|
<div class="solarsystem-input-row">
|
|
<select id="solarMinorplanetSelect"<?= empty($trackedMinorplanetsForJs) ? ' disabled' : '' ?>>
|
|
<option value=""><?= empty($trackedMinorplanetsForJs) ? 'Keine Kleinplaneten ausgewaehlt' : 'Kein Kleinplanet' ?></option>
|
|
<?php foreach ($trackedMinorplanetsForJs as $minorplanet): ?>
|
|
<option value="<?= (int) $minorplanet['id'] ?>"><?= h(formatMinorplanetDisplayName($minorplanet)) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="solarsystem-current-date solarsystem-minorplanet-hint" id="solarMinorplanetHint">
|
|
<?= empty($trackedMinorplanetsForJs) ? 'Fuege in den Favoriten Kleinplaneten hinzu, um hier ihren Orbit anzuzeigen.' : 'Der Orbit des gewaehlten Kleinplaneten wird eingeblendet.' ?>
|
|
</div>
|
|
</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 Planet: Fokus setzen</li>
|
|
<li>Dropdown: gewaehlten Kleinplaneten-Orbit zeigen</li>
|
|
<li>Erneuter Klick ins Leere: Fokus loesen</li>
|
|
</ul>
|
|
</div>
|
|
</aside>
|
|
|
|
<section class="card solarsystem-stage-card">
|
|
<div class="solarsystem-stage-wrap" id="solarStageWrap">
|
|
<canvas id="solarCanvas"></canvas>
|
|
<div id="solarLabels" class="solarsystem-label-layer"></div>
|
|
|
|
<div class="solarsystem-info-panel" id="solarInfoPanel" hidden>
|
|
<h3 id="solarInfoName">--</h3>
|
|
<div class="solarsystem-info-row" title="Aktueller Abstand des Objekts zur Sonne in Astronomischen Einheiten.">
|
|
<span title="Aktueller Abstand des Objekts zur Sonne in Astronomischen Einheiten.">Akt. Abstand</span>
|
|
<strong id="solarInfoDistanceAu" title="Aktueller Abstand des Objekts zur Sonne in Astronomischen Einheiten.">--</strong>
|
|
</div>
|
|
<div class="solarsystem-info-row" title="Aktueller Abstand des Objekts zur Sonne in Kilometern.">
|
|
<span title="Aktueller Abstand des Objekts zur Sonne in Kilometern.">Akt. Abstand km</span>
|
|
<strong id="solarInfoDistanceKm" title="Aktueller Abstand des Objekts zur Sonne in Kilometern.">--</strong>
|
|
</div>
|
|
<div class="solarsystem-info-row" title="Aktueller Abstand zwischen Erde und dem gewaehlten Objekt in Astronomischen Einheiten.">
|
|
<span title="Aktueller Abstand zwischen Erde und dem gewaehlten Objekt in Astronomischen Einheiten.">Abstand Erde</span>
|
|
<strong id="solarInfoEarthDistanceAu" title="Aktueller Abstand zwischen Erde und dem gewaehlten Objekt in Astronomischen Einheiten.">--</strong>
|
|
</div>
|
|
<div class="solarsystem-info-row" title="Aktueller Abstand zwischen Erde und dem gewaehlten Objekt in Kilometern.">
|
|
<span title="Aktueller Abstand zwischen Erde und dem gewaehlten Objekt in Kilometern.">Erde km</span>
|
|
<strong id="solarInfoEarthDistanceKm" title="Aktueller Abstand zwischen Erde und dem gewaehlten Objekt in Kilometern.">--</strong>
|
|
</div>
|
|
<div class="solarsystem-info-row" title="Scheinbare Helligkeit. Bei Kleinplaneten ist das ein geschaetzter Wert aus H-G-Modell, Sonnenabstand, Erdabstand und Phasenwinkel.">
|
|
<span title="Scheinbare Helligkeit. Bei Kleinplaneten ist das ein geschaetzter Wert aus H-G-Modell, Sonnenabstand, Erdabstand und Phasenwinkel.">Helligkeit</span>
|
|
<strong id="solarInfoMagnitude" title="Scheinbare Helligkeit. Bei Kleinplaneten ist das ein geschaetzter Wert aus H-G-Modell, Sonnenabstand, Erdabstand und Phasenwinkel.">--</strong>
|
|
</div>
|
|
<div class="solarsystem-info-row" title="Zeit fuer einen Umlauf um die Sonne.">
|
|
<span title="Zeit fuer einen Umlauf um die Sonne.">Umlaufzeit</span>
|
|
<strong id="solarInfoPeriod" title="Zeit fuer einen Umlauf um die Sonne.">--</strong>
|
|
</div>
|
|
<div class="solarsystem-info-row" title="Grosse Halbachse der Bahn, also der mittlere Bahnradius in Astronomischen Einheiten.">
|
|
<span title="Grosse Halbachse der Bahn, also der mittlere Bahnradius in Astronomischen Einheiten.">Achse</span>
|
|
<strong id="solarInfoAxis" title="Grosse Halbachse der Bahn, also der mittlere Bahnradius in Astronomischen Einheiten.">--</strong>
|
|
</div>
|
|
<div class="solarsystem-info-row" title="Objekttyp oder Zusatzinformation zum gewaehlten Himmelskoerper.">
|
|
<span title="Objekttyp oder Zusatzinformation zum gewaehlten Himmelskoerper.">Typ</span>
|
|
<strong id="solarInfoType" title="Objekttyp oder Zusatzinformation zum gewaehlten Himmelskoerper.">--</strong>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="solarsystem-overlay" id="solarOverlay">Initialisiere Visualisierung...</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="module">
|
|
const AstronomyEngine = window.Astronomy;
|
|
const AU_IN_KM = 149597870.7;
|
|
const DISPLAY_SCALE = 34;
|
|
const USER_MINOR_PLANETS = <?= json_encode($trackedMinorplanetsForJs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
|
const JUPITER_MIN_VISIBLE_SIZE = 1.42;
|
|
|
|
const PLANETS = [
|
|
{ key: 'Mercury', label: 'Merkur', color: '#aeb7c4', emissive: '#29303a', size: 0.34, type: 'Gesteinsplanet', roughness: 0.95, metalness: 0.00, glow: false, texture: 'images/planets/merkur_1024.jpg' },
|
|
{ key: 'Venus', label: 'Venus', color: '#e8d890', emissive: '#5a4412', size: 0.58, type: 'Gesteinsplanet', roughness: 0.90, metalness: 0.00, glow: true, texture: 'images/planets/venus_1024.jpg' },
|
|
{ key: 'Earth', label: 'Erde', color: '#4fb8ff', emissive: '#0a2e50', size: 0.62, type: 'Gesteinsplanet', roughness: 0.88, metalness: 0.00, glow: true, texture: 'images/planets/earth_day_1024.jpg' },
|
|
{ key: 'Mars', label: 'Mars', color: '#d45b2e', emissive: '#42160a', size: 0.46, type: 'Gesteinsplanet', roughness: 0.95, metalness: 0.00, glow: false, texture: 'images/planets/mars_1024.jpg' },
|
|
{ key: 'Jupiter', label: 'Jupiter',color: '#c89c63', emissive: '#3b2412', size: 1.42, type: 'Gasriese', roughness: 0.92, metalness: 0.00, glow: true, texture: 'images/planets/jupiter_1024.jpg' },
|
|
{ key: 'Saturn', label: 'Saturn', color: '#e8d29a', emissive: '#4a3a12', size: 1.18, type: 'Gasriese', roughness: 0.92, metalness: 0.00, glow: true, texture: 'images/planets/saturn_1024.jpg' },
|
|
{ key: 'Uranus', label: 'Uranus', color: '#7fe3e7', emissive: '#12353b', size: 0.94, type: 'Eisriese', roughness: 0.92, metalness: 0.00, glow: true, texture: 'images/planets/uranus_1024.jpg' },
|
|
{ key: 'Neptune', label: 'Neptun', color: '#4f78f0', emissive: '#101b46', size: 0.92, type: 'Eisriese', roughness: 0.92, metalness: 0.00, glow: true, texture: 'images/planets/neptun_1024.jpg' },
|
|
{ key: 'Pluto', label: 'Pluto', color: '#c5b090', emissive: '#3c2f20', size: 0.26, type: 'Zwergplanet', roughness: 0.95, metalness: 0.00, glow: false, texture: 'images/planets/pluto_1024.jpg' }
|
|
];
|
|
|
|
const stageWrap = document.getElementById('solarStageWrap');
|
|
const canvas = document.getElementById('solarCanvas');
|
|
const labelsLayer = document.getElementById('solarLabels');
|
|
const overlay = document.getElementById('solarOverlay');
|
|
const datePicker = document.getElementById('solarDatePicker');
|
|
const currentDateLabel = document.getElementById('solarCurrentDate');
|
|
const btnToday = document.getElementById('solarBtnToday');
|
|
const btnPlay = document.getElementById('solarBtnPlay');
|
|
const btnReset = document.getElementById('solarBtnReset');
|
|
const btnTop = document.getElementById('solarBtnTop');
|
|
const btnSide = document.getElementById('solarBtnSide');
|
|
const btnOrbits = document.getElementById('solarBtnOrbits');
|
|
const btnLabels = document.getElementById('solarBtnLabels');
|
|
const btnEcliptic = document.getElementById('solarBtnEcliptic');
|
|
const speedSlider = document.getElementById('solarSpeedSlider');
|
|
const speedLabel = document.getElementById('solarSpeedLabel');
|
|
const infoPanel = document.getElementById('solarInfoPanel');
|
|
const minorPlanetSelect = document.getElementById('solarMinorplanetSelect');
|
|
|
|
if (!AstronomyEngine) {
|
|
overlay.textContent = 'Astronomy Engine konnte nicht geladen werden.';
|
|
throw new Error('Astronomy Engine unavailable');
|
|
}
|
|
|
|
let THREE;
|
|
let textureLoader;
|
|
let renderer;
|
|
let scene;
|
|
let camera;
|
|
let planetMeshes;
|
|
let orbitLines;
|
|
let labelElements;
|
|
let raycaster;
|
|
let pointer;
|
|
let eclipticGroup;
|
|
let rotationEqjToEcl;
|
|
let minorPlanetOrbitLine = null;
|
|
let minorPlanetMesh = null;
|
|
let minorPlanetLabelElement = null;
|
|
const planetDynamicsCache = new Map();
|
|
|
|
let showOrbits = true;
|
|
let showLabels = false;
|
|
let showEcliptic = false;
|
|
let playing = false;
|
|
let speedDaysPerSecond = 1;
|
|
let focusedPlanetKey = null;
|
|
let selectedMinorPlanetId = null;
|
|
let focusedMinorPlanetId = null;
|
|
let simDate = new Date();
|
|
let lastAnimationTime = performance.now();
|
|
let orbitRefreshCursor = 0;
|
|
let cameraTarget;
|
|
let camTheta = 0.45;
|
|
let camPhi = 1.05;
|
|
let camRadius = 190;
|
|
let camRadiusTarget = 190;
|
|
let isDragging = false;
|
|
let pointerDownMoved = false;
|
|
let lastPointerX = 0;
|
|
let lastPointerY = 0;
|
|
|
|
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) {
|
|
if (Math.abs(daysPerSecond) < 0.05) {
|
|
return '0 d/s';
|
|
}
|
|
|
|
if (Math.abs(daysPerSecond) >= 1) {
|
|
return `${daysPerSecond.toFixed(1)} d/s`;
|
|
}
|
|
|
|
return `${(daysPerSecond * 24).toFixed(1)} h/s`;
|
|
}
|
|
|
|
function updateSpeedFromSlider() {
|
|
speedDaysPerSecond = Number(speedSlider.value);
|
|
speedLabel.textContent = normalizedSpeedText(speedDaysPerSecond);
|
|
}
|
|
|
|
function getPlanetMeta(key) {
|
|
return PLANETS.find((planet) => planet.key === key) || null;
|
|
}
|
|
|
|
function getPlanetBodyKey(key) {
|
|
return AstronomyEngine.Body && AstronomyEngine.Body[key] ? AstronomyEngine.Body[key] : key;
|
|
}
|
|
|
|
function getPlanetOrbitalPeriodDays(key) {
|
|
try {
|
|
return AstronomyEngine.PlanetOrbitalPeriod(getPlanetBodyKey(key));
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function calculatePlanetSemimajorAxisAu(key, periodDays) {
|
|
const referenceDate = new Date('2000-01-01T12:00:00Z');
|
|
const steps = Math.max(240, Math.min(960, Math.round(periodDays / 20)));
|
|
let minRadius = Number.POSITIVE_INFINITY;
|
|
let maxRadius = 0;
|
|
|
|
for (let step = 0; step <= steps; step += 1) {
|
|
const sampleDate = new Date(referenceDate.getTime() + ((periodDays * step) / steps) * 86400000);
|
|
const radius = getHelioPosition(key, sampleDate).r;
|
|
if (radius < minRadius) {
|
|
minRadius = radius;
|
|
}
|
|
if (radius > maxRadius) {
|
|
maxRadius = radius;
|
|
}
|
|
}
|
|
|
|
if (!Number.isFinite(minRadius) || !Number.isFinite(maxRadius)) {
|
|
return null;
|
|
}
|
|
|
|
return (minRadius + maxRadius) / 2;
|
|
}
|
|
|
|
function getPlanetDynamics(key) {
|
|
if (planetDynamicsCache.has(key)) {
|
|
return planetDynamicsCache.get(key);
|
|
}
|
|
|
|
const periodDays = getPlanetOrbitalPeriodDays(key);
|
|
const axisAu = periodDays !== null ? calculatePlanetSemimajorAxisAu(key, periodDays) : null;
|
|
const dynamics = { periodDays, axisAu };
|
|
planetDynamicsCache.set(key, dynamics);
|
|
return dynamics;
|
|
}
|
|
|
|
function getMinorPlanetMeta(id) {
|
|
return USER_MINOR_PLANETS.find((minorPlanet) => minorPlanet.id === Number(id)) || null;
|
|
}
|
|
|
|
function formatMinorPlanetName(minorPlanet) {
|
|
if (!minorPlanet) {
|
|
return '--';
|
|
}
|
|
|
|
const designation = String(minorPlanet.designation || '').trim();
|
|
const designationWithoutLeadingNumber = designation.replace(/^\(\d+\)\s*/, '');
|
|
return minorPlanet.number !== null
|
|
? `(${minorPlanet.number}) ${designationWithoutLeadingNumber}`
|
|
: designation;
|
|
}
|
|
|
|
function getHelioPosition(key, date) {
|
|
const body = getPlanetBodyKey(key);
|
|
const vectorEqj = AstronomyEngine.HelioVector(body, date);
|
|
const vectorEcl = rotationEqjToEcl
|
|
? AstronomyEngine.RotateVector(rotationEqjToEcl, vectorEqj)
|
|
: vectorEqj;
|
|
const radiusAu = Math.hypot(vectorEcl.x, vectorEcl.y, vectorEcl.z);
|
|
return { x: vectorEcl.x, y: vectorEcl.y, z: vectorEcl.z, r: radiusAu };
|
|
}
|
|
|
|
function getPlanetApparentMagnitude(key, date) {
|
|
try {
|
|
const body = AstronomyEngine.Body && AstronomyEngine.Body[key] ? AstronomyEngine.Body[key] : key;
|
|
const illumination = AstronomyEngine.Illumination(body, date);
|
|
return illumination && Number.isFinite(illumination.mag) ? illumination.mag : null;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function toDisplayPosition(position) {
|
|
const radiusAu = Math.max(position.r, 1e-9);
|
|
const displayRadius = Math.pow(radiusAu, 0.56) * DISPLAY_SCALE;
|
|
const scale = displayRadius / radiusAu;
|
|
return new THREE.Vector3(
|
|
position.x * scale,
|
|
position.z * scale,
|
|
-position.y * scale
|
|
);
|
|
}
|
|
|
|
function refreshCurrentDateUi() {
|
|
currentDateLabel.textContent = formatDisplayDate(simDate);
|
|
datePicker.value = formatDateInput(simDate);
|
|
}
|
|
|
|
function resizeRenderer() {
|
|
const width = stageWrap.clientWidth;
|
|
const height = Math.max(520, stageWrap.clientHeight);
|
|
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 clearFocus() {
|
|
focusedPlanetKey = null;
|
|
focusedMinorPlanetId = null;
|
|
cameraTarget.set(0, 0, 0);
|
|
camRadiusTarget = camRadius;
|
|
document.querySelectorAll('.solarsystem-planet-chip').forEach((button) => {
|
|
button.classList.remove('is-focused');
|
|
});
|
|
infoPanel.hidden = true;
|
|
updateCamera();
|
|
}
|
|
|
|
function updatePlanetInfoPanel(key) {
|
|
const meta = getPlanetMeta(key);
|
|
if (!meta) {
|
|
infoPanel.hidden = true;
|
|
return;
|
|
}
|
|
|
|
const position = getHelioPosition(key, simDate);
|
|
const earthPosition = getHelioPosition('Earth', simDate);
|
|
const earthDistanceAu = Math.hypot(
|
|
position.x - earthPosition.x,
|
|
position.y - earthPosition.y,
|
|
position.z - earthPosition.z
|
|
);
|
|
const dynamics = getPlanetDynamics(key);
|
|
infoPanel.hidden = false;
|
|
document.getElementById('solarInfoName').textContent = meta.label;
|
|
document.getElementById('solarInfoName').style.color = meta.color;
|
|
document.getElementById('solarInfoDistanceAu').textContent = `${position.r.toFixed(4)} AU`;
|
|
document.getElementById('solarInfoDistanceKm').textContent = `${(position.r * AU_IN_KM).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, '.')} km`;
|
|
document.getElementById('solarInfoEarthDistanceAu').textContent = `${earthDistanceAu.toFixed(4)} AU`;
|
|
document.getElementById('solarInfoEarthDistanceKm').textContent = `${(earthDistanceAu * AU_IN_KM).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, '.')} km`;
|
|
const apparentMagnitude = getPlanetApparentMagnitude(key, simDate);
|
|
document.getElementById('solarInfoMagnitude').textContent = apparentMagnitude === null ? '--' : `${apparentMagnitude.toFixed(1)} mag`;
|
|
document.getElementById('solarInfoPeriod').textContent = dynamics.periodDays === null
|
|
? '--'
|
|
: dynamics.periodDays >= 365
|
|
? `${(dynamics.periodDays / 365.25).toFixed(2)} Jahre`
|
|
: `${dynamics.periodDays.toFixed(2)} Tage`;
|
|
document.getElementById('solarInfoAxis').textContent = dynamics.axisAu === null ? '--' : `${dynamics.axisAu.toFixed(3)} AU`;
|
|
document.getElementById('solarInfoType').textContent = meta.type;
|
|
}
|
|
|
|
function updateMinorPlanetInfoPanel(id) {
|
|
const minorPlanet = getMinorPlanetMeta(id);
|
|
if (!minorPlanet) {
|
|
infoPanel.hidden = true;
|
|
return;
|
|
}
|
|
|
|
const position = getMinorPlanetHelioPosition(minorPlanet, simDate);
|
|
if (!position) {
|
|
infoPanel.hidden = true;
|
|
return;
|
|
}
|
|
|
|
const earthPosition = getHelioPosition('Earth', simDate);
|
|
const earthDistanceAu = Math.hypot(
|
|
position.x - earthPosition.x,
|
|
position.y - earthPosition.y,
|
|
position.z - earthPosition.z
|
|
);
|
|
const apparentMagnitude = estimateMinorPlanetMagnitude(minorPlanet, position, earthPosition, earthDistanceAu);
|
|
const periodDays = minorPlanet.meanMotionDegPerDay > 0 ? 360 / minorPlanet.meanMotionDegPerDay : null;
|
|
infoPanel.hidden = false;
|
|
document.getElementById('solarInfoName').textContent = formatMinorPlanetName(minorPlanet);
|
|
document.getElementById('solarInfoName').style.color = '#ff2a2a';
|
|
document.getElementById('solarInfoDistanceAu').textContent = `${position.r.toFixed(4)} AU`;
|
|
document.getElementById('solarInfoDistanceKm').textContent = `${(position.r * AU_IN_KM).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, '.')} km`;
|
|
document.getElementById('solarInfoEarthDistanceAu').textContent = `${earthDistanceAu.toFixed(4)} AU`;
|
|
document.getElementById('solarInfoEarthDistanceKm').textContent = `${(earthDistanceAu * AU_IN_KM).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, '.')} km`;
|
|
document.getElementById('solarInfoMagnitude').textContent = apparentMagnitude === null ? '--' : `${apparentMagnitude.toFixed(1)} mag (geschaetzt)`;
|
|
document.getElementById('solarInfoPeriod').textContent = periodDays === null
|
|
? '--'
|
|
: periodDays >= 365
|
|
? `${(periodDays / 365.25).toFixed(2)} Jahre`
|
|
: `${periodDays.toFixed(2)} Tage`;
|
|
document.getElementById('solarInfoAxis').textContent = `${minorPlanet.semimajorAxisAu.toFixed(3)} AU`;
|
|
document.getElementById('solarInfoType').textContent = `Kleinplanet${minorPlanet.absoluteMagnitudeH !== null ? ` | H ${minorPlanet.absoluteMagnitudeH.toFixed(1)}` : ''}`;
|
|
}
|
|
|
|
function focusPlanet(key) {
|
|
focusedPlanetKey = key;
|
|
document.querySelectorAll('.solarsystem-planet-chip').forEach((button) => {
|
|
button.classList.toggle('is-focused', button.dataset.planet === key);
|
|
});
|
|
updatePlanetInfoPanel(key);
|
|
}
|
|
|
|
function buildLabels() {
|
|
PLANETS.forEach((planet) => {
|
|
const label = document.createElement('div');
|
|
label.className = 'solarsystem-planet-label';
|
|
label.textContent = planet.label;
|
|
label.style.color = planet.color;
|
|
label.hidden = true;
|
|
labelsLayer.appendChild(label);
|
|
labelElements.set(planet.key, label);
|
|
});
|
|
}
|
|
|
|
function ensureMinorPlanetObjects() {
|
|
if (minorPlanetOrbitLine && minorPlanetMesh && minorPlanetLabelElement) {
|
|
return;
|
|
}
|
|
|
|
const lineMaterial = new THREE.LineBasicMaterial({
|
|
color: 0xff3333,
|
|
transparent: true,
|
|
opacity: 0.85
|
|
});
|
|
minorPlanetOrbitLine = new THREE.Line(new THREE.BufferGeometry(), lineMaterial);
|
|
minorPlanetOrbitLine.visible = false;
|
|
scene.add(minorPlanetOrbitLine);
|
|
|
|
minorPlanetMesh = new THREE.Mesh(
|
|
new THREE.SphereGeometry(0.58, 28, 28),
|
|
new THREE.MeshStandardMaterial({
|
|
color: 0xff3030,
|
|
emissive: new THREE.Color(0xcc0000),
|
|
emissiveIntensity: 0.06,
|
|
roughness: 0.65,
|
|
metalness: 0.08,
|
|
})
|
|
);
|
|
minorPlanetMesh.visible = false;
|
|
minorPlanetMesh.userData.baseScale = 1;
|
|
minorPlanetMesh.userData.minScaleMultiplier = 1;
|
|
scene.add(minorPlanetMesh);
|
|
|
|
minorPlanetLabelElement = document.createElement('div');
|
|
minorPlanetLabelElement.className = 'solarsystem-planet-label';
|
|
minorPlanetLabelElement.style.color = '#ff2a2a';
|
|
minorPlanetLabelElement.hidden = true;
|
|
labelsLayer.appendChild(minorPlanetLabelElement);
|
|
}
|
|
|
|
function normalizeRadians(value) {
|
|
const tau = Math.PI * 2;
|
|
return ((value % tau) + tau) % tau;
|
|
}
|
|
|
|
function clamp(value, min, max) {
|
|
return Math.min(max, Math.max(min, value));
|
|
}
|
|
|
|
function estimateMinorPlanetMagnitude(minorPlanet, helioPosition, earthHelioPosition, earthDistanceAu) {
|
|
if (!minorPlanet || minorPlanet.absoluteMagnitudeH === null) {
|
|
return null;
|
|
}
|
|
|
|
const r = Math.max(helioPosition.r, 1e-9);
|
|
const delta = Math.max(earthDistanceAu, 1e-9);
|
|
const earthSunDistance = Math.max(earthHelioPosition.r, 1e-9);
|
|
const cosPhase = clamp(
|
|
(r * r + delta * delta - earthSunDistance * earthSunDistance) / (2 * r * delta),
|
|
-1,
|
|
1
|
|
);
|
|
const phaseAngle = Math.acos(cosPhase);
|
|
const tanHalfPhase = Math.tan(phaseAngle / 2);
|
|
const phi1 = Math.exp(-3.33 * Math.pow(Math.max(tanHalfPhase, 0), 0.63));
|
|
const phi2 = Math.exp(-1.87 * Math.pow(Math.max(tanHalfPhase, 0), 1.22));
|
|
const g = minorPlanet.slopeG ?? 0.15;
|
|
const phaseTerm = ((1 - g) * phi1) + (g * phi2);
|
|
|
|
if (!Number.isFinite(phaseTerm) || phaseTerm <= 0) {
|
|
return null;
|
|
}
|
|
|
|
return minorPlanet.absoluteMagnitudeH + (5 * Math.log10(r * delta)) - (2.5 * Math.log10(phaseTerm));
|
|
}
|
|
|
|
function solveKeplerEquation(meanAnomaly, eccentricity) {
|
|
let eccentricAnomaly = eccentricity < 0.8 ? meanAnomaly : Math.PI;
|
|
for (let i = 0; i < 12; i += 1) {
|
|
const delta = (eccentricAnomaly - eccentricity * Math.sin(eccentricAnomaly) - meanAnomaly)
|
|
/ (1 - eccentricity * Math.cos(eccentricAnomaly));
|
|
eccentricAnomaly -= delta;
|
|
if (Math.abs(delta) < 1e-7) {
|
|
break;
|
|
}
|
|
}
|
|
return eccentricAnomaly;
|
|
}
|
|
|
|
function getMinorPlanetHelioPosition(minorPlanet, date) {
|
|
if (!minorPlanet || !minorPlanet.epochDateIso) {
|
|
return null;
|
|
}
|
|
|
|
const epochMs = Date.parse(`${minorPlanet.epochDateIso}T00:00:00Z`);
|
|
if (Number.isNaN(epochMs)) {
|
|
return null;
|
|
}
|
|
|
|
const daysSinceEpoch = (date.getTime() - epochMs) / 86400000;
|
|
const meanAnomaly = normalizeRadians((minorPlanet.meanAnomalyDeg + (minorPlanet.meanMotionDegPerDay * daysSinceEpoch)) * Math.PI / 180);
|
|
const eccentricity = Math.max(0, Math.min(0.999999, minorPlanet.eccentricity));
|
|
const eccentricAnomaly = solveKeplerEquation(meanAnomaly, eccentricity);
|
|
const trueAnomaly = 2 * Math.atan2(
|
|
Math.sqrt(1 + eccentricity) * Math.sin(eccentricAnomaly / 2),
|
|
Math.sqrt(1 - eccentricity) * Math.cos(eccentricAnomaly / 2)
|
|
);
|
|
const radiusAu = minorPlanet.semimajorAxisAu * (1 - eccentricity * Math.cos(eccentricAnomaly));
|
|
const inclination = minorPlanet.inclinationDeg * Math.PI / 180;
|
|
const ascendingNode = minorPlanet.ascendingNodeDeg * Math.PI / 180;
|
|
const argPerihelion = minorPlanet.argPerihelionDeg * Math.PI / 180;
|
|
const argumentOfLatitude = trueAnomaly + argPerihelion;
|
|
|
|
return {
|
|
x: radiusAu * ((Math.cos(ascendingNode) * Math.cos(argumentOfLatitude)) - (Math.sin(ascendingNode) * Math.sin(argumentOfLatitude) * Math.cos(inclination))),
|
|
y: radiusAu * ((Math.sin(ascendingNode) * Math.cos(argumentOfLatitude)) + (Math.cos(ascendingNode) * Math.sin(argumentOfLatitude) * Math.cos(inclination))),
|
|
z: radiusAu * (Math.sin(argumentOfLatitude) * Math.sin(inclination)),
|
|
r: radiusAu
|
|
};
|
|
}
|
|
|
|
function rebuildMinorPlanetOrbit(id) {
|
|
ensureMinorPlanetObjects();
|
|
|
|
const minorPlanet = getMinorPlanetMeta(id);
|
|
if (!minorPlanet) {
|
|
minorPlanetOrbitLine.visible = false;
|
|
minorPlanetMesh.visible = false;
|
|
minorPlanetLabelElement.hidden = true;
|
|
return;
|
|
}
|
|
|
|
const periodDays = minorPlanet.meanMotionDegPerDay > 0 ? 360 / minorPlanet.meanMotionDegPerDay : 365;
|
|
const steps = Math.max(180, Math.min(520, Math.round(periodDays / 8)));
|
|
const positions = [];
|
|
|
|
for (let step = 0; step <= steps; step += 1) {
|
|
const orbitDate = new Date(Date.parse(`${minorPlanet.epochDateIso}T00:00:00Z`) + ((periodDays * step) / steps) * 86400000);
|
|
const heliocentric = getMinorPlanetHelioPosition(minorPlanet, orbitDate);
|
|
if (!heliocentric) {
|
|
continue;
|
|
}
|
|
const display = toDisplayPosition(heliocentric);
|
|
positions.push(display.x, display.y, display.z);
|
|
}
|
|
|
|
minorPlanetOrbitLine.geometry.dispose();
|
|
minorPlanetOrbitLine.geometry = new THREE.BufferGeometry();
|
|
minorPlanetOrbitLine.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
minorPlanetOrbitLine.visible = showOrbits;
|
|
}
|
|
|
|
function updateMinorPlanetVisual() {
|
|
ensureMinorPlanetObjects();
|
|
|
|
const minorPlanet = getMinorPlanetMeta(selectedMinorPlanetId);
|
|
if (!minorPlanet) {
|
|
minorPlanetOrbitLine.visible = false;
|
|
minorPlanetMesh.visible = false;
|
|
minorPlanetLabelElement.hidden = true;
|
|
if (!focusedPlanetKey) {
|
|
infoPanel.hidden = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const heliocentric = getMinorPlanetHelioPosition(minorPlanet, simDate);
|
|
if (!heliocentric) {
|
|
minorPlanetOrbitLine.visible = false;
|
|
minorPlanetMesh.visible = false;
|
|
minorPlanetLabelElement.hidden = true;
|
|
return;
|
|
}
|
|
|
|
const display = toDisplayPosition(heliocentric);
|
|
minorPlanetMesh.visible = true;
|
|
minorPlanetMesh.position.copy(display);
|
|
minorPlanetMesh.rotation.y += 0.01;
|
|
minorPlanetOrbitLine.visible = showOrbits;
|
|
|
|
if (focusedMinorPlanetId === selectedMinorPlanetId) {
|
|
updateMinorPlanetInfoPanel(selectedMinorPlanetId);
|
|
}
|
|
}
|
|
|
|
function createOrbitLine(planet) {
|
|
const periodDays = getPlanetDynamics(planet.key).periodDays ?? 365;
|
|
const steps = planet.key === 'Pluto' ? 300 : 220;
|
|
const positions = [];
|
|
for (let step = 0; step <= steps; step += 1) {
|
|
const offsetDays = (step / steps) * periodDays;
|
|
const date = new Date(simDate.getTime() + offsetDays * 86400000);
|
|
const heliocentric = getHelioPosition(planet.key, date);
|
|
const display = toDisplayPosition(heliocentric);
|
|
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(planet.color).multiplyScalar(0.9),
|
|
transparent: true,
|
|
opacity: 0.55,
|
|
});
|
|
const line = new THREE.Line(geometry, material);
|
|
line.visible = showOrbits;
|
|
orbitLines.set(planet.key, line);
|
|
scene.add(line);
|
|
}
|
|
|
|
function rebuildOrbitLine(planet) {
|
|
const line = orbitLines.get(planet.key);
|
|
if (!line) {
|
|
return;
|
|
}
|
|
|
|
const periodDays = getPlanetDynamics(planet.key).periodDays ?? 365;
|
|
const steps = planet.key === 'Pluto' ? 300 : 220;
|
|
const positions = [];
|
|
for (let step = 0; step <= steps; step += 1) {
|
|
const offsetDays = (step / steps) * periodDays;
|
|
const date = new Date(simDate.getTime() + offsetDays * 86400000);
|
|
const heliocentric = getHelioPosition(planet.key, date);
|
|
const display = toDisplayPosition(heliocentric);
|
|
positions.push(display.x, display.y, display.z);
|
|
}
|
|
|
|
line.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
line.geometry.attributes.position.needsUpdate = true;
|
|
}
|
|
|
|
function createPlanetMesh(planet) {
|
|
const group = new THREE.Group();
|
|
const matParams = {
|
|
emissive: new THREE.Color(planet.emissive),
|
|
emissiveIntensity: 0.12,
|
|
roughness: planet.roughness ?? 0.7,
|
|
metalness: planet.metalness ?? 0.05,
|
|
};
|
|
matParams.color = new THREE.Color(planet.texture ? 0xffffff : planet.color);
|
|
if (planet.texture && textureLoader) {
|
|
const tex = textureLoader.load(
|
|
planet.texture,
|
|
(loaded) => { loaded.colorSpace = THREE.SRGBColorSpace; },
|
|
undefined,
|
|
() => { mesh.material.map = null; mesh.material.color.set(planet.color); mesh.material.needsUpdate = true; }
|
|
);
|
|
tex.colorSpace = THREE.SRGBColorSpace;
|
|
matParams.map = tex;
|
|
}
|
|
const mesh = new THREE.Mesh(
|
|
new THREE.SphereGeometry(planet.size, 48, 48),
|
|
new THREE.MeshStandardMaterial(matParams)
|
|
);
|
|
mesh.castShadow = true;
|
|
mesh.receiveShadow = true;
|
|
group.add(mesh);
|
|
|
|
|
|
if (planet.key === 'Saturn') {
|
|
const ringInner = planet.size * 1.3;
|
|
const ringOuter = planet.size * 2.38;
|
|
const ringGeo = new THREE.RingGeometry(ringInner, ringOuter, 128, 8);
|
|
|
|
// Fix UV mapping: U = radial (0=inner, 1=outer)
|
|
const pos = ringGeo.attributes.position;
|
|
const uv = ringGeo.attributes.uv;
|
|
for (let i = 0; i < pos.count; i++) {
|
|
const x = pos.getX(i);
|
|
const y = pos.getY(i);
|
|
const r = Math.sqrt(x * x + y * y);
|
|
uv.setXY(i, (r - ringInner) / (ringOuter - ringInner), 0.5);
|
|
}
|
|
uv.needsUpdate = true;
|
|
|
|
const ringTex = textureLoader.load('images/planets/saturn_ring_8192.png');
|
|
ringTex.colorSpace = THREE.SRGBColorSpace;
|
|
const ring = new THREE.Mesh(
|
|
ringGeo,
|
|
new THREE.MeshStandardMaterial({
|
|
map: ringTex,
|
|
transparent: true,
|
|
alphaTest: 0.05,
|
|
side: THREE.DoubleSide,
|
|
depthWrite: true,
|
|
roughness: 1.0,
|
|
metalness: 0.0,
|
|
emissive: new THREE.Color(0x000000),
|
|
})
|
|
);
|
|
ring.castShadow = true;
|
|
ring.receiveShadow = true;
|
|
ring.rotation.x = Math.PI * 0.43;
|
|
group.add(ring);
|
|
}
|
|
|
|
group.userData.planetKey = planet.key;
|
|
group.userData.baseScale = 1;
|
|
group.userData.minScaleMultiplier = Math.max(JUPITER_MIN_VISIBLE_SIZE / Math.max(planet.size, 0.001), 1);
|
|
scene.add(group);
|
|
planetMeshes.set(planet.key, group);
|
|
createOrbitLine(planet);
|
|
}
|
|
|
|
function createEclipticPlane() {
|
|
eclipticGroup = new THREE.Group();
|
|
|
|
const grid = new THREE.GridHelper(340, 34, 0x5aa8d8, 0x2c4d66);
|
|
grid.material.transparent = true;
|
|
grid.material.opacity = 0.28;
|
|
eclipticGroup.add(grid);
|
|
|
|
const axisGeometry = new THREE.BufferGeometry().setFromPoints([
|
|
new THREE.Vector3(0, -170, 0),
|
|
new THREE.Vector3(0, 170, 0)
|
|
]);
|
|
const axisMaterial = new THREE.LineBasicMaterial({
|
|
color: 0xffdd55,
|
|
transparent: true,
|
|
opacity: 0.95
|
|
});
|
|
const verticalAxis = new THREE.Line(axisGeometry, axisMaterial);
|
|
eclipticGroup.add(verticalAxis);
|
|
|
|
eclipticGroup.visible = showEcliptic;
|
|
scene.add(eclipticGroup);
|
|
}
|
|
|
|
function updatePlanetPositions() {
|
|
PLANETS.forEach((planet) => {
|
|
const meshGroup = planetMeshes.get(planet.key);
|
|
if (!meshGroup) {
|
|
return;
|
|
}
|
|
|
|
const heliocentric = getHelioPosition(planet.key, simDate);
|
|
const display = toDisplayPosition(heliocentric);
|
|
meshGroup.position.copy(display);
|
|
meshGroup.children[0].rotation.y += 0.004;
|
|
});
|
|
}
|
|
|
|
function applyMinimumDisplayScale(object3D, minVisibleDistance, maxScaleMultiplier) {
|
|
if (!object3D || !camera) {
|
|
return;
|
|
}
|
|
|
|
const baseScale = Number(object3D.userData.baseScale || 1);
|
|
const minScaleMultiplier = Number(object3D.userData.minScaleMultiplier || 1);
|
|
const distanceToCamera = camera.position.distanceTo(object3D.position);
|
|
const scaleMultiplier = Math.min(
|
|
Math.max(distanceToCamera / Math.max(minVisibleDistance, 1), minScaleMultiplier),
|
|
maxScaleMultiplier
|
|
);
|
|
object3D.scale.setScalar(baseScale * scaleMultiplier);
|
|
}
|
|
|
|
function updateBodyDisplayScales() {
|
|
planetMeshes.forEach((group) => {
|
|
applyMinimumDisplayScale(group, 180, 6);
|
|
});
|
|
|
|
if (minorPlanetMesh && minorPlanetMesh.visible) {
|
|
applyMinimumDisplayScale(minorPlanetMesh, 220, 10);
|
|
}
|
|
}
|
|
|
|
function updateLabels() {
|
|
labelElements.forEach((label, key) => {
|
|
label.hidden = !showLabels;
|
|
if (!showLabels) {
|
|
return;
|
|
}
|
|
|
|
const meshGroup = planetMeshes.get(key);
|
|
if (!meshGroup) {
|
|
label.hidden = true;
|
|
return;
|
|
}
|
|
|
|
const projected = meshGroup.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`;
|
|
});
|
|
|
|
if (!minorPlanetLabelElement || !minorPlanetMesh) {
|
|
return;
|
|
}
|
|
|
|
const minorPlanet = getMinorPlanetMeta(selectedMinorPlanetId);
|
|
minorPlanetLabelElement.hidden = !showLabels || !minorPlanet || !minorPlanetMesh.visible;
|
|
if (minorPlanetLabelElement.hidden) {
|
|
return;
|
|
}
|
|
|
|
const projected = minorPlanetMesh.position.clone().project(camera);
|
|
if (projected.z > 1) {
|
|
minorPlanetLabelElement.style.opacity = '0';
|
|
return;
|
|
}
|
|
|
|
minorPlanetLabelElement.textContent = formatMinorPlanetName(minorPlanet);
|
|
minorPlanetLabelElement.style.opacity = '1';
|
|
minorPlanetLabelElement.style.left = `${(projected.x * 0.5 + 0.5) * stageWrap.clientWidth}px`;
|
|
minorPlanetLabelElement.style.top = `${(-projected.y * 0.5 + 0.5) * stageWrap.clientHeight}px`;
|
|
}
|
|
|
|
function pickPlanet(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 meshes = [];
|
|
planetMeshes.forEach((group, key) => {
|
|
meshes.push({ key, mesh: group.children[0] });
|
|
});
|
|
|
|
const hits = raycaster.intersectObjects(meshes.map((item) => item.mesh), false);
|
|
if (minorPlanetMesh && minorPlanetMesh.visible) {
|
|
const minorPlanetHits = raycaster.intersectObject(minorPlanetMesh, false);
|
|
if (minorPlanetHits.length > 0 && selectedMinorPlanetId) {
|
|
focusedPlanetKey = null;
|
|
focusedMinorPlanetId = selectedMinorPlanetId;
|
|
updateMinorPlanetInfoPanel(selectedMinorPlanetId);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (hits.length === 0) {
|
|
clearFocus();
|
|
return;
|
|
}
|
|
|
|
const hit = meshes.find((item) => item.mesh === hits[0].object);
|
|
if (hit) {
|
|
focusedMinorPlanetId = null;
|
|
focusPlanet(hit.key);
|
|
}
|
|
}
|
|
|
|
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.14, Math.min(Math.PI - 0.14, camPhi + dy * 0.005));
|
|
lastPointerX = event.clientX;
|
|
lastPointerY = event.clientY;
|
|
if (!focusedPlanetKey) {
|
|
updateCamera();
|
|
}
|
|
});
|
|
|
|
canvas.addEventListener('pointerup', (event) => {
|
|
if (!pointerDownMoved) {
|
|
pickPlanet(event.clientX, event.clientY);
|
|
}
|
|
isDragging = false;
|
|
canvas.releasePointerCapture(event.pointerId);
|
|
});
|
|
|
|
canvas.addEventListener('wheel', (event) => {
|
|
event.preventDefault();
|
|
camRadiusTarget = Math.max(12, Math.min(900, camRadiusTarget * (1 + event.deltaY * 0.001)));
|
|
camRadius = camRadiusTarget;
|
|
updateCamera();
|
|
}, { passive: false });
|
|
}
|
|
|
|
function attachUiHandlers() {
|
|
btnToday.addEventListener('click', () => {
|
|
simDate = new Date();
|
|
refreshCurrentDateUi();
|
|
});
|
|
|
|
btnReset.addEventListener('click', () => {
|
|
simDate = new Date();
|
|
clearFocus();
|
|
refreshCurrentDateUi();
|
|
});
|
|
|
|
btnPlay.addEventListener('click', () => {
|
|
playing = !playing;
|
|
btnPlay.textContent = playing ? 'Pause' : 'Play';
|
|
btnPlay.classList.toggle('is-active', playing);
|
|
});
|
|
|
|
btnTop.addEventListener('click', () => {
|
|
camPhi = 0.18;
|
|
camTheta = 0;
|
|
camRadius = 230;
|
|
camRadiusTarget = 230;
|
|
clearFocus();
|
|
updateCamera();
|
|
});
|
|
|
|
btnSide.addEventListener('click', () => {
|
|
camPhi = Math.PI / 2;
|
|
camTheta = 0;
|
|
camRadius = 220;
|
|
camRadiusTarget = 220;
|
|
clearFocus();
|
|
updateCamera();
|
|
});
|
|
|
|
btnOrbits.addEventListener('click', () => {
|
|
showOrbits = !showOrbits;
|
|
btnOrbits.classList.toggle('is-active', showOrbits);
|
|
orbitLines.forEach((line) => {
|
|
line.visible = showOrbits;
|
|
});
|
|
if (minorPlanetOrbitLine) {
|
|
minorPlanetOrbitLine.visible = showOrbits && !!getMinorPlanetMeta(selectedMinorPlanetId);
|
|
}
|
|
});
|
|
|
|
btnLabels.addEventListener('click', () => {
|
|
showLabels = !showLabels;
|
|
btnLabels.classList.toggle('is-active', showLabels);
|
|
labelElements.forEach((element) => {
|
|
element.hidden = !showLabels;
|
|
});
|
|
});
|
|
|
|
btnEcliptic.addEventListener('click', () => {
|
|
showEcliptic = !showEcliptic;
|
|
btnEcliptic.classList.toggle('is-active', showEcliptic);
|
|
if (eclipticGroup) {
|
|
eclipticGroup.visible = showEcliptic;
|
|
}
|
|
});
|
|
|
|
speedSlider.addEventListener('input', updateSpeedFromSlider);
|
|
|
|
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();
|
|
});
|
|
|
|
if (minorPlanetSelect) {
|
|
minorPlanetSelect.addEventListener('change', () => {
|
|
selectedMinorPlanetId = minorPlanetSelect.value ? Number(minorPlanetSelect.value) : null;
|
|
focusedMinorPlanetId = null;
|
|
rebuildMinorPlanetOrbit(selectedMinorPlanetId);
|
|
if (!selectedMinorPlanetId && !focusedPlanetKey) {
|
|
infoPanel.hidden = true;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
|
|
const now = performance.now();
|
|
const deltaSeconds = (now - lastAnimationTime) / 1000;
|
|
lastAnimationTime = now;
|
|
|
|
if (playing) {
|
|
simDate = new Date(simDate.getTime() + speedDaysPerSecond * deltaSeconds * 86400000);
|
|
}
|
|
|
|
updatePlanetPositions();
|
|
updateMinorPlanetVisual();
|
|
|
|
// Aim shadow light from sun toward Saturn
|
|
const saturnMesh = planetMeshes.get('Saturn');
|
|
const sl = scene.userData.shadowLight;
|
|
if (saturnMesh && sl) {
|
|
const sp = saturnMesh.position;
|
|
const dist = sp.length();
|
|
sl.position.set(-sp.x / dist * 5, -sp.y / dist * 5, -sp.z / dist * 5);
|
|
sl.target.position.copy(sp);
|
|
sl.target.updateMatrixWorld();
|
|
}
|
|
refreshCurrentDateUi();
|
|
|
|
if (focusedPlanetKey) {
|
|
const focusedMesh = planetMeshes.get(focusedPlanetKey);
|
|
if (focusedMesh) {
|
|
cameraTarget.lerp(focusedMesh.position, 0.06);
|
|
updatePlanetInfoPanel(focusedPlanetKey);
|
|
}
|
|
}
|
|
|
|
if (focusedMinorPlanetId && minorPlanetMesh && minorPlanetMesh.visible) {
|
|
cameraTarget.lerp(minorPlanetMesh.position, 0.06);
|
|
updateMinorPlanetInfoPanel(focusedMinorPlanetId);
|
|
}
|
|
|
|
camRadius += (camRadiusTarget - camRadius) * 0.18;
|
|
updateCamera();
|
|
updateBodyDisplayScales();
|
|
updateLabels();
|
|
|
|
orbitRefreshCursor += 1;
|
|
if (playing && orbitRefreshCursor % 90 === 0) {
|
|
const planet = PLANETS[(orbitRefreshCursor / 90) % PLANETS.length | 0];
|
|
rebuildOrbitLine(planet);
|
|
}
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
async function initSolarSystem() {
|
|
try {
|
|
THREE = await import('./js/three.module.js');
|
|
textureLoader = new THREE.TextureLoader();
|
|
|
|
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.0018);
|
|
|
|
camera = new THREE.PerspectiveCamera(45, 1, 0.1, 2000);
|
|
planetMeshes = new Map();
|
|
orbitLines = 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);
|
|
|
|
const sunLight = new THREE.PointLight(0xfff5d0, 8.0, 0, 0);
|
|
|
|
// Dedicated directional light for shadow casting only (PointLight shadows are unreliable)
|
|
const shadowLight = new THREE.DirectionalLight(0x000000, 0);
|
|
shadowLight.castShadow = true;
|
|
shadowLight.shadow.mapSize.set(2048, 2048);
|
|
shadowLight.shadow.camera.near = 0.5;
|
|
shadowLight.shadow.camera.far = 400;
|
|
shadowLight.shadow.camera.left = -80;
|
|
shadowLight.shadow.camera.right = 80;
|
|
shadowLight.shadow.camera.top = 80;
|
|
shadowLight.shadow.camera.bottom = -80;
|
|
shadowLight.shadow.bias = -0.002;
|
|
scene.add(shadowLight);
|
|
scene.add(shadowLight.target);
|
|
scene.userData.shadowLight = shadowLight;
|
|
scene.add(sunLight);
|
|
|
|
// Starfield background
|
|
(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 = 860 + 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,
|
|
})));
|
|
})();
|
|
|
|
const sunGroup = new THREE.Group();
|
|
const sunTex = textureLoader.load('images/planets/sun_1024.jpg');
|
|
sunTex.colorSpace = THREE.SRGBColorSpace;
|
|
const sunCore = new THREE.Mesh(
|
|
new THREE.SphereGeometry(2.8, 40, 40),
|
|
new THREE.MeshBasicMaterial({ map: sunTex })
|
|
);
|
|
sunGroup.add(sunCore);
|
|
|
|
[
|
|
{ r: 3.7, opacity: 0.24, color: 0xffcc44 },
|
|
{ r: 5.5, opacity: 0.10, color: 0xff9900 },
|
|
{ r: 9.0, opacity: 0.042, color: 0xff6600 },
|
|
{ r: 15.0, opacity: 0.016, color: 0xff4400 },
|
|
].forEach(({ r, opacity, color }) => {
|
|
sunGroup.add(new THREE.Mesh(
|
|
new THREE.SphereGeometry(r, 24, 24),
|
|
new THREE.MeshBasicMaterial({
|
|
color,
|
|
transparent: true,
|
|
opacity,
|
|
side: THREE.BackSide,
|
|
depthWrite: false,
|
|
blending: THREE.AdditiveBlending,
|
|
})
|
|
));
|
|
});
|
|
scene.add(sunGroup);
|
|
|
|
createEclipticPlane();
|
|
|
|
buildLabels();
|
|
PLANETS.forEach(createPlanetMesh);
|
|
ensureMinorPlanetObjects();
|
|
attachPointerControls();
|
|
attachUiHandlers();
|
|
updateSpeedFromSlider();
|
|
refreshCurrentDateUi();
|
|
resizeRenderer();
|
|
updatePlanetPositions();
|
|
rebuildMinorPlanetOrbit(null);
|
|
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('Sonnensystem konnte nicht initialisiert werden.', error);
|
|
overlay.hidden = false;
|
|
overlay.textContent = 'Fehler beim Laden der Visualisierung. Bitte Browser-Konsole pruefen.';
|
|
}
|
|
}
|
|
|
|
initSolarSystem();
|
|
</script>
|
|
|
|
<?php require __DIR__ . '/footer.php'; ?>
|