Kometen jetzt im Sonnensystem
This commit is contained in:
+570
-3
@@ -7,6 +7,7 @@ session_start();
|
|||||||
|
|
||||||
$loggedIn = isset($_SESSION['user_id']);
|
$loggedIn = isset($_SESSION['user_id']);
|
||||||
$trackedMinorplanetsForJs = [];
|
$trackedMinorplanetsForJs = [];
|
||||||
|
$trackedCometsForJs = [];
|
||||||
|
|
||||||
function h(?string $value): string
|
function h(?string $value): string
|
||||||
{
|
{
|
||||||
@@ -71,6 +72,39 @@ function formatMinorplanetDisplayName(array $minorplanet): string
|
|||||||
return $designation !== '' ? $designation : (string) ($minorplanet['packedDesignation'] ?? $minorplanet['desig_packed'] ?? '--');
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
if ($loggedIn) {
|
if ($loggedIn) {
|
||||||
try {
|
try {
|
||||||
$config = require __DIR__ . '/../config/database.php';
|
$config = require __DIR__ . '/../config/database.php';
|
||||||
@@ -138,8 +172,54 @@ if ($loggedIn) {
|
|||||||
'isFavorite' => (bool) $minorplanet['is_favorite'],
|
'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) {
|
} catch (Throwable $e) {
|
||||||
$trackedMinorplanetsForJs = [];
|
$trackedMinorplanetsForJs = [];
|
||||||
|
$trackedCometsForJs = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +299,34 @@ $bodyClass = 'solarsystem-page';
|
|||||||
</div>
|
</div>
|
||||||
</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.' : 'Mehrere Kometen koennen gleichzeitig per Haken eingeblendet werden.' ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="solarsystem-sidebar-block">
|
<div class="solarsystem-sidebar-block">
|
||||||
<div class="solarsystem-label">Hinweise</div>
|
<div class="solarsystem-label">Hinweise</div>
|
||||||
<ul class="solarsystem-notes">
|
<ul class="solarsystem-notes">
|
||||||
@@ -283,6 +391,7 @@ const AstronomyEngine = window.Astronomy;
|
|||||||
const AU_IN_KM = 149597870.7;
|
const AU_IN_KM = 149597870.7;
|
||||||
const DISPLAY_SCALE = 34;
|
const DISPLAY_SCALE = 34;
|
||||||
const USER_MINOR_PLANETS = <?= json_encode($trackedMinorplanetsForJs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
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 JUPITER_MIN_VISIBLE_SIZE = 1.42;
|
const JUPITER_MIN_VISIBLE_SIZE = 1.42;
|
||||||
const MOON_DISPLAY_DISTANCE_MULTIPLIER = 2200;
|
const MOON_DISPLAY_DISTANCE_MULTIPLIER = 2200;
|
||||||
|
|
||||||
@@ -330,6 +439,9 @@ const infoPanel = document.getElementById('solarInfoPanel');
|
|||||||
const minorPlanetSelectWrap = document.getElementById('solarMinorplanetSelectWrap');
|
const minorPlanetSelectWrap = document.getElementById('solarMinorplanetSelectWrap');
|
||||||
const minorPlanetSelectSummary = document.getElementById('solarMinorplanetSelectSummary');
|
const minorPlanetSelectSummary = document.getElementById('solarMinorplanetSelectSummary');
|
||||||
const minorPlanetCheckboxes = Array.from(document.querySelectorAll('.solar-minorplanet-checkbox'));
|
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'));
|
||||||
|
|
||||||
if (!AstronomyEngine) {
|
if (!AstronomyEngine) {
|
||||||
overlay.textContent = 'Astronomy Engine konnte nicht geladen werden.';
|
overlay.textContent = 'Astronomy Engine konnte nicht geladen werden.';
|
||||||
@@ -352,6 +464,7 @@ let moonMesh = null;
|
|||||||
let moonOrbitLine = null;
|
let moonOrbitLine = null;
|
||||||
let moonLabelElement = null;
|
let moonLabelElement = null;
|
||||||
let minorPlanetObjects = null;
|
let minorPlanetObjects = null;
|
||||||
|
let cometObjects = null;
|
||||||
const planetDynamicsCache = new Map();
|
const planetDynamicsCache = new Map();
|
||||||
|
|
||||||
let showOrbits = true;
|
let showOrbits = true;
|
||||||
@@ -362,6 +475,8 @@ let speedDaysPerSecond = 1;
|
|||||||
let focusedPlanetKey = null;
|
let focusedPlanetKey = null;
|
||||||
let selectedMinorPlanetIds = [];
|
let selectedMinorPlanetIds = [];
|
||||||
let focusedMinorPlanetId = null;
|
let focusedMinorPlanetId = null;
|
||||||
|
let selectedCometIds = [];
|
||||||
|
let focusedCometId = null;
|
||||||
let simDate = new Date();
|
let simDate = new Date();
|
||||||
let lastAnimationTime = performance.now();
|
let lastAnimationTime = performance.now();
|
||||||
let orbitRefreshCursor = 0;
|
let orbitRefreshCursor = 0;
|
||||||
@@ -470,6 +585,14 @@ function getSelectedMinorPlanetSet() {
|
|||||||
return new Set(selectedMinorPlanetIds);
|
return new Set(selectedMinorPlanetIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCometMeta(id) {
|
||||||
|
return USER_COMETS.find((comet) => comet.id === Number(id)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedCometSet() {
|
||||||
|
return new Set(selectedCometIds);
|
||||||
|
}
|
||||||
|
|
||||||
const MINOR_PLANET_COLOR_PALETTE = [
|
const MINOR_PLANET_COLOR_PALETTE = [
|
||||||
{ line: '#ff6b6b', body: '#ff5a5a', emissive: '#d63b3b' },
|
{ line: '#ff6b6b', body: '#ff5a5a', emissive: '#d63b3b' },
|
||||||
{ line: '#ff5252', body: '#ff4747', emissive: '#c62828' },
|
{ line: '#ff5252', body: '#ff4747', emissive: '#c62828' },
|
||||||
@@ -484,6 +607,20 @@ function getMinorPlanetColor(id) {
|
|||||||
return MINOR_PLANET_COLOR_PALETTE[safeId % MINOR_PLANET_COLOR_PALETTE.length];
|
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() {
|
function updateMinorPlanetSummary() {
|
||||||
if (!minorPlanetSelectSummary) {
|
if (!minorPlanetSelectSummary) {
|
||||||
return;
|
return;
|
||||||
@@ -508,6 +645,30 @@ function updateMinorPlanetSummary() {
|
|||||||
: `${selectedNames.length} Kleinplaneten ausgewaehlt`;
|
: `${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 formatMinorPlanetName(minorPlanet) {
|
function formatMinorPlanetName(minorPlanet) {
|
||||||
if (!minorPlanet) {
|
if (!minorPlanet) {
|
||||||
return '--';
|
return '--';
|
||||||
@@ -520,6 +681,14 @@ function formatMinorPlanetName(minorPlanet) {
|
|||||||
: designation;
|
: designation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatCometName(comet) {
|
||||||
|
if (!comet) {
|
||||||
|
return '--';
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(comet.designation || comet.packedDesignation || '--').trim();
|
||||||
|
}
|
||||||
|
|
||||||
function getHelioPosition(key, date) {
|
function getHelioPosition(key, date) {
|
||||||
const body = getPlanetBodyKey(key);
|
const body = getPlanetBodyKey(key);
|
||||||
const vectorEqj = AstronomyEngine.HelioVector(body, date);
|
const vectorEqj = AstronomyEngine.HelioVector(body, date);
|
||||||
@@ -650,6 +819,7 @@ function updateCamera() {
|
|||||||
function clearFocus() {
|
function clearFocus() {
|
||||||
focusedPlanetKey = null;
|
focusedPlanetKey = null;
|
||||||
focusedMinorPlanetId = null;
|
focusedMinorPlanetId = null;
|
||||||
|
focusedCometId = null;
|
||||||
cameraTarget.set(0, 0, 0);
|
cameraTarget.set(0, 0, 0);
|
||||||
camRadiusTarget = camRadius;
|
camRadiusTarget = camRadius;
|
||||||
document.querySelectorAll('.solarsystem-planet-chip').forEach((button) => {
|
document.querySelectorAll('.solarsystem-planet-chip').forEach((button) => {
|
||||||
@@ -731,8 +901,51 @@ function updateMinorPlanetInfoPanel(id) {
|
|||||||
document.getElementById('solarInfoType').textContent = `Kleinplanet${minorPlanet.absoluteMagnitudeH !== null ? ` | H ${minorPlanet.absoluteMagnitudeH.toFixed(1)}` : ''}`;
|
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 focusPlanet(key) {
|
function focusPlanet(key) {
|
||||||
focusedPlanetKey = key;
|
focusedPlanetKey = key;
|
||||||
|
focusedMinorPlanetId = null;
|
||||||
|
focusedCometId = null;
|
||||||
document.querySelectorAll('.solarsystem-planet-chip').forEach((button) => {
|
document.querySelectorAll('.solarsystem-planet-chip').forEach((button) => {
|
||||||
button.classList.toggle('is-focused', button.dataset.planet === key);
|
button.classList.toggle('is-focused', button.dataset.planet === key);
|
||||||
});
|
});
|
||||||
@@ -992,6 +1205,187 @@ function getMinorPlanetHelioPosition(minorPlanet, date) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function rebuildMinorPlanetOrbit(id) {
|
||||||
const minorPlanet = getMinorPlanetMeta(id);
|
const minorPlanet = getMinorPlanetMeta(id);
|
||||||
const objects = createMinorPlanetObjects(id);
|
const objects = createMinorPlanetObjects(id);
|
||||||
@@ -1067,6 +1461,88 @@ function updateMinorPlanetVisual() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function createOrbitLine(planet) {
|
||||||
const periodDays = getPlanetDynamics(planet.key).periodDays ?? 365;
|
const periodDays = getPlanetDynamics(planet.key).periodDays ?? 365;
|
||||||
const steps = planet.key === 'Pluto' ? 300 : 220;
|
const steps = planet.key === 'Pluto' ? 300 : 220;
|
||||||
@@ -1271,6 +1747,14 @@ function updateBodyDisplayScales() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cometObjects) {
|
||||||
|
cometObjects.forEach(({ mesh }) => {
|
||||||
|
if (mesh.visible) {
|
||||||
|
applyMinimumDisplayScale(mesh, 220, 10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLabels() {
|
function updateLabels() {
|
||||||
@@ -1312,9 +1796,7 @@ function updateLabels() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!minorPlanetObjects) {
|
if (!minorPlanetObjects) {
|
||||||
return;
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
minorPlanetObjects.forEach(({ mesh, labelElement }, id) => {
|
minorPlanetObjects.forEach(({ mesh, labelElement }, id) => {
|
||||||
const minorPlanet = getMinorPlanetMeta(id);
|
const minorPlanet = getMinorPlanetMeta(id);
|
||||||
labelElement.hidden = !showLabels || !minorPlanet || !mesh.visible;
|
labelElement.hidden = !showLabels || !minorPlanet || !mesh.visible;
|
||||||
@@ -1333,6 +1815,30 @@ function updateLabels() {
|
|||||||
labelElement.style.left = `${(projected.x * 0.5 + 0.5) * stageWrap.clientWidth}px`;
|
labelElement.style.left = `${(projected.x * 0.5 + 0.5) * stageWrap.clientWidth}px`;
|
||||||
labelElement.style.top = `${(-projected.y * 0.5 + 0.5) * stageWrap.clientHeight}px`;
|
labelElement.style.top = `${(-projected.y * 0.5 + 0.5) * stageWrap.clientHeight}px`;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cometObjects) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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`;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickPlanet(clientX, clientY) {
|
function pickPlanet(clientX, clientY) {
|
||||||
@@ -1367,6 +1873,24 @@ function pickPlanet(clientX, clientY) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
updateCometInfoPanel(hitItem.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (hits.length === 0) {
|
if (hits.length === 0) {
|
||||||
clearFocus();
|
clearFocus();
|
||||||
return;
|
return;
|
||||||
@@ -1374,6 +1898,7 @@ function pickPlanet(clientX, clientY) {
|
|||||||
|
|
||||||
const hit = meshes.find((item) => item.mesh === hits[0].object);
|
const hit = meshes.find((item) => item.mesh === hits[0].object);
|
||||||
if (hit) {
|
if (hit) {
|
||||||
|
focusedCometId = null;
|
||||||
focusedMinorPlanetId = null;
|
focusedMinorPlanetId = null;
|
||||||
focusPlanet(hit.key);
|
focusPlanet(hit.key);
|
||||||
}
|
}
|
||||||
@@ -1468,6 +1993,11 @@ function attachUiHandlers() {
|
|||||||
orbitLine.visible = showOrbits;
|
orbitLine.visible = showOrbits;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (cometObjects) {
|
||||||
|
cometObjects.forEach(({ orbitLine }) => {
|
||||||
|
orbitLine.visible = showOrbits;
|
||||||
|
});
|
||||||
|
}
|
||||||
if (moonOrbitLine) {
|
if (moonOrbitLine) {
|
||||||
moonOrbitLine.visible = showOrbits;
|
moonOrbitLine.visible = showOrbits;
|
||||||
}
|
}
|
||||||
@@ -1522,6 +2052,28 @@ function attachUiHandlers() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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) {
|
||||||
|
infoPanel.hidden = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function animate() {
|
function animate() {
|
||||||
@@ -1538,6 +2090,7 @@ function animate() {
|
|||||||
updatePlanetPositions();
|
updatePlanetPositions();
|
||||||
updateMoonVisual();
|
updateMoonVisual();
|
||||||
updateMinorPlanetVisual();
|
updateMinorPlanetVisual();
|
||||||
|
updateCometVisual();
|
||||||
|
|
||||||
// Aim shadow light from sun toward Saturn
|
// Aim shadow light from sun toward Saturn
|
||||||
const saturnMesh = planetMeshes.get('Saturn');
|
const saturnMesh = planetMeshes.get('Saturn');
|
||||||
@@ -1567,6 +2120,14 @@ function animate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (focusedCometId && cometObjects && cometObjects.has(focusedCometId)) {
|
||||||
|
const focusedCometObjects = cometObjects.get(focusedCometId);
|
||||||
|
if (focusedCometObjects.mesh.visible) {
|
||||||
|
cameraTarget.lerp(focusedCometObjects.mesh.position, 0.06);
|
||||||
|
updateCometInfoPanel(focusedCometId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
camRadius += (camRadiusTarget - camRadius) * 0.18;
|
camRadius += (camRadiusTarget - camRadius) * 0.18;
|
||||||
updateCamera();
|
updateCamera();
|
||||||
updateBodyDisplayScales();
|
updateBodyDisplayScales();
|
||||||
@@ -1580,6 +2141,11 @@ function animate() {
|
|||||||
if (playing && orbitRefreshCursor % 45 === 0) {
|
if (playing && orbitRefreshCursor % 45 === 0) {
|
||||||
rebuildMoonOrbitLine();
|
rebuildMoonOrbitLine();
|
||||||
}
|
}
|
||||||
|
if (playing && orbitRefreshCursor % 60 === 0) {
|
||||||
|
selectedCometIds.forEach((id) => {
|
||||||
|
rebuildCometOrbit(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1605,6 +2171,7 @@ async function initSolarSystem() {
|
|||||||
cameraTarget = new THREE.Vector3(0, 0, 0);
|
cameraTarget = new THREE.Vector3(0, 0, 0);
|
||||||
rotationEqjToEcl = AstronomyEngine.Rotation_EQJ_ECL();
|
rotationEqjToEcl = AstronomyEngine.Rotation_EQJ_ECL();
|
||||||
updateMinorPlanetSummary();
|
updateMinorPlanetSummary();
|
||||||
|
updateCometSummary();
|
||||||
|
|
||||||
const ambientLight = new THREE.AmbientLight(0x334466, 0.8);
|
const ambientLight = new THREE.AmbientLight(0x334466, 0.8);
|
||||||
scene.add(ambientLight);
|
scene.add(ambientLight);
|
||||||
|
|||||||
Reference in New Issue
Block a user