3575 lines
124 KiB
PHP
3575 lines
124 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
ini_set('display_errors', '1');
|
|
error_reporting(E_ALL);
|
|
session_start();
|
|
|
|
$loggedIn = isset($_SESSION['user_id']);
|
|
$trackedMinorplanetsForJs = [];
|
|
$trackedCometsForJs = [];
|
|
$voyager1TrackForJs = [];
|
|
$voyager2TrackForJs = [];
|
|
$newHorizonsTrackForJs = [];
|
|
$pioneer10TrackForJs = [];
|
|
$pdo = null;
|
|
$spacecraftOptionsForJs = [];
|
|
|
|
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'] ?? '--');
|
|
}
|
|
|
|
function formatCometDisplayName(array $comet): string
|
|
{
|
|
$designation = trim((string) ($comet['designation_and_name'] ?? $comet['designation'] ?? ''));
|
|
return $designation !== '' ? $designation : (string) ($comet['designation_packed'] ?? $comet['packedDesignation'] ?? '--');
|
|
}
|
|
|
|
function buildCometPerihelionIso(array $comet): ?string
|
|
{
|
|
$year = isset($comet['year_of_perihelion']) ? (int) $comet['year_of_perihelion'] : 0;
|
|
$month = isset($comet['month_of_perihelion']) ? (int) $comet['month_of_perihelion'] : 0;
|
|
$dayValue = isset($comet['day_of_perihelion']) ? (float) $comet['day_of_perihelion'] : 0.0;
|
|
|
|
if ($year <= 0 || $month < 1 || $month > 12 || $dayValue <= 0.0) {
|
|
return null;
|
|
}
|
|
|
|
$day = (int) floor($dayValue);
|
|
if ($day < 1 || $day > 31) {
|
|
return null;
|
|
}
|
|
|
|
$fractionalDay = $dayValue - $day;
|
|
$seconds = (int) round($fractionalDay * 86400.0);
|
|
|
|
try {
|
|
$date = new DateTimeImmutable(sprintf('%04d-%02d-%02d 00:00:00', $year, $month, $day), new DateTimeZone('UTC'));
|
|
} catch (Throwable $e) {
|
|
return null;
|
|
}
|
|
|
|
return $date->modify(sprintf('+%d seconds', $seconds))->format('c');
|
|
}
|
|
|
|
function loadSpacecraftTrackFromDatabase(PDO $pdo, string $missionKey): array
|
|
{
|
|
$stmt = $pdo->prepare("
|
|
SELECT
|
|
`date_tdb`,
|
|
`x_km`,
|
|
`y_km`,
|
|
`z_km`,
|
|
`distance_au`
|
|
FROM `spacecraft_tracks`
|
|
WHERE `mission_key` = :mission_key
|
|
ORDER BY `date_tdb` ASC
|
|
");
|
|
$stmt->execute([
|
|
':mission_key' => $missionKey,
|
|
]);
|
|
|
|
$rows = [];
|
|
foreach ($stmt->fetchAll() as $row) {
|
|
$timestamp = strtotime((string) ($row['date_tdb'] ?? ''));
|
|
$xKm = isset($row['x_km']) ? (float) $row['x_km'] : null;
|
|
$yKm = isset($row['y_km']) ? (float) $row['y_km'] : null;
|
|
$zKm = isset($row['z_km']) ? (float) $row['z_km'] : null;
|
|
$distanceAu = isset($row['distance_au']) ? (float) $row['distance_au'] : null;
|
|
|
|
if ($timestamp === false || !is_finite((float) $xKm) || !is_finite((float) $yKm) || !is_finite((float) $zKm) || !is_finite((float) $distanceAu)) {
|
|
continue;
|
|
}
|
|
|
|
$rows[] = [
|
|
'timestampMs' => $timestamp * 1000,
|
|
'dateTdb' => (string) $row['date_tdb'],
|
|
'xAu' => $xKm / 149597870.7,
|
|
'yAu' => $yKm / 149597870.7,
|
|
'zAu' => $zKm / 149597870.7,
|
|
'distanceAu' => $distanceAu,
|
|
];
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
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,
|
|
]
|
|
);
|
|
} catch (Throwable $e) {
|
|
$pdo = null;
|
|
}
|
|
|
|
if ($loggedIn && $pdo instanceof PDO) {
|
|
try {
|
|
$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'],
|
|
];
|
|
}
|
|
|
|
$stmtTrackedComets = $pdo->prepare("
|
|
SELECT
|
|
c.`id`,
|
|
c.`designation_packed`,
|
|
c.`designation_and_name`,
|
|
c.`orbit_type`,
|
|
c.`year_of_perihelion`,
|
|
c.`month_of_perihelion`,
|
|
c.`day_of_perihelion`,
|
|
c.`perihelion_dist_au`,
|
|
c.`eccentricity`,
|
|
c.`arg_perihelion_deg`,
|
|
c.`ascending_node_deg`,
|
|
c.`inclination_deg`,
|
|
c.`absolute_magnitude_h`,
|
|
c.`slope_parameter_g`,
|
|
auc.`is_favorite`
|
|
FROM `app_user_comets` auc
|
|
INNER JOIN `comets_mpc` c
|
|
ON c.`id` = auc.`comet_id`
|
|
WHERE auc.`user_id` = :user_id
|
|
ORDER BY auc.`is_favorite` DESC, c.`year_of_perihelion` ASC, c.`month_of_perihelion` ASC, c.`day_of_perihelion` ASC, c.`designation_and_name` ASC
|
|
");
|
|
$stmtTrackedComets->execute([
|
|
':user_id' => (int) $_SESSION['user_id'],
|
|
]);
|
|
|
|
foreach ($stmtTrackedComets->fetchAll() as $comet) {
|
|
$trackedCometsForJs[] = [
|
|
'id' => (int) $comet['id'],
|
|
'designation' => (string) $comet['designation_and_name'],
|
|
'packedDesignation' => (string) $comet['designation_packed'],
|
|
'orbitType' => (string) $comet['orbit_type'],
|
|
'perihelionIso' => buildCometPerihelionIso($comet),
|
|
'perihelionDistanceAu' => (float) $comet['perihelion_dist_au'],
|
|
'eccentricity' => (float) $comet['eccentricity'],
|
|
'argPerihelionDeg' => (float) $comet['arg_perihelion_deg'],
|
|
'ascendingNodeDeg' => (float) $comet['ascending_node_deg'],
|
|
'inclinationDeg' => (float) $comet['inclination_deg'],
|
|
'absoluteMagnitudeH' => $comet['absolute_magnitude_h'] !== null ? (float) $comet['absolute_magnitude_h'] : null,
|
|
'photometricIndex' => $comet['slope_parameter_g'] !== null ? (float) $comet['slope_parameter_g'] : null,
|
|
'isFavorite' => (bool) $comet['is_favorite'],
|
|
];
|
|
}
|
|
} catch (Throwable $e) {
|
|
$trackedMinorplanetsForJs = [];
|
|
$trackedCometsForJs = [];
|
|
}
|
|
}
|
|
|
|
if ($pdo instanceof PDO) {
|
|
try {
|
|
$voyager1TrackForJs = loadSpacecraftTrackFromDatabase($pdo, 'voyager1');
|
|
$voyager2TrackForJs = loadSpacecraftTrackFromDatabase($pdo, 'voyager2');
|
|
$newHorizonsTrackForJs = loadSpacecraftTrackFromDatabase($pdo, 'new_horizons');
|
|
$pioneer10TrackForJs = loadSpacecraftTrackFromDatabase($pdo, 'pioneer10');
|
|
} catch (Throwable $e) {
|
|
$voyager1TrackForJs = [];
|
|
$voyager2TrackForJs = [];
|
|
$newHorizonsTrackForJs = [];
|
|
$pioneer10TrackForJs = [];
|
|
}
|
|
}
|
|
|
|
if ($voyager1TrackForJs !== []) {
|
|
$spacecraftOptionsForJs[] = ['key' => 'voyager1', 'label' => 'Voyager 1'];
|
|
}
|
|
if ($voyager2TrackForJs !== []) {
|
|
$spacecraftOptionsForJs[] = ['key' => 'voyager2', 'label' => 'Voyager 2'];
|
|
}
|
|
if ($newHorizonsTrackForJs !== []) {
|
|
$spacecraftOptionsForJs[] = ['key' => 'new_horizons', 'label' => 'New Horizons'];
|
|
}
|
|
if ($pioneer10TrackForJs !== []) {
|
|
$spacecraftOptionsForJs[] = ['key' => 'pioneer10', 'label' => 'Pioneer 10'];
|
|
}
|
|
|
|
$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">
|
|
<details class="solarsystem-multiselect<?= empty($trackedMinorplanetsForJs) ? ' is-disabled' : '' ?>" id="solarMinorplanetSelectWrap"<?= empty($trackedMinorplanetsForJs) ? ' open' : '' ?>>
|
|
<summary id="solarMinorplanetSelectSummary">
|
|
<?= empty($trackedMinorplanetsForJs) ? 'Keine Kleinplaneten verfuegbar' : 'Kleinplaneten auswaehlen' ?>
|
|
</summary>
|
|
<div class="solarsystem-multiselect-panel" id="solarMinorplanetSelectPanel">
|
|
<?php if (!empty($trackedMinorplanetsForJs)): ?>
|
|
<?php foreach ($trackedMinorplanetsForJs as $minorplanet): ?>
|
|
<label class="solarsystem-multiselect-option">
|
|
<input
|
|
type="checkbox"
|
|
class="solar-minorplanet-checkbox"
|
|
value="<?= (int) $minorplanet['id'] ?>"
|
|
>
|
|
<span><?= h(formatMinorplanetDisplayName($minorplanet)) ?></span>
|
|
</label>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
<div class="solarsystem-current-date solarsystem-minorplanet-hint" id="solarMinorplanetHint">
|
|
<?= empty($trackedMinorplanetsForJs) ? 'Fuege in den Favoriten Kleinplaneten hinzu, um hier ihre Orbits anzuzeigen.' : '' ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="solarsystem-sidebar-block">
|
|
<div class="solarsystem-label">Komet</div>
|
|
<div class="solarsystem-input-row">
|
|
<details class="solarsystem-multiselect<?= empty($trackedCometsForJs) ? ' is-disabled' : '' ?>" id="solarCometSelectWrap"<?= empty($trackedCometsForJs) ? ' open' : '' ?>>
|
|
<summary id="solarCometSelectSummary">
|
|
<?= empty($trackedCometsForJs) ? 'Keine Kometen verfuegbar' : 'Kometen auswaehlen' ?>
|
|
</summary>
|
|
<div class="solarsystem-multiselect-panel" id="solarCometSelectPanel">
|
|
<?php if (!empty($trackedCometsForJs)): ?>
|
|
<?php foreach ($trackedCometsForJs as $comet): ?>
|
|
<label class="solarsystem-multiselect-option">
|
|
<input
|
|
type="checkbox"
|
|
class="solar-comet-checkbox"
|
|
value="<?= (int) $comet['id'] ?>"
|
|
>
|
|
<span><?= h(formatCometDisplayName($comet)) ?></span>
|
|
</label>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
<div class="solarsystem-current-date solarsystem-minorplanet-hint" id="solarCometHint">
|
|
<?= empty($trackedCometsForJs) ? 'Fuege in den Favoriten Kometen hinzu, um hier ihre Orbits anzuzeigen.' : '' ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="solarsystem-sidebar-block">
|
|
<div class="solarsystem-label">Raumfahrzeuge</div>
|
|
<div class="solarsystem-input-row">
|
|
<details class="solarsystem-multiselect<?= empty($spacecraftOptionsForJs) ? ' is-disabled' : '' ?>" id="solarSpacecraftSelectWrap"<?= empty($spacecraftOptionsForJs) ? ' open' : '' ?>>
|
|
<summary id="solarSpacecraftSelectSummary">
|
|
<?= empty($spacecraftOptionsForJs) ? 'Keine Raumfahrzeuge verfuegbar' : 'Raumfahrzeuge auswaehlen' ?>
|
|
</summary>
|
|
<div class="solarsystem-multiselect-panel" id="solarSpacecraftSelectPanel">
|
|
<?php if (!empty($spacecraftOptionsForJs)): ?>
|
|
<?php foreach ($spacecraftOptionsForJs as $spacecraft): ?>
|
|
<label class="solarsystem-multiselect-option">
|
|
<input
|
|
type="checkbox"
|
|
class="solar-spacecraft-checkbox"
|
|
value="<?= h($spacecraft['key']) ?>"
|
|
>
|
|
<span><?= h($spacecraft['label']) ?></span>
|
|
</label>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
<div class="solarsystem-current-date solarsystem-minorplanet-hint" id="solarSpacecraftHint">
|
|
<?= empty($spacecraftOptionsForJs) ? 'Keine Raumfahrzeuge mit Bahndaten verfuegbar.' : '' ?>
|
|
</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: mehrere Kleinplaneten per Haken anzeigen</li>
|
|
<li>Dropdown: Raumfahrzeuge ein- und ausblenden</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 USER_COMETS = <?= json_encode($trackedCometsForJs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
|
const SPACECRAFT_OPTIONS = <?= json_encode($spacecraftOptionsForJs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
|
const VOYAGER1_TRACK = <?= json_encode($voyager1TrackForJs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
|
const VOYAGER2_TRACK = <?= json_encode($voyager2TrackForJs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
|
const NEW_HORIZONS_TRACK = <?= json_encode($newHorizonsTrackForJs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
|
const PIONEER10_TRACK = <?= json_encode($pioneer10TrackForJs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
|
const JUPITER_MIN_VISIBLE_SIZE = 1.42;
|
|
const MOON_DISPLAY_DISTANCE_MULTIPLIER = 2200;
|
|
|
|
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 MOON = {
|
|
key: 'Moon',
|
|
label: 'Mond',
|
|
color: '#d7d7d7',
|
|
emissive: '#2d2d30',
|
|
size: 0.17,
|
|
type: 'Mond',
|
|
roughness: 0.96,
|
|
metalness: 0.00,
|
|
texture: 'images/moons/moon_1024.jpg',
|
|
orbitalPeriodDays: 27.321661,
|
|
};
|
|
|
|
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 minorPlanetSelectWrap = document.getElementById('solarMinorplanetSelectWrap');
|
|
const minorPlanetSelectSummary = document.getElementById('solarMinorplanetSelectSummary');
|
|
const minorPlanetCheckboxes = Array.from(document.querySelectorAll('.solar-minorplanet-checkbox'));
|
|
const cometSelectWrap = document.getElementById('solarCometSelectWrap');
|
|
const cometSelectSummary = document.getElementById('solarCometSelectSummary');
|
|
const cometCheckboxes = Array.from(document.querySelectorAll('.solar-comet-checkbox'));
|
|
const spacecraftSelectWrap = document.getElementById('solarSpacecraftSelectWrap');
|
|
const spacecraftSelectSummary = document.getElementById('solarSpacecraftSelectSummary');
|
|
const spacecraftCheckboxes = Array.from(document.querySelectorAll('.solar-spacecraft-checkbox'));
|
|
|
|
if (!AstronomyEngine) {
|
|
overlay.textContent = 'Astronomy Engine konnte nicht geladen werden.';
|
|
throw new Error('Astronomy Engine unavailable');
|
|
}
|
|
|
|
let THREE;
|
|
let GLTFLoaderClass;
|
|
let DRACOLoaderClass;
|
|
let textureLoader;
|
|
let renderer;
|
|
let scene;
|
|
let camera;
|
|
let planetMeshes;
|
|
let orbitLines;
|
|
let labelElements;
|
|
let raycaster;
|
|
let pointer;
|
|
let eclipticGroup;
|
|
let rotationEqjToEcl;
|
|
let moonMesh = null;
|
|
let moonOrbitLine = null;
|
|
let moonLabelElement = null;
|
|
let minorPlanetObjects = null;
|
|
let cometObjects = null;
|
|
let voyager1Objects = null;
|
|
let voyager1LabelElement = null;
|
|
let voyager2Objects = null;
|
|
let voyager2LabelElement = null;
|
|
let newHorizonsObjects = null;
|
|
let newHorizonsLabelElement = null;
|
|
let pioneer10Objects = null;
|
|
let pioneer10LabelElement = null;
|
|
let voyagerModelTemplatePromise = null;
|
|
let voyagerModelTemplate = null;
|
|
const planetDynamicsCache = new Map();
|
|
|
|
let showOrbits = true;
|
|
let showLabels = false;
|
|
let showEcliptic = false;
|
|
let playing = false;
|
|
let speedDaysPerSecond = 1;
|
|
let focusedPlanetKey = null;
|
|
let selectedMinorPlanetIds = [];
|
|
let focusedMinorPlanetId = null;
|
|
let selectedCometIds = [];
|
|
let focusedCometId = null;
|
|
let selectedSpacecraftKeys = spacecraftCheckboxes
|
|
.filter((input) => input.checked)
|
|
.map((input) => String(input.value));
|
|
let focusedVoyager1 = false;
|
|
let focusedVoyager2 = false;
|
|
let focusedNewHorizons = false;
|
|
let focusedPioneer10 = false;
|
|
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);
|
|
}
|
|
|
|
async function loadVoyagerModelTemplate() {
|
|
if (voyagerModelTemplatePromise) {
|
|
return voyagerModelTemplatePromise;
|
|
}
|
|
|
|
if (!GLTFLoaderClass) {
|
|
return null;
|
|
}
|
|
|
|
const loader = new GLTFLoaderClass();
|
|
if (DRACOLoaderClass) {
|
|
const dracoLoader = new DRACOLoaderClass();
|
|
dracoLoader.setDecoderPath('./js/libs/draco/');
|
|
loader.setDRACOLoader(dracoLoader);
|
|
}
|
|
voyagerModelTemplatePromise = new Promise((resolve) => {
|
|
loader.load(
|
|
'./images/3DModels/voyager.glb',
|
|
(gltf) => {
|
|
const template = gltf.scene;
|
|
template.traverse((node) => {
|
|
if (!node.isMesh) {
|
|
return;
|
|
}
|
|
node.castShadow = false;
|
|
node.receiveShadow = false;
|
|
});
|
|
voyagerModelTemplate = template;
|
|
resolve(template);
|
|
},
|
|
undefined,
|
|
(error) => {
|
|
console.warn('Voyager-Modell konnte nicht geladen werden, Fallback wird verwendet.', error);
|
|
voyagerModelTemplate = null;
|
|
resolve(null);
|
|
}
|
|
);
|
|
});
|
|
|
|
return voyagerModelTemplatePromise;
|
|
}
|
|
|
|
function createVoyagerFallbackMesh(color, emissive) {
|
|
const mesh = new THREE.Mesh(
|
|
new THREE.SphereGeometry(0.28, 20, 20),
|
|
new THREE.MeshStandardMaterial({
|
|
color: new THREE.Color(color),
|
|
emissive: new THREE.Color(emissive),
|
|
emissiveIntensity: 0.45,
|
|
roughness: 0.45,
|
|
metalness: 0.7,
|
|
})
|
|
);
|
|
mesh.userData.baseScale = 1;
|
|
mesh.userData.minScaleMultiplier = 1.8;
|
|
return mesh;
|
|
}
|
|
|
|
function createVoyagerDisplayObject(color, emissive) {
|
|
if (voyagerModelTemplate) {
|
|
const model = voyagerModelTemplate.clone(true);
|
|
model.scale.setScalar(0.16);
|
|
model.rotation.x = Math.PI / 2;
|
|
model.userData.baseScale = 1;
|
|
model.userData.minScaleMultiplier = 1.8;
|
|
return model;
|
|
}
|
|
|
|
return createVoyagerFallbackMesh(color, emissive);
|
|
}
|
|
|
|
function adjustSpeedSliderFromWheel(event) {
|
|
if (!speedSlider) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
const min = Number(speedSlider.min || -100);
|
|
const max = Number(speedSlider.max || 100);
|
|
const step = Number(speedSlider.step || 0.1);
|
|
const direction = event.deltaY < 0 ? 1 : -1;
|
|
const multiplier = event.shiftKey ? 10 : 1;
|
|
const nextValue = Number(speedSlider.value) + (direction * step * multiplier);
|
|
const clampedValue = Math.max(min, Math.min(max, nextValue));
|
|
|
|
speedSlider.value = String(clampedValue);
|
|
updateSpeedFromSlider();
|
|
}
|
|
|
|
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 getSelectedMinorPlanetSet() {
|
|
return new Set(selectedMinorPlanetIds);
|
|
}
|
|
|
|
function getCometMeta(id) {
|
|
return USER_COMETS.find((comet) => comet.id === Number(id)) || null;
|
|
}
|
|
|
|
function getSelectedCometSet() {
|
|
return new Set(selectedCometIds);
|
|
}
|
|
|
|
function getSelectedSpacecraftSet() {
|
|
return new Set(selectedSpacecraftKeys);
|
|
}
|
|
|
|
function getVoyager1Position(date) {
|
|
if (!Array.isArray(VOYAGER1_TRACK) || VOYAGER1_TRACK.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const targetMs = date.getTime();
|
|
if (targetMs <= Number(VOYAGER1_TRACK[0].timestampMs)) {
|
|
return VOYAGER1_TRACK[0];
|
|
}
|
|
|
|
const lastIndex = VOYAGER1_TRACK.length - 1;
|
|
if (targetMs >= Number(VOYAGER1_TRACK[lastIndex].timestampMs)) {
|
|
return VOYAGER1_TRACK[lastIndex];
|
|
}
|
|
|
|
for (let index = 0; index < lastIndex; index += 1) {
|
|
const left = VOYAGER1_TRACK[index];
|
|
const right = VOYAGER1_TRACK[index + 1];
|
|
const leftMs = Number(left.timestampMs);
|
|
const rightMs = Number(right.timestampMs);
|
|
if (targetMs < leftMs || targetMs > rightMs) {
|
|
continue;
|
|
}
|
|
|
|
const span = Math.max(rightMs - leftMs, 1);
|
|
const factor = (targetMs - leftMs) / span;
|
|
const xAu = Number(left.xAu) + ((Number(right.xAu) - Number(left.xAu)) * factor);
|
|
const yAu = Number(left.yAu) + ((Number(right.yAu) - Number(left.yAu)) * factor);
|
|
const zAu = Number(left.zAu) + ((Number(right.zAu) - Number(left.zAu)) * factor);
|
|
const distanceAu = Math.hypot(xAu, yAu, zAu);
|
|
|
|
return {
|
|
timestampMs: targetMs,
|
|
dateTdb: String(left.dateTdb || ''),
|
|
xAu,
|
|
yAu,
|
|
zAu,
|
|
distanceAu,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function getVoyager2Position(date) {
|
|
if (!Array.isArray(VOYAGER2_TRACK) || VOYAGER2_TRACK.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const targetMs = date.getTime();
|
|
if (targetMs <= Number(VOYAGER2_TRACK[0].timestampMs)) {
|
|
return VOYAGER2_TRACK[0];
|
|
}
|
|
|
|
const lastIndex = VOYAGER2_TRACK.length - 1;
|
|
if (targetMs >= Number(VOYAGER2_TRACK[lastIndex].timestampMs)) {
|
|
return VOYAGER2_TRACK[lastIndex];
|
|
}
|
|
|
|
for (let index = 0; index < lastIndex; index += 1) {
|
|
const left = VOYAGER2_TRACK[index];
|
|
const right = VOYAGER2_TRACK[index + 1];
|
|
const leftMs = Number(left.timestampMs);
|
|
const rightMs = Number(right.timestampMs);
|
|
if (targetMs < leftMs || targetMs > rightMs) {
|
|
continue;
|
|
}
|
|
|
|
const span = Math.max(rightMs - leftMs, 1);
|
|
const factor = (targetMs - leftMs) / span;
|
|
const xAu = Number(left.xAu) + ((Number(right.xAu) - Number(left.xAu)) * factor);
|
|
const yAu = Number(left.yAu) + ((Number(right.yAu) - Number(left.yAu)) * factor);
|
|
const zAu = Number(left.zAu) + ((Number(right.zAu) - Number(left.zAu)) * factor);
|
|
const distanceAu = Math.hypot(xAu, yAu, zAu);
|
|
|
|
return {
|
|
timestampMs: targetMs,
|
|
dateTdb: String(left.dateTdb || ''),
|
|
xAu,
|
|
yAu,
|
|
zAu,
|
|
distanceAu,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function getNewHorizonsPosition(date) {
|
|
if (!Array.isArray(NEW_HORIZONS_TRACK) || NEW_HORIZONS_TRACK.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const targetMs = date.getTime();
|
|
if (targetMs <= Number(NEW_HORIZONS_TRACK[0].timestampMs)) {
|
|
return NEW_HORIZONS_TRACK[0];
|
|
}
|
|
|
|
const lastIndex = NEW_HORIZONS_TRACK.length - 1;
|
|
if (targetMs >= Number(NEW_HORIZONS_TRACK[lastIndex].timestampMs)) {
|
|
return NEW_HORIZONS_TRACK[lastIndex];
|
|
}
|
|
|
|
for (let index = 0; index < lastIndex; index += 1) {
|
|
const left = NEW_HORIZONS_TRACK[index];
|
|
const right = NEW_HORIZONS_TRACK[index + 1];
|
|
const leftMs = Number(left.timestampMs);
|
|
const rightMs = Number(right.timestampMs);
|
|
if (targetMs < leftMs || targetMs > rightMs) {
|
|
continue;
|
|
}
|
|
|
|
const span = Math.max(rightMs - leftMs, 1);
|
|
const factor = (targetMs - leftMs) / span;
|
|
const xAu = Number(left.xAu) + ((Number(right.xAu) - Number(left.xAu)) * factor);
|
|
const yAu = Number(left.yAu) + ((Number(right.yAu) - Number(left.yAu)) * factor);
|
|
const zAu = Number(left.zAu) + ((Number(right.zAu) - Number(left.zAu)) * factor);
|
|
const distanceAu = Math.hypot(xAu, yAu, zAu);
|
|
|
|
return {
|
|
timestampMs: targetMs,
|
|
dateTdb: String(left.dateTdb || ''),
|
|
xAu,
|
|
yAu,
|
|
zAu,
|
|
distanceAu,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function getPioneer10Position(date) {
|
|
if (!Array.isArray(PIONEER10_TRACK) || PIONEER10_TRACK.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const targetMs = date.getTime();
|
|
if (targetMs <= Number(PIONEER10_TRACK[0].timestampMs)) {
|
|
return PIONEER10_TRACK[0];
|
|
}
|
|
|
|
const lastIndex = PIONEER10_TRACK.length - 1;
|
|
if (targetMs >= Number(PIONEER10_TRACK[lastIndex].timestampMs)) {
|
|
return PIONEER10_TRACK[lastIndex];
|
|
}
|
|
|
|
for (let index = 0; index < lastIndex; index += 1) {
|
|
const left = PIONEER10_TRACK[index];
|
|
const right = PIONEER10_TRACK[index + 1];
|
|
const leftMs = Number(left.timestampMs);
|
|
const rightMs = Number(right.timestampMs);
|
|
if (targetMs < leftMs || targetMs > rightMs) {
|
|
continue;
|
|
}
|
|
|
|
const span = Math.max(rightMs - leftMs, 1);
|
|
const factor = (targetMs - leftMs) / span;
|
|
const xAu = Number(left.xAu) + ((Number(right.xAu) - Number(left.xAu)) * factor);
|
|
const yAu = Number(left.yAu) + ((Number(right.yAu) - Number(left.yAu)) * factor);
|
|
const zAu = Number(left.zAu) + ((Number(right.zAu) - Number(left.zAu)) * factor);
|
|
const distanceAu = Math.hypot(xAu, yAu, zAu);
|
|
|
|
return {
|
|
timestampMs: targetMs,
|
|
dateTdb: String(left.dateTdb || ''),
|
|
xAu,
|
|
yAu,
|
|
zAu,
|
|
distanceAu,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
const MINOR_PLANET_COLOR_PALETTE = [
|
|
{ line: '#ff6b6b', body: '#ff5a5a', emissive: '#d63b3b' },
|
|
{ line: '#ff5252', body: '#ff4747', emissive: '#c62828' },
|
|
{ line: '#ff7a5c', body: '#ff6b4a', emissive: '#d9482c' },
|
|
{ line: '#ff4d6d', body: '#ff3f5f', emissive: '#c81f45' },
|
|
{ line: '#ff8a65', body: '#ff7043', emissive: '#d4512c' },
|
|
{ line: '#f0625d', body: '#eb554f', emissive: '#bf3c36' },
|
|
];
|
|
|
|
function getMinorPlanetColor(id) {
|
|
const safeId = Math.abs(Number(id) || 0);
|
|
return MINOR_PLANET_COLOR_PALETTE[safeId % MINOR_PLANET_COLOR_PALETTE.length];
|
|
}
|
|
|
|
const COMET_COLOR_PALETTE = [
|
|
{ line: '#63e6be', body: '#46d9a5', emissive: '#1d7f63' },
|
|
{ line: '#7ad8ff', body: '#58c8f4', emissive: '#1f6e8d' },
|
|
{ line: '#8ce99a', body: '#69db7c', emissive: '#2b8a3e' },
|
|
{ line: '#b2f2bb', body: '#8ce99a', emissive: '#2f9e44' },
|
|
{ line: '#66d9e8', body: '#3bc9db', emissive: '#0b7285' },
|
|
{ line: '#95d5b2', body: '#74c69d', emissive: '#2d6a4f' },
|
|
];
|
|
|
|
function getCometColor(id) {
|
|
const safeId = Math.abs(Number(id) || 0);
|
|
return COMET_COLOR_PALETTE[safeId % COMET_COLOR_PALETTE.length];
|
|
}
|
|
|
|
function updateMinorPlanetSummary() {
|
|
if (!minorPlanetSelectSummary) {
|
|
return;
|
|
}
|
|
|
|
if (minorPlanetCheckboxes.length === 0) {
|
|
minorPlanetSelectSummary.textContent = 'Keine Kleinplaneten verfuegbar';
|
|
return;
|
|
}
|
|
|
|
if (selectedMinorPlanetIds.length === 0) {
|
|
minorPlanetSelectSummary.textContent = 'Kleinplaneten auswaehlen';
|
|
return;
|
|
}
|
|
|
|
const selectedNames = selectedMinorPlanetIds
|
|
.map((id) => formatMinorPlanetName(getMinorPlanetMeta(id)))
|
|
.filter((name) => name && name !== '--');
|
|
|
|
minorPlanetSelectSummary.textContent = selectedNames.length <= 2
|
|
? selectedNames.join(', ')
|
|
: `${selectedNames.length} Kleinplaneten ausgewaehlt`;
|
|
}
|
|
|
|
function updateCometSummary() {
|
|
if (!cometSelectSummary) {
|
|
return;
|
|
}
|
|
|
|
if (cometCheckboxes.length === 0) {
|
|
cometSelectSummary.textContent = 'Keine Kometen verfuegbar';
|
|
return;
|
|
}
|
|
|
|
if (selectedCometIds.length === 0) {
|
|
cometSelectSummary.textContent = 'Kometen auswaehlen';
|
|
return;
|
|
}
|
|
|
|
const selectedNames = selectedCometIds
|
|
.map((id) => formatCometName(getCometMeta(id)))
|
|
.filter((name) => name && name !== '--');
|
|
|
|
cometSelectSummary.textContent = selectedNames.length <= 2
|
|
? selectedNames.join(', ')
|
|
: `${selectedNames.length} Kometen ausgewaehlt`;
|
|
}
|
|
|
|
function updateSpacecraftSummary() {
|
|
if (!spacecraftSelectSummary) {
|
|
return;
|
|
}
|
|
|
|
if (spacecraftCheckboxes.length === 0) {
|
|
spacecraftSelectSummary.textContent = 'Keine Raumfahrzeuge verfuegbar';
|
|
return;
|
|
}
|
|
|
|
if (selectedSpacecraftKeys.length === 0) {
|
|
spacecraftSelectSummary.textContent = 'Raumfahrzeuge auswaehlen';
|
|
return;
|
|
}
|
|
|
|
const selectedLabels = SPACECRAFT_OPTIONS
|
|
.filter((spacecraft) => selectedSpacecraftKeys.includes(String(spacecraft.key)))
|
|
.map((spacecraft) => String(spacecraft.label || '').trim())
|
|
.filter((label) => label !== '');
|
|
|
|
spacecraftSelectSummary.textContent = selectedLabels.length <= 2
|
|
? selectedLabels.join(', ')
|
|
: `${selectedLabels.length} Raumfahrzeuge ausgewaehlt`;
|
|
}
|
|
|
|
function isSpacecraftSelected(spacecraftKey) {
|
|
return getSelectedSpacecraftSet().has(String(spacecraftKey));
|
|
}
|
|
|
|
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 formatCometName(comet) {
|
|
if (!comet) {
|
|
return '--';
|
|
}
|
|
|
|
return String(comet.designation || comet.packedDesignation || '--').trim();
|
|
}
|
|
|
|
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 toDisplayDirection(vector) {
|
|
return new THREE.Vector3(
|
|
vector.x,
|
|
vector.z,
|
|
-vector.y
|
|
);
|
|
}
|
|
|
|
function getPlanetRotationState(key, date) {
|
|
try {
|
|
const axisInfo = AstronomyEngine.RotationAxis(getPlanetBodyKey(key), date);
|
|
if (!axisInfo || !axisInfo.north) {
|
|
return null;
|
|
}
|
|
|
|
const northEqj = axisInfo.north;
|
|
const northEcl = rotationEqjToEcl
|
|
? AstronomyEngine.RotateVector(rotationEqjToEcl, northEqj)
|
|
: northEqj;
|
|
const northDisplay = toDisplayDirection(northEcl).normalize();
|
|
|
|
if (!Number.isFinite(northDisplay.lengthSq()) || northDisplay.lengthSq() < 1e-12) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
northDisplay,
|
|
spinRad: THREE.MathUtils.degToRad(Number(axisInfo.spin) || 0),
|
|
};
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getMoonGeoPosition(date) {
|
|
const astroTime = AstronomyEngine.MakeTime(date);
|
|
const vectorEqj = AstronomyEngine.GeoVector('Moon', astroTime, true);
|
|
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 getMoonHelioPosition(date) {
|
|
const earthPosition = getHelioPosition('Earth', date);
|
|
const moonGeoPosition = getMoonGeoPosition(date);
|
|
return {
|
|
x: earthPosition.x + moonGeoPosition.x,
|
|
y: earthPosition.y + moonGeoPosition.y,
|
|
z: earthPosition.z + moonGeoPosition.z,
|
|
r: Math.hypot(
|
|
earthPosition.x + moonGeoPosition.x,
|
|
earthPosition.y + moonGeoPosition.y,
|
|
earthPosition.z + moonGeoPosition.z
|
|
),
|
|
};
|
|
}
|
|
|
|
function getMoonDisplayPosition(date) {
|
|
const earthHelioPosition = getHelioPosition('Earth', date);
|
|
const earthDisplayPosition = toDisplayPosition(earthHelioPosition);
|
|
const moonGeoPosition = getMoonGeoPosition(date);
|
|
const moonGeoDisplayOffset = toDisplayDirection(moonGeoPosition)
|
|
.multiplyScalar(MOON_DISPLAY_DISTANCE_MULTIPLIER);
|
|
|
|
return earthDisplayPosition.clone().add(moonGeoDisplayOffset);
|
|
}
|
|
|
|
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;
|
|
focusedCometId = null;
|
|
focusedVoyager1 = false;
|
|
focusedVoyager2 = false;
|
|
focusedNewHorizons = false;
|
|
focusedPioneer10 = false;
|
|
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;
|
|
const color = getMinorPlanetColor(id);
|
|
infoPanel.hidden = false;
|
|
document.getElementById('solarInfoName').textContent = formatMinorPlanetName(minorPlanet);
|
|
document.getElementById('solarInfoName').style.color = color.line;
|
|
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 updateCometInfoPanel(id) {
|
|
const comet = getCometMeta(id);
|
|
if (!comet) {
|
|
infoPanel.hidden = true;
|
|
return;
|
|
}
|
|
|
|
const position = getCometHelioPosition(comet, 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 = estimateCometMagnitude(comet, position, earthDistanceAu);
|
|
const color = getCometColor(id);
|
|
const semimajorAxisAu = comet.eccentricity < 1 ? comet.perihelionDistanceAu / (1 - comet.eccentricity) : null;
|
|
const periodDays = semimajorAxisAu !== null ? 365.25 * Math.sqrt(semimajorAxisAu ** 3) : null;
|
|
|
|
infoPanel.hidden = false;
|
|
document.getElementById('solarInfoName').textContent = formatCometName(comet);
|
|
document.getElementById('solarInfoName').style.color = color.line;
|
|
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 = semimajorAxisAu === null ? '--' : `${semimajorAxisAu.toFixed(3)} AU`;
|
|
document.getElementById('solarInfoType').textContent = `Komet${comet.absoluteMagnitudeH !== null ? ` | H ${comet.absoluteMagnitudeH.toFixed(1)}` : ''}${comet.photometricIndex !== null ? ` | n ${comet.photometricIndex.toFixed(1)}` : ''}`;
|
|
}
|
|
|
|
function updateVoyager1InfoPanel() {
|
|
const position = getVoyager1Position(simDate);
|
|
if (!position) {
|
|
infoPanel.hidden = true;
|
|
return;
|
|
}
|
|
|
|
const earthPosition = getHelioPosition('Earth', simDate);
|
|
const earthDistanceAu = Math.hypot(
|
|
position.xAu - earthPosition.x,
|
|
position.yAu - earthPosition.y,
|
|
position.zAu - earthPosition.z
|
|
);
|
|
|
|
infoPanel.hidden = false;
|
|
document.getElementById('solarInfoName').textContent = 'Voyager 1';
|
|
document.getElementById('solarInfoName').style.color = '#8fe3ff';
|
|
document.getElementById('solarInfoDistanceAu').textContent = `${Number(position.distanceAu).toFixed(3)} AU`;
|
|
document.getElementById('solarInfoDistanceKm').textContent = `${(Number(position.distanceAu) * AU_IN_KM).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, '.')} km`;
|
|
document.getElementById('solarInfoEarthDistanceAu').textContent = `${earthDistanceAu.toFixed(3)} AU`;
|
|
document.getElementById('solarInfoEarthDistanceKm').textContent = `${(earthDistanceAu * AU_IN_KM).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, '.')} km`;
|
|
document.getElementById('solarInfoMagnitude').textContent = '--';
|
|
document.getElementById('solarInfoPeriod').textContent = '--';
|
|
document.getElementById('solarInfoAxis').textContent = '--';
|
|
document.getElementById('solarInfoType').textContent = 'Raumsonde | JPL Horizons CSV';
|
|
}
|
|
|
|
function updateVoyager2InfoPanel() {
|
|
const position = getVoyager2Position(simDate);
|
|
if (!position) {
|
|
infoPanel.hidden = true;
|
|
return;
|
|
}
|
|
|
|
const earthPosition = getHelioPosition('Earth', simDate);
|
|
const earthDistanceAu = Math.hypot(
|
|
position.xAu - earthPosition.x,
|
|
position.yAu - earthPosition.y,
|
|
position.zAu - earthPosition.z
|
|
);
|
|
|
|
infoPanel.hidden = false;
|
|
document.getElementById('solarInfoName').textContent = 'Voyager 2';
|
|
document.getElementById('solarInfoName').style.color = '#ffd36f';
|
|
document.getElementById('solarInfoDistanceAu').textContent = `${Number(position.distanceAu).toFixed(3)} AU`;
|
|
document.getElementById('solarInfoDistanceKm').textContent = `${(Number(position.distanceAu) * AU_IN_KM).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, '.')} km`;
|
|
document.getElementById('solarInfoEarthDistanceAu').textContent = `${earthDistanceAu.toFixed(3)} AU`;
|
|
document.getElementById('solarInfoEarthDistanceKm').textContent = `${(earthDistanceAu * AU_IN_KM).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, '.')} km`;
|
|
document.getElementById('solarInfoMagnitude').textContent = '--';
|
|
document.getElementById('solarInfoPeriod').textContent = '--';
|
|
document.getElementById('solarInfoAxis').textContent = '--';
|
|
document.getElementById('solarInfoType').textContent = 'Raumsonde | JPL Horizons CSV';
|
|
}
|
|
|
|
function updateNewHorizonsInfoPanel() {
|
|
const position = getNewHorizonsPosition(simDate);
|
|
if (!position) {
|
|
infoPanel.hidden = true;
|
|
return;
|
|
}
|
|
|
|
const earthPosition = getHelioPosition('Earth', simDate);
|
|
const earthDistanceAu = Math.hypot(
|
|
position.xAu - earthPosition.x,
|
|
position.yAu - earthPosition.y,
|
|
position.zAu - earthPosition.z
|
|
);
|
|
|
|
infoPanel.hidden = false;
|
|
document.getElementById('solarInfoName').textContent = 'New Horizons';
|
|
document.getElementById('solarInfoName').style.color = '#ff9fd6';
|
|
document.getElementById('solarInfoDistanceAu').textContent = `${Number(position.distanceAu).toFixed(3)} AU`;
|
|
document.getElementById('solarInfoDistanceKm').textContent = `${(Number(position.distanceAu) * AU_IN_KM).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, '.')} km`;
|
|
document.getElementById('solarInfoEarthDistanceAu').textContent = `${earthDistanceAu.toFixed(3)} AU`;
|
|
document.getElementById('solarInfoEarthDistanceKm').textContent = `${(earthDistanceAu * AU_IN_KM).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, '.')} km`;
|
|
document.getElementById('solarInfoMagnitude').textContent = '--';
|
|
document.getElementById('solarInfoPeriod').textContent = '--';
|
|
document.getElementById('solarInfoAxis').textContent = '--';
|
|
document.getElementById('solarInfoType').textContent = 'Raumsonde | JPL Horizons CSV';
|
|
}
|
|
|
|
function updatePioneer10InfoPanel() {
|
|
const position = getPioneer10Position(simDate);
|
|
if (!position) {
|
|
infoPanel.hidden = true;
|
|
return;
|
|
}
|
|
|
|
const earthPosition = getHelioPosition('Earth', simDate);
|
|
const earthDistanceAu = Math.hypot(
|
|
position.xAu - earthPosition.x,
|
|
position.yAu - earthPosition.y,
|
|
position.zAu - earthPosition.z
|
|
);
|
|
|
|
infoPanel.hidden = false;
|
|
document.getElementById('solarInfoName').textContent = 'Pioneer 10';
|
|
document.getElementById('solarInfoName').style.color = '#f3a86f';
|
|
document.getElementById('solarInfoDistanceAu').textContent = `${Number(position.distanceAu).toFixed(3)} AU`;
|
|
document.getElementById('solarInfoDistanceKm').textContent = `${(Number(position.distanceAu) * AU_IN_KM).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, '.')} km`;
|
|
document.getElementById('solarInfoEarthDistanceAu').textContent = `${earthDistanceAu.toFixed(3)} AU`;
|
|
document.getElementById('solarInfoEarthDistanceKm').textContent = `${(earthDistanceAu * AU_IN_KM).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, '.')} km`;
|
|
document.getElementById('solarInfoMagnitude').textContent = '--';
|
|
document.getElementById('solarInfoPeriod').textContent = '--';
|
|
document.getElementById('solarInfoAxis').textContent = '--';
|
|
document.getElementById('solarInfoType').textContent = 'Raumsonde | JPL Horizons CSV';
|
|
}
|
|
|
|
function focusPlanet(key) {
|
|
focusedPlanetKey = key;
|
|
focusedMinorPlanetId = null;
|
|
focusedCometId = null;
|
|
focusedVoyager1 = false;
|
|
focusedVoyager2 = false;
|
|
focusedNewHorizons = false;
|
|
focusedPioneer10 = false;
|
|
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);
|
|
});
|
|
|
|
moonLabelElement = document.createElement('div');
|
|
moonLabelElement.className = 'solarsystem-planet-label';
|
|
moonLabelElement.textContent = MOON.label;
|
|
moonLabelElement.style.color = MOON.color;
|
|
moonLabelElement.hidden = true;
|
|
labelsLayer.appendChild(moonLabelElement);
|
|
|
|
voyager1LabelElement = document.createElement('div');
|
|
voyager1LabelElement.className = 'solarsystem-planet-label';
|
|
voyager1LabelElement.textContent = 'Voyager 1';
|
|
voyager1LabelElement.style.color = '#8fe3ff';
|
|
voyager1LabelElement.hidden = true;
|
|
labelsLayer.appendChild(voyager1LabelElement);
|
|
|
|
voyager2LabelElement = document.createElement('div');
|
|
voyager2LabelElement.className = 'solarsystem-planet-label';
|
|
voyager2LabelElement.textContent = 'Voyager 2';
|
|
voyager2LabelElement.style.color = '#ffd36f';
|
|
voyager2LabelElement.hidden = true;
|
|
labelsLayer.appendChild(voyager2LabelElement);
|
|
|
|
newHorizonsLabelElement = document.createElement('div');
|
|
newHorizonsLabelElement.className = 'solarsystem-planet-label';
|
|
newHorizonsLabelElement.textContent = 'New Horizons';
|
|
newHorizonsLabelElement.style.color = '#ff9fd6';
|
|
newHorizonsLabelElement.hidden = true;
|
|
labelsLayer.appendChild(newHorizonsLabelElement);
|
|
|
|
pioneer10LabelElement = document.createElement('div');
|
|
pioneer10LabelElement.className = 'solarsystem-planet-label';
|
|
pioneer10LabelElement.textContent = 'Pioneer 10';
|
|
pioneer10LabelElement.style.color = '#f3a86f';
|
|
pioneer10LabelElement.hidden = true;
|
|
labelsLayer.appendChild(pioneer10LabelElement);
|
|
}
|
|
|
|
function ensureVoyager1Objects() {
|
|
if (voyager1Objects || !Array.isArray(VOYAGER1_TRACK) || VOYAGER1_TRACK.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const orbitLine = new THREE.Line(
|
|
new THREE.BufferGeometry(),
|
|
new THREE.LineBasicMaterial({
|
|
color: new THREE.Color('#8fe3ff'),
|
|
transparent: true,
|
|
opacity: 0.8,
|
|
})
|
|
);
|
|
orbitLine.visible = showOrbits;
|
|
scene.add(orbitLine);
|
|
|
|
const mesh = createVoyagerDisplayObject('#b7f0ff', '#4db5d4');
|
|
scene.add(mesh);
|
|
|
|
voyager1Objects = { orbitLine, mesh, labelElement: voyager1LabelElement };
|
|
}
|
|
|
|
function rebuildVoyager1Track() {
|
|
ensureVoyager1Objects();
|
|
if (!voyager1Objects) {
|
|
return;
|
|
}
|
|
|
|
const positions = [];
|
|
VOYAGER1_TRACK.forEach((entry) => {
|
|
const display = toDisplayPosition({
|
|
x: Number(entry.xAu),
|
|
y: Number(entry.yAu),
|
|
z: Number(entry.zAu),
|
|
r: Math.max(Number(entry.distanceAu), 1e-9),
|
|
});
|
|
positions.push(display.x, display.y, display.z);
|
|
});
|
|
|
|
voyager1Objects.orbitLine.geometry.dispose();
|
|
voyager1Objects.orbitLine.geometry = new THREE.BufferGeometry();
|
|
voyager1Objects.orbitLine.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
voyager1Objects.orbitLine.visible = showOrbits;
|
|
}
|
|
|
|
function updateVoyager1Visual() {
|
|
ensureVoyager1Objects();
|
|
if (!voyager1Objects) {
|
|
return;
|
|
}
|
|
|
|
if (!isSpacecraftSelected('voyager1')) {
|
|
voyager1Objects.mesh.visible = false;
|
|
voyager1Objects.orbitLine.visible = false;
|
|
if (voyager1Objects.labelElement) {
|
|
voyager1Objects.labelElement.hidden = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const position = getVoyager1Position(simDate);
|
|
if (!position) {
|
|
voyager1Objects.mesh.visible = false;
|
|
voyager1Objects.orbitLine.visible = false;
|
|
if (voyager1Objects.labelElement) {
|
|
voyager1Objects.labelElement.hidden = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
voyager1Objects.mesh.visible = true;
|
|
voyager1Objects.mesh.position.copy(toDisplayPosition({
|
|
x: Number(position.xAu),
|
|
y: Number(position.yAu),
|
|
z: Number(position.zAu),
|
|
r: Math.max(Number(position.distanceAu), 1e-9),
|
|
}));
|
|
voyager1Objects.orbitLine.visible = showOrbits;
|
|
|
|
if (focusedVoyager1) {
|
|
updateVoyager1InfoPanel();
|
|
}
|
|
}
|
|
|
|
function ensureVoyager2Objects() {
|
|
if (voyager2Objects || !Array.isArray(VOYAGER2_TRACK) || VOYAGER2_TRACK.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const orbitLine = new THREE.Line(
|
|
new THREE.BufferGeometry(),
|
|
new THREE.LineBasicMaterial({
|
|
color: new THREE.Color('#ffd36f'),
|
|
transparent: true,
|
|
opacity: 0.8,
|
|
})
|
|
);
|
|
orbitLine.visible = showOrbits;
|
|
scene.add(orbitLine);
|
|
|
|
const mesh = createVoyagerDisplayObject('#ffe4a6', '#d4972e');
|
|
scene.add(mesh);
|
|
|
|
voyager2Objects = { orbitLine, mesh, labelElement: voyager2LabelElement };
|
|
}
|
|
|
|
function rebuildVoyager2Track() {
|
|
ensureVoyager2Objects();
|
|
if (!voyager2Objects) {
|
|
return;
|
|
}
|
|
|
|
const positions = [];
|
|
VOYAGER2_TRACK.forEach((entry) => {
|
|
const display = toDisplayPosition({
|
|
x: Number(entry.xAu),
|
|
y: Number(entry.yAu),
|
|
z: Number(entry.zAu),
|
|
r: Math.max(Number(entry.distanceAu), 1e-9),
|
|
});
|
|
positions.push(display.x, display.y, display.z);
|
|
});
|
|
|
|
voyager2Objects.orbitLine.geometry.dispose();
|
|
voyager2Objects.orbitLine.geometry = new THREE.BufferGeometry();
|
|
voyager2Objects.orbitLine.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
voyager2Objects.orbitLine.visible = showOrbits;
|
|
}
|
|
|
|
function updateVoyager2Visual() {
|
|
ensureVoyager2Objects();
|
|
if (!voyager2Objects) {
|
|
return;
|
|
}
|
|
|
|
if (!isSpacecraftSelected('voyager2')) {
|
|
voyager2Objects.mesh.visible = false;
|
|
voyager2Objects.orbitLine.visible = false;
|
|
if (voyager2Objects.labelElement) {
|
|
voyager2Objects.labelElement.hidden = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const position = getVoyager2Position(simDate);
|
|
if (!position) {
|
|
voyager2Objects.mesh.visible = false;
|
|
voyager2Objects.orbitLine.visible = false;
|
|
if (voyager2Objects.labelElement) {
|
|
voyager2Objects.labelElement.hidden = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
voyager2Objects.mesh.visible = true;
|
|
voyager2Objects.mesh.position.copy(toDisplayPosition({
|
|
x: Number(position.xAu),
|
|
y: Number(position.yAu),
|
|
z: Number(position.zAu),
|
|
r: Math.max(Number(position.distanceAu), 1e-9),
|
|
}));
|
|
voyager2Objects.orbitLine.visible = showOrbits;
|
|
|
|
if (focusedVoyager2) {
|
|
updateVoyager2InfoPanel();
|
|
}
|
|
}
|
|
|
|
function ensureNewHorizonsObjects() {
|
|
if (newHorizonsObjects || !Array.isArray(NEW_HORIZONS_TRACK) || NEW_HORIZONS_TRACK.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const orbitLine = new THREE.Line(
|
|
new THREE.BufferGeometry(),
|
|
new THREE.LineBasicMaterial({
|
|
color: new THREE.Color('#ff9fd6'),
|
|
transparent: true,
|
|
opacity: 0.8,
|
|
})
|
|
);
|
|
orbitLine.visible = showOrbits;
|
|
scene.add(orbitLine);
|
|
|
|
const mesh = new THREE.Mesh(
|
|
new THREE.SphereGeometry(0.28, 20, 20),
|
|
new THREE.MeshStandardMaterial({
|
|
color: new THREE.Color('#ffd4ec'),
|
|
emissive: new THREE.Color('#cc4da0'),
|
|
emissiveIntensity: 0.45,
|
|
roughness: 0.45,
|
|
metalness: 0.7,
|
|
})
|
|
);
|
|
mesh.userData.baseScale = 1;
|
|
mesh.userData.minScaleMultiplier = 1.8;
|
|
scene.add(mesh);
|
|
|
|
newHorizonsObjects = { orbitLine, mesh, labelElement: newHorizonsLabelElement };
|
|
}
|
|
|
|
function rebuildNewHorizonsTrack() {
|
|
ensureNewHorizonsObjects();
|
|
if (!newHorizonsObjects) {
|
|
return;
|
|
}
|
|
|
|
const positions = [];
|
|
NEW_HORIZONS_TRACK.forEach((entry) => {
|
|
const display = toDisplayPosition({
|
|
x: Number(entry.xAu),
|
|
y: Number(entry.yAu),
|
|
z: Number(entry.zAu),
|
|
r: Math.max(Number(entry.distanceAu), 1e-9),
|
|
});
|
|
positions.push(display.x, display.y, display.z);
|
|
});
|
|
|
|
newHorizonsObjects.orbitLine.geometry.dispose();
|
|
newHorizonsObjects.orbitLine.geometry = new THREE.BufferGeometry();
|
|
newHorizonsObjects.orbitLine.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
newHorizonsObjects.orbitLine.visible = showOrbits;
|
|
}
|
|
|
|
function updateNewHorizonsVisual() {
|
|
ensureNewHorizonsObjects();
|
|
if (!newHorizonsObjects) {
|
|
return;
|
|
}
|
|
|
|
if (!isSpacecraftSelected('new_horizons')) {
|
|
newHorizonsObjects.mesh.visible = false;
|
|
newHorizonsObjects.orbitLine.visible = false;
|
|
if (newHorizonsObjects.labelElement) {
|
|
newHorizonsObjects.labelElement.hidden = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const position = getNewHorizonsPosition(simDate);
|
|
if (!position) {
|
|
newHorizonsObjects.mesh.visible = false;
|
|
newHorizonsObjects.orbitLine.visible = false;
|
|
if (newHorizonsObjects.labelElement) {
|
|
newHorizonsObjects.labelElement.hidden = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
newHorizonsObjects.mesh.visible = true;
|
|
newHorizonsObjects.mesh.position.copy(toDisplayPosition({
|
|
x: Number(position.xAu),
|
|
y: Number(position.yAu),
|
|
z: Number(position.zAu),
|
|
r: Math.max(Number(position.distanceAu), 1e-9),
|
|
}));
|
|
newHorizonsObjects.orbitLine.visible = showOrbits;
|
|
|
|
if (focusedNewHorizons) {
|
|
updateNewHorizonsInfoPanel();
|
|
}
|
|
}
|
|
|
|
function ensurePioneer10Objects() {
|
|
if (pioneer10Objects || !Array.isArray(PIONEER10_TRACK) || PIONEER10_TRACK.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const orbitLine = new THREE.Line(
|
|
new THREE.BufferGeometry(),
|
|
new THREE.LineBasicMaterial({
|
|
color: new THREE.Color('#f3a86f'),
|
|
transparent: true,
|
|
opacity: 0.8,
|
|
})
|
|
);
|
|
orbitLine.visible = showOrbits;
|
|
scene.add(orbitLine);
|
|
|
|
const mesh = new THREE.Mesh(
|
|
new THREE.SphereGeometry(0.28, 20, 20),
|
|
new THREE.MeshStandardMaterial({
|
|
color: new THREE.Color('#f7c49c'),
|
|
emissive: new THREE.Color('#bc6a2e'),
|
|
emissiveIntensity: 0.45,
|
|
roughness: 0.45,
|
|
metalness: 0.7,
|
|
})
|
|
);
|
|
mesh.userData.baseScale = 1;
|
|
mesh.userData.minScaleMultiplier = 1.8;
|
|
scene.add(mesh);
|
|
|
|
pioneer10Objects = { orbitLine, mesh, labelElement: pioneer10LabelElement };
|
|
}
|
|
|
|
function rebuildPioneer10Track() {
|
|
ensurePioneer10Objects();
|
|
if (!pioneer10Objects) {
|
|
return;
|
|
}
|
|
|
|
const positions = [];
|
|
PIONEER10_TRACK.forEach((entry) => {
|
|
const display = toDisplayPosition({
|
|
x: Number(entry.xAu),
|
|
y: Number(entry.yAu),
|
|
z: Number(entry.zAu),
|
|
r: Math.max(Number(entry.distanceAu), 1e-9),
|
|
});
|
|
positions.push(display.x, display.y, display.z);
|
|
});
|
|
|
|
pioneer10Objects.orbitLine.geometry.dispose();
|
|
pioneer10Objects.orbitLine.geometry = new THREE.BufferGeometry();
|
|
pioneer10Objects.orbitLine.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
pioneer10Objects.orbitLine.visible = showOrbits;
|
|
}
|
|
|
|
function updatePioneer10Visual() {
|
|
ensurePioneer10Objects();
|
|
if (!pioneer10Objects) {
|
|
return;
|
|
}
|
|
|
|
if (!isSpacecraftSelected('pioneer10')) {
|
|
pioneer10Objects.mesh.visible = false;
|
|
pioneer10Objects.orbitLine.visible = false;
|
|
if (pioneer10Objects.labelElement) {
|
|
pioneer10Objects.labelElement.hidden = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const position = getPioneer10Position(simDate);
|
|
if (!position) {
|
|
pioneer10Objects.mesh.visible = false;
|
|
pioneer10Objects.orbitLine.visible = false;
|
|
if (pioneer10Objects.labelElement) {
|
|
pioneer10Objects.labelElement.hidden = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
pioneer10Objects.mesh.visible = true;
|
|
pioneer10Objects.mesh.position.copy(toDisplayPosition({
|
|
x: Number(position.xAu),
|
|
y: Number(position.yAu),
|
|
z: Number(position.zAu),
|
|
r: Math.max(Number(position.distanceAu), 1e-9),
|
|
}));
|
|
pioneer10Objects.orbitLine.visible = showOrbits;
|
|
|
|
if (focusedPioneer10) {
|
|
updatePioneer10InfoPanel();
|
|
}
|
|
}
|
|
|
|
function syncSpacecraftObjects() {
|
|
const selectedSpacecraftSet = getSelectedSpacecraftSet();
|
|
|
|
if (voyager1Objects) {
|
|
const isSelected = selectedSpacecraftSet.has('voyager1');
|
|
voyager1Objects.mesh.visible = isSelected && voyager1Objects.mesh.visible;
|
|
voyager1Objects.orbitLine.visible = isSelected && showOrbits && voyager1Objects.orbitLine.visible;
|
|
if (!isSelected && voyager1Objects.labelElement) {
|
|
voyager1Objects.labelElement.hidden = true;
|
|
}
|
|
}
|
|
|
|
if (voyager2Objects) {
|
|
const isSelected = selectedSpacecraftSet.has('voyager2');
|
|
voyager2Objects.mesh.visible = isSelected && voyager2Objects.mesh.visible;
|
|
voyager2Objects.orbitLine.visible = isSelected && showOrbits && voyager2Objects.orbitLine.visible;
|
|
if (!isSelected && voyager2Objects.labelElement) {
|
|
voyager2Objects.labelElement.hidden = true;
|
|
}
|
|
}
|
|
|
|
if (newHorizonsObjects) {
|
|
const isSelected = selectedSpacecraftSet.has('new_horizons');
|
|
newHorizonsObjects.mesh.visible = isSelected && newHorizonsObjects.mesh.visible;
|
|
newHorizonsObjects.orbitLine.visible = isSelected && showOrbits && newHorizonsObjects.orbitLine.visible;
|
|
if (!isSelected && newHorizonsObjects.labelElement) {
|
|
newHorizonsObjects.labelElement.hidden = true;
|
|
}
|
|
}
|
|
|
|
if (pioneer10Objects) {
|
|
const isSelected = selectedSpacecraftSet.has('pioneer10');
|
|
pioneer10Objects.mesh.visible = isSelected && pioneer10Objects.mesh.visible;
|
|
pioneer10Objects.orbitLine.visible = isSelected && showOrbits && pioneer10Objects.orbitLine.visible;
|
|
if (!isSelected && pioneer10Objects.labelElement) {
|
|
pioneer10Objects.labelElement.hidden = true;
|
|
}
|
|
}
|
|
|
|
if (focusedVoyager1 && !selectedSpacecraftSet.has('voyager1')) {
|
|
clearFocus();
|
|
}
|
|
if (focusedVoyager2 && !selectedSpacecraftSet.has('voyager2')) {
|
|
clearFocus();
|
|
}
|
|
if (focusedNewHorizons && !selectedSpacecraftSet.has('new_horizons')) {
|
|
clearFocus();
|
|
}
|
|
if (focusedPioneer10 && !selectedSpacecraftSet.has('pioneer10')) {
|
|
clearFocus();
|
|
}
|
|
}
|
|
|
|
function ensureMoonObjects() {
|
|
if (moonMesh && moonOrbitLine) {
|
|
return;
|
|
}
|
|
|
|
const moonTex = textureLoader.load(MOON.texture);
|
|
moonTex.colorSpace = THREE.SRGBColorSpace;
|
|
|
|
moonMesh = new THREE.Mesh(
|
|
new THREE.SphereGeometry(MOON.size, 28, 28),
|
|
new THREE.MeshStandardMaterial({
|
|
color: 0xffffff,
|
|
map: moonTex,
|
|
emissive: new THREE.Color(MOON.emissive),
|
|
emissiveIntensity: 0.05,
|
|
roughness: MOON.roughness,
|
|
metalness: MOON.metalness,
|
|
})
|
|
);
|
|
moonMesh.castShadow = true;
|
|
moonMesh.receiveShadow = true;
|
|
moonMesh.userData.baseScale = 1;
|
|
moonMesh.userData.minScaleMultiplier = 1;
|
|
scene.add(moonMesh);
|
|
|
|
moonOrbitLine = new THREE.Line(
|
|
new THREE.BufferGeometry(),
|
|
new THREE.LineBasicMaterial({
|
|
color: new THREE.Color(MOON.color),
|
|
transparent: true,
|
|
opacity: 0.55,
|
|
})
|
|
);
|
|
moonOrbitLine.visible = showOrbits;
|
|
scene.add(moonOrbitLine);
|
|
}
|
|
|
|
function rebuildMoonOrbitLine() {
|
|
ensureMoonObjects();
|
|
|
|
const positions = [];
|
|
const steps = 180;
|
|
for (let step = 0; step <= steps; step += 1) {
|
|
const offsetDays = (step / steps) * MOON.orbitalPeriodDays;
|
|
const date = new Date(simDate.getTime() + offsetDays * 86400000);
|
|
const display = getMoonDisplayPosition(date);
|
|
positions.push(display.x, display.y, display.z);
|
|
}
|
|
|
|
moonOrbitLine.geometry.dispose();
|
|
moonOrbitLine.geometry = new THREE.BufferGeometry();
|
|
moonOrbitLine.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
moonOrbitLine.visible = showOrbits;
|
|
}
|
|
|
|
function updateMoonVisual() {
|
|
ensureMoonObjects();
|
|
|
|
const display = getMoonDisplayPosition(simDate);
|
|
moonMesh.position.copy(display);
|
|
|
|
const rotationState = getPlanetRotationState('Moon', simDate);
|
|
if (rotationState) {
|
|
moonMesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), rotationState.northDisplay);
|
|
moonMesh.rotateY(rotationState.spinRad);
|
|
}
|
|
|
|
moonOrbitLine.visible = showOrbits;
|
|
}
|
|
|
|
function ensureMinorPlanetObjects() {
|
|
if (!minorPlanetObjects) {
|
|
minorPlanetObjects = new Map();
|
|
}
|
|
}
|
|
|
|
function createMinorPlanetObjects(id) {
|
|
ensureMinorPlanetObjects();
|
|
|
|
if (minorPlanetObjects.has(id)) {
|
|
return minorPlanetObjects.get(id);
|
|
}
|
|
|
|
const color = getMinorPlanetColor(id);
|
|
const lineMaterial = new THREE.LineBasicMaterial({
|
|
color: new THREE.Color(color.line),
|
|
transparent: true,
|
|
opacity: 0.85
|
|
});
|
|
const orbitLine = new THREE.Line(new THREE.BufferGeometry(), lineMaterial);
|
|
orbitLine.visible = false;
|
|
scene.add(orbitLine);
|
|
|
|
const mesh = new THREE.Mesh(
|
|
new THREE.SphereGeometry(0.58, 28, 28),
|
|
new THREE.MeshStandardMaterial({
|
|
color: new THREE.Color(color.body),
|
|
emissive: new THREE.Color(color.emissive),
|
|
emissiveIntensity: 0.06,
|
|
roughness: 0.65,
|
|
metalness: 0.08,
|
|
})
|
|
);
|
|
mesh.visible = false;
|
|
mesh.userData.baseScale = 1;
|
|
mesh.userData.minScaleMultiplier = 1;
|
|
mesh.userData.minorPlanetId = id;
|
|
scene.add(mesh);
|
|
|
|
const labelElement = document.createElement('div');
|
|
labelElement.className = 'solarsystem-planet-label';
|
|
labelElement.style.color = color.line;
|
|
labelElement.hidden = true;
|
|
labelsLayer.appendChild(labelElement);
|
|
|
|
const objects = { orbitLine, mesh, labelElement };
|
|
minorPlanetObjects.set(id, objects);
|
|
return objects;
|
|
}
|
|
|
|
function removeMinorPlanetObjects(id) {
|
|
if (!minorPlanetObjects || !minorPlanetObjects.has(id)) {
|
|
return;
|
|
}
|
|
|
|
const { orbitLine, mesh, labelElement } = minorPlanetObjects.get(id);
|
|
orbitLine.geometry.dispose();
|
|
orbitLine.material.dispose();
|
|
scene.remove(orbitLine);
|
|
|
|
mesh.geometry.dispose();
|
|
mesh.material.dispose();
|
|
scene.remove(mesh);
|
|
|
|
labelElement.remove();
|
|
minorPlanetObjects.delete(id);
|
|
}
|
|
|
|
function syncMinorPlanetObjects() {
|
|
ensureMinorPlanetObjects();
|
|
|
|
const selectedSet = getSelectedMinorPlanetSet();
|
|
selectedMinorPlanetIds.forEach((id) => {
|
|
createMinorPlanetObjects(id);
|
|
});
|
|
|
|
Array.from(minorPlanetObjects.keys()).forEach((id) => {
|
|
if (!selectedSet.has(id)) {
|
|
removeMinorPlanetObjects(id);
|
|
}
|
|
});
|
|
}
|
|
|
|
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 getMinorPlanetHelioPositionFromEccentricAnomaly(minorPlanet, eccentricAnomaly) {
|
|
if (!minorPlanet) {
|
|
return null;
|
|
}
|
|
|
|
const eccentricity = Math.max(0, Math.min(0.999999, minorPlanet.eccentricity));
|
|
const semimajorAxisAu = minorPlanet.semimajorAxisAu;
|
|
const inclination = minorPlanet.inclinationDeg * Math.PI / 180;
|
|
const ascendingNode = minorPlanet.ascendingNodeDeg * Math.PI / 180;
|
|
const argPerihelion = minorPlanet.argPerihelionDeg * Math.PI / 180;
|
|
const orbitalX = semimajorAxisAu * (Math.cos(eccentricAnomaly) - eccentricity);
|
|
const orbitalY = semimajorAxisAu * Math.sqrt(1 - eccentricity ** 2) * Math.sin(eccentricAnomaly);
|
|
const cosArg = Math.cos(argPerihelion);
|
|
const sinArg = Math.sin(argPerihelion);
|
|
const cosNode = Math.cos(ascendingNode);
|
|
const sinNode = Math.sin(ascendingNode);
|
|
const cosInc = Math.cos(inclination);
|
|
const sinInc = Math.sin(inclination);
|
|
const perifocalX = (cosArg * orbitalX) - (sinArg * orbitalY);
|
|
const perifocalY = (sinArg * orbitalX) + (cosArg * orbitalY);
|
|
|
|
return {
|
|
x: (cosNode * perifocalX) - (sinNode * perifocalY * cosInc),
|
|
y: (sinNode * perifocalX) + (cosNode * perifocalY * cosInc),
|
|
z: perifocalY * sinInc,
|
|
r: semimajorAxisAu * (1 - eccentricity * Math.cos(eccentricAnomaly)),
|
|
};
|
|
}
|
|
|
|
function ensureCometObjects() {
|
|
if (!cometObjects) {
|
|
cometObjects = new Map();
|
|
}
|
|
}
|
|
|
|
function createCometObjects(id) {
|
|
ensureCometObjects();
|
|
|
|
if (cometObjects.has(id)) {
|
|
return cometObjects.get(id);
|
|
}
|
|
|
|
const color = getCometColor(id);
|
|
const orbitLine = new THREE.Line(
|
|
new THREE.BufferGeometry(),
|
|
new THREE.LineBasicMaterial({
|
|
color: new THREE.Color(color.line),
|
|
transparent: true,
|
|
opacity: 0.85
|
|
})
|
|
);
|
|
orbitLine.visible = false;
|
|
scene.add(orbitLine);
|
|
|
|
const mesh = new THREE.Mesh(
|
|
new THREE.SphereGeometry(0.5, 24, 24),
|
|
new THREE.MeshStandardMaterial({
|
|
color: new THREE.Color(color.body),
|
|
emissive: new THREE.Color(color.emissive),
|
|
emissiveIntensity: 0.08,
|
|
roughness: 0.5,
|
|
metalness: 0.03,
|
|
})
|
|
);
|
|
mesh.visible = false;
|
|
mesh.userData.baseScale = 1;
|
|
mesh.userData.minScaleMultiplier = 1;
|
|
mesh.userData.cometId = id;
|
|
scene.add(mesh);
|
|
|
|
const labelElement = document.createElement('div');
|
|
labelElement.className = 'solarsystem-planet-label';
|
|
labelElement.style.color = color.line;
|
|
labelElement.hidden = true;
|
|
labelsLayer.appendChild(labelElement);
|
|
|
|
const objects = { orbitLine, mesh, labelElement };
|
|
cometObjects.set(id, objects);
|
|
return objects;
|
|
}
|
|
|
|
function removeCometObjects(id) {
|
|
if (!cometObjects || !cometObjects.has(id)) {
|
|
return;
|
|
}
|
|
|
|
const { orbitLine, mesh, labelElement } = cometObjects.get(id);
|
|
orbitLine.geometry.dispose();
|
|
orbitLine.material.dispose();
|
|
scene.remove(orbitLine);
|
|
mesh.geometry.dispose();
|
|
mesh.material.dispose();
|
|
scene.remove(mesh);
|
|
labelElement.remove();
|
|
cometObjects.delete(id);
|
|
}
|
|
|
|
function syncCometObjects() {
|
|
ensureCometObjects();
|
|
|
|
const selectedSet = getSelectedCometSet();
|
|
selectedCometIds.forEach((id) => {
|
|
createCometObjects(id);
|
|
});
|
|
|
|
Array.from(cometObjects.keys()).forEach((id) => {
|
|
if (!selectedSet.has(id)) {
|
|
removeCometObjects(id);
|
|
}
|
|
});
|
|
}
|
|
|
|
const GAUSSIAN_GRAVITATIONAL_CONSTANT = 0.01720209895;
|
|
const PARABOLIC_ECCENTRICITY_TOLERANCE = 1e-6;
|
|
|
|
function solveHyperbolicAnomaly(meanAnomaly, eccentricity) {
|
|
let anomaly = meanAnomaly === 0 ? 0 : Math.asinh(meanAnomaly / eccentricity);
|
|
for (let i = 0; i < 40; i += 1) {
|
|
const sinhValue = Math.sinh(anomaly);
|
|
const coshValue = Math.cosh(anomaly);
|
|
const delta = (eccentricity * sinhValue - anomaly - meanAnomaly) / (eccentricity * coshValue - 1);
|
|
anomaly -= delta;
|
|
if (Math.abs(delta) < 1e-12) {
|
|
break;
|
|
}
|
|
}
|
|
return anomaly;
|
|
}
|
|
|
|
function solveParabolicParameter(deltaDays, perihelionDistanceAu) {
|
|
const scale = GAUSSIAN_GRAVITATIONAL_CONSTANT * deltaDays / Math.sqrt(2 * perihelionDistanceAu ** 3);
|
|
let parameter = scale;
|
|
for (let i = 0; i < 40; i += 1) {
|
|
const delta = (parameter + (parameter ** 3) / 3 - scale) / (1 + parameter ** 2);
|
|
parameter -= delta;
|
|
if (Math.abs(delta) < 1e-12) {
|
|
break;
|
|
}
|
|
}
|
|
return parameter;
|
|
}
|
|
|
|
function getCometTrueAnomalyAndRadius(deltaDays, perihelionDistanceAu, eccentricity) {
|
|
if (eccentricity < 1 - PARABOLIC_ECCENTRICITY_TOLERANCE) {
|
|
const semimajorAxis = perihelionDistanceAu / (1 - eccentricity);
|
|
const meanMotion = GAUSSIAN_GRAVITATIONAL_CONSTANT / (semimajorAxis ** 1.5);
|
|
const meanAnomaly = normalizeRadians(meanMotion * deltaDays);
|
|
const eccentricAnomaly = solveKeplerEquation(meanAnomaly, eccentricity);
|
|
const radiusAu = semimajorAxis * (1 - eccentricity * Math.cos(eccentricAnomaly));
|
|
const trueAnomaly = 2 * Math.atan2(
|
|
Math.sqrt(1 + eccentricity) * Math.sin(eccentricAnomaly / 2),
|
|
Math.sqrt(1 - eccentricity) * Math.cos(eccentricAnomaly / 2)
|
|
);
|
|
return { trueAnomaly, radiusAu };
|
|
}
|
|
|
|
if (eccentricity > 1 + PARABOLIC_ECCENTRICITY_TOLERANCE) {
|
|
const semimajorAxisAbs = perihelionDistanceAu / (eccentricity - 1);
|
|
const meanAnomaly = GAUSSIAN_GRAVITATIONAL_CONSTANT * deltaDays / (semimajorAxisAbs ** 1.5);
|
|
const hyperbolicAnomaly = solveHyperbolicAnomaly(meanAnomaly, eccentricity);
|
|
const radiusAu = semimajorAxisAbs * (eccentricity * Math.cosh(hyperbolicAnomaly) - 1);
|
|
const trueAnomaly = 2 * Math.atan2(
|
|
Math.sqrt(eccentricity + 1) * Math.sinh(hyperbolicAnomaly / 2),
|
|
Math.sqrt(eccentricity - 1) * Math.cosh(hyperbolicAnomaly / 2)
|
|
);
|
|
return { trueAnomaly, radiusAu };
|
|
}
|
|
|
|
const parameter = solveParabolicParameter(deltaDays, perihelionDistanceAu);
|
|
return {
|
|
trueAnomaly: 2 * Math.atan(parameter),
|
|
radiusAu: perihelionDistanceAu * (1 + parameter ** 2),
|
|
};
|
|
}
|
|
|
|
function estimateCometMagnitude(comet, helioPosition, earthDistanceAu) {
|
|
if (!comet || comet.absoluteMagnitudeH === null || comet.photometricIndex === null) {
|
|
return null;
|
|
}
|
|
|
|
const r = Math.max(helioPosition.r, 1e-9);
|
|
const delta = Math.max(earthDistanceAu, 1e-9);
|
|
return comet.absoluteMagnitudeH + (5 * Math.log10(delta)) + (2.5 * comet.photometricIndex * Math.log10(r));
|
|
}
|
|
|
|
function getCometHelioPosition(comet, date) {
|
|
if (!comet || !comet.perihelionIso) {
|
|
return null;
|
|
}
|
|
|
|
const perihelionMs = Date.parse(comet.perihelionIso);
|
|
if (Number.isNaN(perihelionMs)) {
|
|
return null;
|
|
}
|
|
|
|
const deltaDays = (date.getTime() - perihelionMs) / 86400000;
|
|
const orbit = getCometTrueAnomalyAndRadius(deltaDays, comet.perihelionDistanceAu, comet.eccentricity);
|
|
const inclination = comet.inclinationDeg * Math.PI / 180;
|
|
const ascendingNode = comet.ascendingNodeDeg * Math.PI / 180;
|
|
const argPerihelion = comet.argPerihelionDeg * Math.PI / 180;
|
|
const argumentOfLatitude = orbit.trueAnomaly + argPerihelion;
|
|
|
|
return {
|
|
x: orbit.radiusAu * ((Math.cos(ascendingNode) * Math.cos(argumentOfLatitude)) - (Math.sin(ascendingNode) * Math.sin(argumentOfLatitude) * Math.cos(inclination))),
|
|
y: orbit.radiusAu * ((Math.sin(ascendingNode) * Math.cos(argumentOfLatitude)) + (Math.cos(ascendingNode) * Math.sin(argumentOfLatitude) * Math.cos(inclination))),
|
|
z: orbit.radiusAu * (Math.sin(argumentOfLatitude) * Math.sin(inclination)),
|
|
r: orbit.radiusAu,
|
|
};
|
|
}
|
|
|
|
function rebuildMinorPlanetOrbit(id) {
|
|
const minorPlanet = getMinorPlanetMeta(id);
|
|
const objects = createMinorPlanetObjects(id);
|
|
if (!minorPlanet || !objects) {
|
|
return;
|
|
}
|
|
|
|
const periodDays = minorPlanet.meanMotionDegPerDay > 0 ? 360 / minorPlanet.meanMotionDegPerDay : 365;
|
|
const steps = Math.max(
|
|
360,
|
|
Math.min(
|
|
1440,
|
|
Math.round(
|
|
360
|
|
+ (minorPlanet.eccentricity * 720)
|
|
+ (Math.log10(Math.max(periodDays, 365) / 365) * 220)
|
|
)
|
|
)
|
|
);
|
|
const positions = [];
|
|
|
|
for (let step = 0; step <= steps; step += 1) {
|
|
const eccentricAnomaly = (step / steps) * Math.PI * 2;
|
|
const heliocentric = getMinorPlanetHelioPositionFromEccentricAnomaly(minorPlanet, eccentricAnomaly);
|
|
if (!heliocentric) {
|
|
continue;
|
|
}
|
|
const display = toDisplayPosition(heliocentric);
|
|
positions.push(display.x, display.y, display.z);
|
|
}
|
|
|
|
objects.orbitLine.geometry.dispose();
|
|
objects.orbitLine.geometry = new THREE.BufferGeometry();
|
|
objects.orbitLine.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
objects.orbitLine.visible = showOrbits;
|
|
}
|
|
|
|
function updateMinorPlanetVisual() {
|
|
syncMinorPlanetObjects();
|
|
|
|
if (!minorPlanetObjects || minorPlanetObjects.size === 0) {
|
|
if (!focusedPlanetKey) {
|
|
infoPanel.hidden = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const selectedSet = getSelectedMinorPlanetSet();
|
|
minorPlanetObjects.forEach((objects, id) => {
|
|
const minorPlanet = getMinorPlanetMeta(id);
|
|
const isSelected = selectedSet.has(id);
|
|
if (!minorPlanet || !isSelected) {
|
|
objects.orbitLine.visible = false;
|
|
objects.mesh.visible = false;
|
|
objects.labelElement.hidden = true;
|
|
return;
|
|
}
|
|
|
|
const heliocentric = getMinorPlanetHelioPosition(minorPlanet, simDate);
|
|
if (!heliocentric) {
|
|
objects.orbitLine.visible = false;
|
|
objects.mesh.visible = false;
|
|
objects.labelElement.hidden = true;
|
|
return;
|
|
}
|
|
|
|
const display = toDisplayPosition(heliocentric);
|
|
objects.mesh.visible = true;
|
|
objects.mesh.position.copy(display);
|
|
objects.mesh.rotation.y = (simDate.getTime() / 1000) * 0.0002;
|
|
objects.orbitLine.visible = showOrbits;
|
|
|
|
if (focusedMinorPlanetId === id) {
|
|
updateMinorPlanetInfoPanel(id);
|
|
}
|
|
});
|
|
|
|
if (focusedMinorPlanetId !== null && !selectedSet.has(focusedMinorPlanetId)) {
|
|
focusedMinorPlanetId = null;
|
|
if (!focusedPlanetKey) {
|
|
infoPanel.hidden = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
function rebuildCometOrbit(id) {
|
|
const comet = getCometMeta(id);
|
|
const objects = createCometObjects(id);
|
|
if (!comet || !objects) {
|
|
return;
|
|
}
|
|
|
|
const positions = [];
|
|
const steps = 720;
|
|
const beforeDays = comet.eccentricity < 1 ? 3650 : 1200;
|
|
const afterDays = comet.eccentricity < 1 ? 3650 : 1200;
|
|
|
|
for (let step = 0; step <= steps; step += 1) {
|
|
const fraction = step / steps;
|
|
const centered = (fraction * 2) - 1;
|
|
const curved = Math.sign(centered) * Math.pow(Math.abs(centered), 2.2);
|
|
const offsetDays = curved < 0
|
|
? curved * beforeDays
|
|
: curved * afterDays;
|
|
const orbitDate = new Date(simDate.getTime() + offsetDays * 86400000);
|
|
const heliocentric = getCometHelioPosition(comet, orbitDate);
|
|
if (!heliocentric) {
|
|
continue;
|
|
}
|
|
const display = toDisplayPosition(heliocentric);
|
|
positions.push(display.x, display.y, display.z);
|
|
}
|
|
|
|
objects.orbitLine.geometry.dispose();
|
|
objects.orbitLine.geometry = new THREE.BufferGeometry();
|
|
objects.orbitLine.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
objects.orbitLine.visible = showOrbits;
|
|
}
|
|
|
|
function updateCometVisual() {
|
|
syncCometObjects();
|
|
|
|
if (!cometObjects || cometObjects.size === 0) {
|
|
if (!focusedPlanetKey && focusedMinorPlanetId === null) {
|
|
infoPanel.hidden = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const selectedSet = getSelectedCometSet();
|
|
cometObjects.forEach((objects, id) => {
|
|
const comet = getCometMeta(id);
|
|
const isSelected = selectedSet.has(id);
|
|
if (!comet || !isSelected) {
|
|
objects.orbitLine.visible = false;
|
|
objects.mesh.visible = false;
|
|
objects.labelElement.hidden = true;
|
|
return;
|
|
}
|
|
|
|
const heliocentric = getCometHelioPosition(comet, simDate);
|
|
if (!heliocentric) {
|
|
objects.orbitLine.visible = false;
|
|
objects.mesh.visible = false;
|
|
objects.labelElement.hidden = true;
|
|
return;
|
|
}
|
|
|
|
const display = toDisplayPosition(heliocentric);
|
|
objects.mesh.visible = true;
|
|
objects.mesh.position.copy(display);
|
|
objects.mesh.rotation.y = (simDate.getTime() / 1000) * 0.0002;
|
|
objects.orbitLine.visible = showOrbits;
|
|
|
|
if (focusedCometId === id) {
|
|
updateCometInfoPanel(id);
|
|
}
|
|
});
|
|
|
|
if (focusedCometId !== null && !selectedSet.has(focusedCometId)) {
|
|
focusedCometId = null;
|
|
if (!focusedPlanetKey && focusedMinorPlanetId === null) {
|
|
infoPanel.hidden = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
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 axisGroup = new THREE.Group();
|
|
const spinGroup = 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;
|
|
spinGroup.add(mesh);
|
|
axisGroup.add(spinGroup);
|
|
|
|
|
|
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 / 2;
|
|
axisGroup.add(ring);
|
|
}
|
|
|
|
group.add(axisGroup);
|
|
|
|
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);
|
|
group.userData.axisGroup = axisGroup;
|
|
group.userData.spinGroup = spinGroup;
|
|
group.userData.surfaceMesh = mesh;
|
|
scene.add(group);
|
|
planetMeshes.set(planet.key, group);
|
|
createOrbitLine(planet);
|
|
}
|
|
|
|
function createEclipticPlane() {
|
|
eclipticGroup = new THREE.Group();
|
|
|
|
// Konzentrische Kreise bei astronomisch sinnvollen AU-Abständen
|
|
const auRings = [1, 2, 3, 4, 5, 10, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100];
|
|
const segments = 128;
|
|
for (const au of auRings) {
|
|
const r = Math.pow(au, 0.56) * DISPLAY_SCALE;
|
|
const pts = [];
|
|
for (let i = 0; i <= segments; i++) {
|
|
const a = (i / segments) * Math.PI * 2;
|
|
pts.push(new THREE.Vector3(Math.cos(a) * r, 0, Math.sin(a) * r));
|
|
}
|
|
const ringGeo = new THREE.BufferGeometry().setFromPoints(pts);
|
|
const ringMat = new THREE.LineBasicMaterial({ color: 0x2c4d66, transparent: true, opacity: 0.45 });
|
|
eclipticGroup.add(new THREE.Line(ringGeo, ringMat));
|
|
}
|
|
|
|
// Radiale Speichen alle 30°
|
|
const maxR = Math.pow(100, 0.56) * DISPLAY_SCALE;
|
|
for (let i = 0; i < 12; i++) {
|
|
const a = (i / 12) * Math.PI * 2;
|
|
const spokePts = [
|
|
new THREE.Vector3(0, 0, 0),
|
|
new THREE.Vector3(Math.cos(a) * maxR, 0, Math.sin(a) * maxR)
|
|
];
|
|
const spokeGeo = new THREE.BufferGeometry().setFromPoints(spokePts);
|
|
const spokeMat = new THREE.LineBasicMaterial({ color: 0x2c4d66, transparent: true, opacity: 0.25 });
|
|
eclipticGroup.add(new THREE.Line(spokeGeo, spokeMat));
|
|
}
|
|
|
|
// Vertikale Achse (Nordpol der Ekliptik)
|
|
const axisGeometry = new THREE.BufferGeometry().setFromPoints([
|
|
new THREE.Vector3(0, -340, 0),
|
|
new THREE.Vector3(0, 340, 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() {
|
|
const upAxis = new THREE.Vector3(0, 1, 0);
|
|
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);
|
|
|
|
const axisGroup = meshGroup.userData.axisGroup;
|
|
const spinGroup = meshGroup.userData.spinGroup;
|
|
const rotationState = getPlanetRotationState(planet.key, simDate);
|
|
if (axisGroup && spinGroup && rotationState) {
|
|
axisGroup.quaternion.setFromUnitVectors(upAxis, rotationState.northDisplay);
|
|
spinGroup.rotation.y = rotationState.spinRad;
|
|
}
|
|
});
|
|
}
|
|
|
|
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 (moonMesh) {
|
|
applyMinimumDisplayScale(moonMesh, 220, 10);
|
|
}
|
|
|
|
if (minorPlanetObjects) {
|
|
minorPlanetObjects.forEach(({ mesh }) => {
|
|
if (mesh.visible) {
|
|
applyMinimumDisplayScale(mesh, 220, 10);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (cometObjects) {
|
|
cometObjects.forEach(({ mesh }) => {
|
|
if (mesh.visible) {
|
|
applyMinimumDisplayScale(mesh, 220, 10);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (voyager1Objects && voyager1Objects.mesh && voyager1Objects.mesh.visible) {
|
|
applyMinimumDisplayScale(voyager1Objects.mesh, 260, 10);
|
|
}
|
|
|
|
if (voyager2Objects && voyager2Objects.mesh && voyager2Objects.mesh.visible) {
|
|
applyMinimumDisplayScale(voyager2Objects.mesh, 260, 10);
|
|
}
|
|
|
|
if (newHorizonsObjects && newHorizonsObjects.mesh && newHorizonsObjects.mesh.visible) {
|
|
applyMinimumDisplayScale(newHorizonsObjects.mesh, 260, 10);
|
|
}
|
|
|
|
if (pioneer10Objects && pioneer10Objects.mesh && pioneer10Objects.mesh.visible) {
|
|
applyMinimumDisplayScale(pioneer10Objects.mesh, 260, 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 (moonLabelElement && moonMesh) {
|
|
moonLabelElement.hidden = !showLabels;
|
|
if (!moonLabelElement.hidden) {
|
|
const projectedMoon = moonMesh.position.clone().project(camera);
|
|
if (projectedMoon.z > 1) {
|
|
moonLabelElement.style.opacity = '0';
|
|
} else {
|
|
moonLabelElement.style.opacity = '1';
|
|
moonLabelElement.style.left = `${(projectedMoon.x * 0.5 + 0.5) * stageWrap.clientWidth}px`;
|
|
moonLabelElement.style.top = `${(-projectedMoon.y * 0.5 + 0.5) * stageWrap.clientHeight}px`;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!minorPlanetObjects) {
|
|
} else {
|
|
minorPlanetObjects.forEach(({ mesh, labelElement }, id) => {
|
|
const minorPlanet = getMinorPlanetMeta(id);
|
|
labelElement.hidden = !showLabels || !minorPlanet || !mesh.visible;
|
|
if (labelElement.hidden) {
|
|
return;
|
|
}
|
|
|
|
const projected = mesh.position.clone().project(camera);
|
|
if (projected.z > 1) {
|
|
labelElement.style.opacity = '0';
|
|
return;
|
|
}
|
|
|
|
labelElement.textContent = formatMinorPlanetName(minorPlanet);
|
|
labelElement.style.opacity = '1';
|
|
labelElement.style.left = `${(projected.x * 0.5 + 0.5) * stageWrap.clientWidth}px`;
|
|
labelElement.style.top = `${(-projected.y * 0.5 + 0.5) * stageWrap.clientHeight}px`;
|
|
});
|
|
}
|
|
|
|
if (!cometObjects) {
|
|
} else {
|
|
cometObjects.forEach(({ mesh, labelElement }, id) => {
|
|
const comet = getCometMeta(id);
|
|
labelElement.hidden = !showLabels || !comet || !mesh.visible;
|
|
if (labelElement.hidden) {
|
|
return;
|
|
}
|
|
|
|
const projected = mesh.position.clone().project(camera);
|
|
if (projected.z > 1) {
|
|
labelElement.style.opacity = '0';
|
|
return;
|
|
}
|
|
|
|
labelElement.textContent = formatCometName(comet);
|
|
labelElement.style.opacity = '1';
|
|
labelElement.style.left = `${(projected.x * 0.5 + 0.5) * stageWrap.clientWidth}px`;
|
|
labelElement.style.top = `${(-projected.y * 0.5 + 0.5) * stageWrap.clientHeight}px`;
|
|
});
|
|
}
|
|
|
|
if (voyager1Objects && voyager1LabelElement) {
|
|
voyager1LabelElement.hidden = !showLabels || !voyager1Objects.mesh.visible;
|
|
if (!voyager1LabelElement.hidden) {
|
|
const projectedVoyager = voyager1Objects.mesh.position.clone().project(camera);
|
|
if (projectedVoyager.z > 1) {
|
|
voyager1LabelElement.style.opacity = '0';
|
|
} else {
|
|
voyager1LabelElement.style.opacity = '1';
|
|
voyager1LabelElement.style.left = `${(projectedVoyager.x * 0.5 + 0.5) * stageWrap.clientWidth}px`;
|
|
voyager1LabelElement.style.top = `${(-projectedVoyager.y * 0.5 + 0.5) * stageWrap.clientHeight}px`;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (voyager2Objects && voyager2LabelElement) {
|
|
voyager2LabelElement.hidden = !showLabels || !voyager2Objects.mesh.visible;
|
|
if (!voyager2LabelElement.hidden) {
|
|
const projectedVoyager2 = voyager2Objects.mesh.position.clone().project(camera);
|
|
if (projectedVoyager2.z > 1) {
|
|
voyager2LabelElement.style.opacity = '0';
|
|
} else {
|
|
voyager2LabelElement.style.opacity = '1';
|
|
voyager2LabelElement.style.left = `${(projectedVoyager2.x * 0.5 + 0.5) * stageWrap.clientWidth}px`;
|
|
voyager2LabelElement.style.top = `${(-projectedVoyager2.y * 0.5 + 0.5) * stageWrap.clientHeight}px`;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (newHorizonsObjects && newHorizonsLabelElement) {
|
|
newHorizonsLabelElement.hidden = !showLabels || !newHorizonsObjects.mesh.visible;
|
|
if (!newHorizonsLabelElement.hidden) {
|
|
const projectedNewHorizons = newHorizonsObjects.mesh.position.clone().project(camera);
|
|
if (projectedNewHorizons.z > 1) {
|
|
newHorizonsLabelElement.style.opacity = '0';
|
|
} else {
|
|
newHorizonsLabelElement.style.opacity = '1';
|
|
newHorizonsLabelElement.style.left = `${(projectedNewHorizons.x * 0.5 + 0.5) * stageWrap.clientWidth}px`;
|
|
newHorizonsLabelElement.style.top = `${(-projectedNewHorizons.y * 0.5 + 0.5) * stageWrap.clientHeight}px`;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pioneer10Objects && pioneer10LabelElement) {
|
|
pioneer10LabelElement.hidden = !showLabels || !pioneer10Objects.mesh.visible;
|
|
if (!pioneer10LabelElement.hidden) {
|
|
const projectedPioneer10 = pioneer10Objects.mesh.position.clone().project(camera);
|
|
if (projectedPioneer10.z > 1) {
|
|
pioneer10LabelElement.style.opacity = '0';
|
|
} else {
|
|
pioneer10LabelElement.style.opacity = '1';
|
|
pioneer10LabelElement.style.left = `${(projectedPioneer10.x * 0.5 + 0.5) * stageWrap.clientWidth}px`;
|
|
pioneer10LabelElement.style.top = `${(-projectedPioneer10.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) => {
|
|
const surfaceMesh = group.userData.surfaceMesh;
|
|
if (surfaceMesh) {
|
|
meshes.push({ key, mesh: surfaceMesh });
|
|
}
|
|
});
|
|
|
|
const hits = raycaster.intersectObjects(meshes.map((item) => item.mesh), false);
|
|
if (minorPlanetObjects) {
|
|
const visibleMinorPlanetMeshes = Array.from(minorPlanetObjects.entries())
|
|
.filter(([, objects]) => objects.mesh.visible)
|
|
.map(([id, objects]) => ({ id, mesh: objects.mesh }));
|
|
|
|
const minorPlanetHits = raycaster.intersectObjects(visibleMinorPlanetMeshes.map((item) => item.mesh), false);
|
|
if (minorPlanetHits.length > 0) {
|
|
const hitItem = visibleMinorPlanetMeshes.find((item) => item.mesh === minorPlanetHits[0].object);
|
|
if (hitItem) {
|
|
focusedPlanetKey = null;
|
|
focusedMinorPlanetId = hitItem.id;
|
|
focusedCometId = null;
|
|
focusedVoyager1 = false;
|
|
focusedVoyager2 = false;
|
|
focusedNewHorizons = false;
|
|
focusedPioneer10 = false;
|
|
document.querySelectorAll('.solarsystem-planet-chip').forEach((button) => {
|
|
button.classList.remove('is-focused');
|
|
});
|
|
updateMinorPlanetInfoPanel(hitItem.id);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (cometObjects) {
|
|
const visibleCometMeshes = Array.from(cometObjects.entries())
|
|
.filter(([, objects]) => objects.mesh.visible)
|
|
.map(([id, objects]) => ({ id, mesh: objects.mesh }));
|
|
|
|
const cometHits = raycaster.intersectObjects(visibleCometMeshes.map((item) => item.mesh), false);
|
|
if (cometHits.length > 0) {
|
|
const hitItem = visibleCometMeshes.find((item) => item.mesh === cometHits[0].object);
|
|
if (hitItem) {
|
|
focusedPlanetKey = null;
|
|
focusedMinorPlanetId = null;
|
|
focusedCometId = hitItem.id;
|
|
focusedVoyager1 = false;
|
|
focusedVoyager2 = false;
|
|
focusedNewHorizons = false;
|
|
focusedPioneer10 = false;
|
|
document.querySelectorAll('.solarsystem-planet-chip').forEach((button) => {
|
|
button.classList.remove('is-focused');
|
|
});
|
|
updateCometInfoPanel(hitItem.id);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (voyager1Objects && voyager1Objects.mesh.visible) {
|
|
const voyagerHits = raycaster.intersectObject(voyager1Objects.mesh, true);
|
|
if (voyagerHits.length > 0) {
|
|
focusedPlanetKey = null;
|
|
focusedMinorPlanetId = null;
|
|
focusedCometId = null;
|
|
focusedVoyager1 = true;
|
|
focusedVoyager2 = false;
|
|
focusedNewHorizons = false;
|
|
focusedPioneer10 = false;
|
|
document.querySelectorAll('.solarsystem-planet-chip').forEach((button) => {
|
|
button.classList.remove('is-focused');
|
|
});
|
|
updateVoyager1InfoPanel();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (voyager2Objects && voyager2Objects.mesh.visible) {
|
|
const voyager2Hits = raycaster.intersectObject(voyager2Objects.mesh, true);
|
|
if (voyager2Hits.length > 0) {
|
|
focusedPlanetKey = null;
|
|
focusedMinorPlanetId = null;
|
|
focusedCometId = null;
|
|
focusedVoyager1 = false;
|
|
focusedVoyager2 = true;
|
|
focusedNewHorizons = false;
|
|
focusedPioneer10 = false;
|
|
document.querySelectorAll('.solarsystem-planet-chip').forEach((button) => {
|
|
button.classList.remove('is-focused');
|
|
});
|
|
updateVoyager2InfoPanel();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (newHorizonsObjects && newHorizonsObjects.mesh.visible) {
|
|
const newHorizonsHits = raycaster.intersectObject(newHorizonsObjects.mesh, true);
|
|
if (newHorizonsHits.length > 0) {
|
|
focusedPlanetKey = null;
|
|
focusedMinorPlanetId = null;
|
|
focusedCometId = null;
|
|
focusedVoyager1 = false;
|
|
focusedVoyager2 = false;
|
|
focusedNewHorizons = true;
|
|
focusedPioneer10 = false;
|
|
document.querySelectorAll('.solarsystem-planet-chip').forEach((button) => {
|
|
button.classList.remove('is-focused');
|
|
});
|
|
updateNewHorizonsInfoPanel();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (pioneer10Objects && pioneer10Objects.mesh.visible) {
|
|
const pioneer10Hits = raycaster.intersectObject(pioneer10Objects.mesh, true);
|
|
if (pioneer10Hits.length > 0) {
|
|
focusedPlanetKey = null;
|
|
focusedMinorPlanetId = null;
|
|
focusedCometId = null;
|
|
focusedVoyager1 = false;
|
|
focusedVoyager2 = false;
|
|
focusedNewHorizons = false;
|
|
focusedPioneer10 = true;
|
|
document.querySelectorAll('.solarsystem-planet-chip').forEach((button) => {
|
|
button.classList.remove('is-focused');
|
|
});
|
|
updatePioneer10InfoPanel();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (hits.length === 0) {
|
|
clearFocus();
|
|
return;
|
|
}
|
|
|
|
const hit = meshes.find((item) => item.mesh === hits[0].object);
|
|
if (hit) {
|
|
focusedCometId = null;
|
|
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(2600, 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 (minorPlanetObjects) {
|
|
minorPlanetObjects.forEach(({ orbitLine }) => {
|
|
orbitLine.visible = showOrbits;
|
|
});
|
|
}
|
|
if (cometObjects) {
|
|
cometObjects.forEach(({ orbitLine }) => {
|
|
orbitLine.visible = showOrbits;
|
|
});
|
|
}
|
|
if (voyager1Objects) {
|
|
voyager1Objects.orbitLine.visible = showOrbits;
|
|
}
|
|
if (voyager2Objects) {
|
|
voyager2Objects.orbitLine.visible = showOrbits;
|
|
}
|
|
if (newHorizonsObjects) {
|
|
newHorizonsObjects.orbitLine.visible = showOrbits;
|
|
}
|
|
if (pioneer10Objects) {
|
|
pioneer10Objects.orbitLine.visible = showOrbits;
|
|
}
|
|
if (moonOrbitLine) {
|
|
moonOrbitLine.visible = showOrbits;
|
|
}
|
|
});
|
|
|
|
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);
|
|
speedSlider.addEventListener('wheel', adjustSpeedSliderFromWheel, { passive: false });
|
|
|
|
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();
|
|
});
|
|
|
|
minorPlanetCheckboxes.forEach((checkbox) => {
|
|
checkbox.addEventListener('change', () => {
|
|
selectedMinorPlanetIds = minorPlanetCheckboxes
|
|
.filter((input) => input.checked)
|
|
.map((input) => Number(input.value));
|
|
|
|
if (focusedMinorPlanetId !== null && !selectedMinorPlanetIds.includes(focusedMinorPlanetId)) {
|
|
focusedMinorPlanetId = null;
|
|
}
|
|
|
|
syncMinorPlanetObjects();
|
|
selectedMinorPlanetIds.forEach((id) => {
|
|
rebuildMinorPlanetOrbit(id);
|
|
});
|
|
updateMinorPlanetSummary();
|
|
|
|
if (selectedMinorPlanetIds.length === 0 && !focusedPlanetKey && focusedMinorPlanetId === null && focusedCometId === null && !focusedVoyager1 && !focusedVoyager2 && !focusedNewHorizons && !focusedPioneer10) {
|
|
infoPanel.hidden = true;
|
|
}
|
|
});
|
|
});
|
|
|
|
cometCheckboxes.forEach((checkbox) => {
|
|
checkbox.addEventListener('change', () => {
|
|
selectedCometIds = cometCheckboxes
|
|
.filter((input) => input.checked)
|
|
.map((input) => Number(input.value));
|
|
|
|
if (focusedCometId !== null && !selectedCometIds.includes(focusedCometId)) {
|
|
focusedCometId = null;
|
|
}
|
|
|
|
syncCometObjects();
|
|
selectedCometIds.forEach((id) => {
|
|
rebuildCometOrbit(id);
|
|
});
|
|
updateCometSummary();
|
|
|
|
if (selectedCometIds.length === 0 && !focusedPlanetKey && focusedMinorPlanetId === null && focusedCometId === null) {
|
|
if (!focusedVoyager1 && !focusedVoyager2 && !focusedNewHorizons && !focusedPioneer10) {
|
|
infoPanel.hidden = true;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
spacecraftCheckboxes.forEach((checkbox) => {
|
|
checkbox.addEventListener('change', () => {
|
|
selectedSpacecraftKeys = spacecraftCheckboxes
|
|
.filter((input) => input.checked)
|
|
.map((input) => String(input.value));
|
|
|
|
syncSpacecraftObjects();
|
|
updateSpacecraftSummary();
|
|
|
|
if (!focusedPlanetKey && focusedMinorPlanetId === null && focusedCometId === null && !focusedVoyager1 && !focusedVoyager2 && !focusedNewHorizons && !focusedPioneer10) {
|
|
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();
|
|
updateMoonVisual();
|
|
updateMinorPlanetVisual();
|
|
updateCometVisual();
|
|
updateVoyager1Visual();
|
|
updateVoyager2Visual();
|
|
updateNewHorizonsVisual();
|
|
updatePioneer10Visual();
|
|
|
|
// 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 && minorPlanetObjects && minorPlanetObjects.has(focusedMinorPlanetId)) {
|
|
const focusedMinorPlanetObjects = minorPlanetObjects.get(focusedMinorPlanetId);
|
|
if (focusedMinorPlanetObjects.mesh.visible) {
|
|
cameraTarget.lerp(focusedMinorPlanetObjects.mesh.position, 0.06);
|
|
updateMinorPlanetInfoPanel(focusedMinorPlanetId);
|
|
}
|
|
}
|
|
|
|
if (focusedCometId && cometObjects && cometObjects.has(focusedCometId)) {
|
|
const focusedCometObjects = cometObjects.get(focusedCometId);
|
|
if (focusedCometObjects.mesh.visible) {
|
|
cameraTarget.lerp(focusedCometObjects.mesh.position, 0.06);
|
|
updateCometInfoPanel(focusedCometId);
|
|
}
|
|
}
|
|
|
|
if (focusedVoyager1 && voyager1Objects && voyager1Objects.mesh.visible) {
|
|
cameraTarget.lerp(voyager1Objects.mesh.position, 0.06);
|
|
updateVoyager1InfoPanel();
|
|
}
|
|
|
|
if (focusedVoyager2 && voyager2Objects && voyager2Objects.mesh.visible) {
|
|
cameraTarget.lerp(voyager2Objects.mesh.position, 0.06);
|
|
updateVoyager2InfoPanel();
|
|
}
|
|
|
|
if (focusedNewHorizons && newHorizonsObjects && newHorizonsObjects.mesh.visible) {
|
|
cameraTarget.lerp(newHorizonsObjects.mesh.position, 0.06);
|
|
updateNewHorizonsInfoPanel();
|
|
}
|
|
|
|
if (focusedPioneer10 && pioneer10Objects && pioneer10Objects.mesh.visible) {
|
|
cameraTarget.lerp(pioneer10Objects.mesh.position, 0.06);
|
|
updatePioneer10InfoPanel();
|
|
}
|
|
|
|
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);
|
|
}
|
|
if (playing && orbitRefreshCursor % 45 === 0) {
|
|
rebuildMoonOrbitLine();
|
|
}
|
|
if (playing && orbitRefreshCursor % 60 === 0) {
|
|
selectedCometIds.forEach((id) => {
|
|
rebuildCometOrbit(id);
|
|
});
|
|
}
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
async function initSolarSystem() {
|
|
try {
|
|
THREE = await import('./js/three.module.js');
|
|
({ GLTFLoader: GLTFLoaderClass } = await import('./js/GLTFLoader.js'));
|
|
({ DRACOLoader: DRACOLoaderClass } = await import('./js/DRACOLoader.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();
|
|
|
|
camera = new THREE.PerspectiveCamera(45, 1, 0.1, 6000);
|
|
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();
|
|
updateMinorPlanetSummary();
|
|
updateCometSummary();
|
|
updateSpacecraftSummary();
|
|
|
|
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 = 2600 + Math.random() * 200;
|
|
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();
|
|
await loadVoyagerModelTemplate();
|
|
PLANETS.forEach(createPlanetMesh);
|
|
ensureMoonObjects();
|
|
ensureMinorPlanetObjects();
|
|
ensureVoyager1Objects();
|
|
rebuildVoyager1Track();
|
|
ensureVoyager2Objects();
|
|
rebuildVoyager2Track();
|
|
ensureNewHorizonsObjects();
|
|
rebuildNewHorizonsTrack();
|
|
ensurePioneer10Objects();
|
|
rebuildPioneer10Track();
|
|
syncSpacecraftObjects();
|
|
attachPointerControls();
|
|
attachUiHandlers();
|
|
updateSpeedFromSlider();
|
|
refreshCurrentDateUi();
|
|
resizeRenderer();
|
|
updatePlanetPositions();
|
|
rebuildMoonOrbitLine();
|
|
updateMoonVisual();
|
|
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'; ?>
|