Kometen jetzt im Sonnensystem
This commit is contained in:
+571
-4
@@ -7,6 +7,7 @@ session_start();
|
||||
|
||||
$loggedIn = isset($_SESSION['user_id']);
|
||||
$trackedMinorplanetsForJs = [];
|
||||
$trackedCometsForJs = [];
|
||||
|
||||
function h(?string $value): string
|
||||
{
|
||||
@@ -71,6 +72,39 @@ function formatMinorplanetDisplayName(array $minorplanet): string
|
||||
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) {
|
||||
try {
|
||||
$config = require __DIR__ . '/../config/database.php';
|
||||
@@ -138,8 +172,54 @@ if ($loggedIn) {
|
||||
'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 = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +299,34 @@ $bodyClass = 'solarsystem-page';
|
||||
</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-label">Hinweise</div>
|
||||
<ul class="solarsystem-notes">
|
||||
@@ -283,6 +391,7 @@ 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 JUPITER_MIN_VISIBLE_SIZE = 1.42;
|
||||
const MOON_DISPLAY_DISTANCE_MULTIPLIER = 2200;
|
||||
|
||||
@@ -330,6 +439,9 @@ 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'));
|
||||
|
||||
if (!AstronomyEngine) {
|
||||
overlay.textContent = 'Astronomy Engine konnte nicht geladen werden.';
|
||||
@@ -352,6 +464,7 @@ let moonMesh = null;
|
||||
let moonOrbitLine = null;
|
||||
let moonLabelElement = null;
|
||||
let minorPlanetObjects = null;
|
||||
let cometObjects = null;
|
||||
const planetDynamicsCache = new Map();
|
||||
|
||||
let showOrbits = true;
|
||||
@@ -362,6 +475,8 @@ let speedDaysPerSecond = 1;
|
||||
let focusedPlanetKey = null;
|
||||
let selectedMinorPlanetIds = [];
|
||||
let focusedMinorPlanetId = null;
|
||||
let selectedCometIds = [];
|
||||
let focusedCometId = null;
|
||||
let simDate = new Date();
|
||||
let lastAnimationTime = performance.now();
|
||||
let orbitRefreshCursor = 0;
|
||||
@@ -470,6 +585,14 @@ 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);
|
||||
}
|
||||
|
||||
const MINOR_PLANET_COLOR_PALETTE = [
|
||||
{ line: '#ff6b6b', body: '#ff5a5a', emissive: '#d63b3b' },
|
||||
{ line: '#ff5252', body: '#ff4747', emissive: '#c62828' },
|
||||
@@ -484,6 +607,20 @@ function getMinorPlanetColor(id) {
|
||||
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;
|
||||
@@ -508,6 +645,30 @@ function updateMinorPlanetSummary() {
|
||||
: `${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) {
|
||||
if (!minorPlanet) {
|
||||
return '--';
|
||||
@@ -520,6 +681,14 @@ function formatMinorPlanetName(minorPlanet) {
|
||||
: 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);
|
||||
@@ -650,6 +819,7 @@ function updateCamera() {
|
||||
function clearFocus() {
|
||||
focusedPlanetKey = null;
|
||||
focusedMinorPlanetId = null;
|
||||
focusedCometId = null;
|
||||
cameraTarget.set(0, 0, 0);
|
||||
camRadiusTarget = camRadius;
|
||||
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)}` : ''}`;
|
||||
}
|
||||
|
||||
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) {
|
||||
focusedPlanetKey = key;
|
||||
focusedMinorPlanetId = null;
|
||||
focusedCometId = null;
|
||||
document.querySelectorAll('.solarsystem-planet-chip').forEach((button) => {
|
||||
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) {
|
||||
const minorPlanet = getMinorPlanetMeta(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) {
|
||||
const periodDays = getPlanetDynamics(planet.key).periodDays ?? 365;
|
||||
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() {
|
||||
@@ -1312,12 +1796,34 @@ function updateLabels() {
|
||||
}
|
||||
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
minorPlanetObjects.forEach(({ mesh, labelElement }, id) => {
|
||||
const minorPlanet = getMinorPlanetMeta(id);
|
||||
labelElement.hidden = !showLabels || !minorPlanet || !mesh.visible;
|
||||
cometObjects.forEach(({ mesh, labelElement }, id) => {
|
||||
const comet = getCometMeta(id);
|
||||
labelElement.hidden = !showLabels || !comet || !mesh.visible;
|
||||
if (labelElement.hidden) {
|
||||
return;
|
||||
}
|
||||
@@ -1328,7 +1834,7 @@ function updateLabels() {
|
||||
return;
|
||||
}
|
||||
|
||||
labelElement.textContent = formatMinorPlanetName(minorPlanet);
|
||||
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`;
|
||||
@@ -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) {
|
||||
clearFocus();
|
||||
return;
|
||||
@@ -1374,6 +1898,7 @@ function pickPlanet(clientX, clientY) {
|
||||
|
||||
const hit = meshes.find((item) => item.mesh === hits[0].object);
|
||||
if (hit) {
|
||||
focusedCometId = null;
|
||||
focusedMinorPlanetId = null;
|
||||
focusPlanet(hit.key);
|
||||
}
|
||||
@@ -1468,6 +1993,11 @@ function attachUiHandlers() {
|
||||
orbitLine.visible = showOrbits;
|
||||
});
|
||||
}
|
||||
if (cometObjects) {
|
||||
cometObjects.forEach(({ orbitLine }) => {
|
||||
orbitLine.visible = showOrbits;
|
||||
});
|
||||
}
|
||||
if (moonOrbitLine) {
|
||||
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() {
|
||||
@@ -1538,6 +2090,7 @@ function animate() {
|
||||
updatePlanetPositions();
|
||||
updateMoonVisual();
|
||||
updateMinorPlanetVisual();
|
||||
updateCometVisual();
|
||||
|
||||
// Aim shadow light from sun toward 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;
|
||||
updateCamera();
|
||||
updateBodyDisplayScales();
|
||||
@@ -1580,6 +2141,11 @@ function animate() {
|
||||
if (playing && orbitRefreshCursor % 45 === 0) {
|
||||
rebuildMoonOrbitLine();
|
||||
}
|
||||
if (playing && orbitRefreshCursor % 60 === 0) {
|
||||
selectedCometIds.forEach((id) => {
|
||||
rebuildCometOrbit(id);
|
||||
});
|
||||
}
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
@@ -1605,6 +2171,7 @@ async function initSolarSystem() {
|
||||
cameraTarget = new THREE.Vector3(0, 0, 0);
|
||||
rotationEqjToEcl = AstronomyEngine.Rotation_EQJ_ECL();
|
||||
updateMinorPlanetSummary();
|
||||
updateCometSummary();
|
||||
|
||||
const ambientLight = new THREE.AmbientLight(0x334466, 0.8);
|
||||
scene.add(ambientLight);
|
||||
|
||||
Reference in New Issue
Block a user