1410 lines
51 KiB
PHP
1410 lines
51 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
ini_set('display_errors', '1');
|
|
error_reporting(E_ALL);
|
|
session_start();
|
|
|
|
$loggedIn = isset($_SESSION['user_id']);
|
|
$pageTitle = 'Astronomische Umrechnungen';
|
|
$headerIntroPre = 'Astronomie-Werkzeuge';
|
|
$headerIntroTitle = 'Astronomische Umrechnungen';
|
|
$headerIntroSub = 'Entfernungen zwischen haeufigen astronomischen Einheiten umrechnen.';
|
|
|
|
function h(?string $value): string
|
|
{
|
|
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
|
}
|
|
|
|
function runPythonApi(string $action, array $args): array
|
|
{
|
|
$scriptPath = __DIR__ . '/py/api.py';
|
|
$pythonCandidates = PHP_OS_FAMILY === 'Windows'
|
|
? ['python', 'py']
|
|
: ['/usr/bin/python3', '/usr/bin/python', 'python3', 'python'];
|
|
|
|
$escapedArguments = array_map('escapeshellarg', array_merge([$scriptPath, $action], $args));
|
|
$lastError = 'Python konnte nicht gestartet werden.';
|
|
|
|
foreach ($pythonCandidates as $pythonBinary) {
|
|
$command = $pythonBinary . ' ' . implode(' ', $escapedArguments) . ' 2>&1';
|
|
$output = [];
|
|
$resultCode = 0;
|
|
exec($command, $output, $resultCode);
|
|
|
|
$joined = trim(implode("\n", $output));
|
|
$decoded = json_decode($joined, true);
|
|
|
|
if (is_array($decoded)) {
|
|
return [
|
|
'ok' => (bool) ($decoded['ok'] ?? false),
|
|
'data' => $decoded,
|
|
'raw' => $joined,
|
|
'command' => $command,
|
|
];
|
|
}
|
|
|
|
if ($joined !== '') {
|
|
$lastError = $joined;
|
|
} elseif ($resultCode !== 0) {
|
|
$lastError = 'Fehlercode ' . $resultCode . ' bei ' . $pythonBinary;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'ok' => false,
|
|
'error' => $lastError,
|
|
];
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string) ($_POST['ajax'] ?? '') === '1') {
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
|
|
$kind = trim((string) ($_POST['kind'] ?? ''));
|
|
if ($kind === 'distance') {
|
|
$pythonResult = runPythonApi('astronomical_conversions', [
|
|
'distance',
|
|
trim((string) ($_POST['sourceUnit'] ?? '')),
|
|
trim((string) ($_POST['sourceValue'] ?? '')),
|
|
]);
|
|
} elseif ($kind === 'redshift') {
|
|
$pythonResult = runPythonApi('astronomical_conversions', [
|
|
'redshift',
|
|
trim((string) ($_POST['z'] ?? '')),
|
|
trim((string) ($_POST['restNm'] ?? '')),
|
|
]);
|
|
} elseif ($kind === 'time') {
|
|
$pythonResult = runPythonApi('astronomical_conversions', [
|
|
'time',
|
|
trim((string) ($_POST['sourceUnit'] ?? '')),
|
|
trim((string) ($_POST['sourceValue'] ?? '')),
|
|
trim((string) ($_POST['longitude'] ?? '')),
|
|
]);
|
|
} else {
|
|
echo json_encode([
|
|
'ok' => false,
|
|
'error' => 'Unbekannter Umrechnungsbereich.',
|
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
exit;
|
|
}
|
|
|
|
if (!$pythonResult['ok']) {
|
|
echo json_encode([
|
|
'ok' => false,
|
|
'error' => (string) ($pythonResult['error'] ?? 'Python-Fehler'),
|
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
exit;
|
|
}
|
|
|
|
echo json_encode($pythonResult['data'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
exit;
|
|
}
|
|
?>
|
|
<?php require __DIR__ . '/header.php'; ?>
|
|
<?php
|
|
$timeLocationName = !empty($currentLocation['name']) ? (string) $currentLocation['name'] : '';
|
|
$timeLocationLongitude = isset($currentLocation['longitude']) && is_numeric($currentLocation['longitude'])
|
|
? (float) $currentLocation['longitude']
|
|
: null;
|
|
?>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
|
|
|
<style>
|
|
.astro-conv-layout {
|
|
display: grid;
|
|
gap: 1.25rem;
|
|
}
|
|
|
|
.astro-conv-tabs {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
margin: 0 0 1.75rem;
|
|
border-bottom: 1px solid var(--border);
|
|
padding-bottom: 0;
|
|
}
|
|
|
|
.astro-conv-tab {
|
|
border: 1px solid transparent;
|
|
border-bottom: none;
|
|
background: transparent;
|
|
color: var(--text-dim);
|
|
border-radius: var(--radius) var(--radius) 0 0;
|
|
padding: 0.6rem 1.25rem;
|
|
cursor: pointer;
|
|
font: inherit;
|
|
font-size: 0.92rem;
|
|
letter-spacing: 0.03em;
|
|
transition: color 0.15s, background 0.15s, border-color 0.15s;
|
|
position: relative;
|
|
bottom: -1px;
|
|
}
|
|
|
|
.astro-conv-tab:hover {
|
|
color: var(--gold-pale);
|
|
background: rgba(201, 168, 76, 0.06);
|
|
border-color: var(--border);
|
|
}
|
|
|
|
.astro-conv-tab.is-active {
|
|
color: var(--gold-pale);
|
|
background: var(--bg-card);
|
|
border-color: var(--border);
|
|
border-bottom-color: var(--bg-card);
|
|
}
|
|
|
|
.astro-conv-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: 0.85rem;
|
|
}
|
|
|
|
.astro-conv-field {
|
|
display: grid;
|
|
gap: 0.45rem;
|
|
}
|
|
|
|
.astro-conv-field label {
|
|
color: var(--text-dim);
|
|
font-size: 0.94rem;
|
|
}
|
|
|
|
.astro-conv-label-help {
|
|
border-bottom: 1px dotted rgba(201, 168, 76, 0.4);
|
|
cursor: help;
|
|
}
|
|
|
|
.astro-conv-input {
|
|
width: 100%;
|
|
padding: 0.72rem 0.8rem;
|
|
border-radius: var(--radius);
|
|
border: 1px solid var(--border);
|
|
background: rgba(255,255,255,0.04);
|
|
color: var(--text);
|
|
font-size: 0.96rem;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.astro-conv-input:focus {
|
|
outline: none;
|
|
border-color: rgba(201, 168, 76, 0.5);
|
|
box-shadow: 0 0 0 3px rgba(201, 168, 76, 0.1);
|
|
}
|
|
|
|
.astro-conv-field {
|
|
position: relative;
|
|
}
|
|
|
|
.astro-conv-tooltip {
|
|
position: absolute;
|
|
left: 0;
|
|
top: calc(100% + 0.45rem);
|
|
z-index: 20;
|
|
min-width: 14rem;
|
|
max-width: 22rem;
|
|
padding: 0.7rem 0.8rem;
|
|
border-radius: 0.8rem;
|
|
background: rgba(6, 8, 18, 0.97);
|
|
border: 1px solid var(--border);
|
|
color: var(--text);
|
|
font-size: 0.9rem;
|
|
line-height: 1.45;
|
|
white-space: pre-line;
|
|
box-shadow: 0 12px 30px rgba(0,0,0,0.35);
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transform: translateY(-0.25rem);
|
|
transition: opacity 140ms ease, transform 140ms ease;
|
|
}
|
|
|
|
.astro-conv-field.is-tooltip-open .astro-conv-tooltip {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.astro-conv-actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.8rem;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.astro-conv-tab-panel {
|
|
display: none;
|
|
}
|
|
|
|
.astro-conv-tab-panel.is-active {
|
|
display: block;
|
|
}
|
|
|
|
.astro-conv-panel {
|
|
border-radius: 0 var(--radius) var(--radius) var(--radius);
|
|
}
|
|
|
|
@media (max-width: 960px) {
|
|
.astro-conv-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<div class="astro-conv-layout">
|
|
<div class="astro-conv-tabs" role="tablist" aria-label="Umrechnungsbereiche">
|
|
<button type="button" class="astro-conv-tab is-active" data-tab-target="distanceTab" role="tab" aria-selected="true">Entfernung</button>
|
|
<button type="button" class="astro-conv-tab" data-tab-target="redshiftTab" role="tab" aria-selected="false">Rotverschiebung</button>
|
|
<button type="button" class="astro-conv-tab" data-tab-target="timeTab" role="tab" aria-selected="false">Zeit</button>
|
|
</div>
|
|
|
|
<section class="card astro-conv-panel astro-conv-tab-panel is-active" id="distanceTab" role="tabpanel">
|
|
<h2>Entfernungs-Umrechner</h2>
|
|
|
|
<div class="astro-conv-grid" id="distanceConverter">
|
|
<div class="astro-conv-field">
|
|
<label for="distanceKm">Kilometer (km)</label>
|
|
<input class="astro-conv-input" id="distanceKm" type="text" inputmode="decimal" value="384400">
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="distanceAu">Astronomische Einheiten (AU)</label>
|
|
<input class="astro-conv-input" id="distanceAu" type="text" inputmode="decimal">
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="distanceLightMinute">Lichtminuten (lm)</label>
|
|
<input class="astro-conv-input" id="distanceLightMinute" type="text" inputmode="decimal">
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="distanceLightHour">Lichtstunden (lh)</label>
|
|
<input class="astro-conv-input" id="distanceLightHour" type="text" inputmode="decimal">
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="distanceLy">Lichtjahre (ly)</label>
|
|
<input class="astro-conv-input" id="distanceLy" type="text" inputmode="decimal">
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="distancePc">Parsec (pc)</label>
|
|
<input class="astro-conv-input" id="distancePc" type="text" inputmode="decimal">
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="distanceMpc">Megaparsec (Mpc)</label>
|
|
<input class="astro-conv-input" id="distanceMpc" type="text" inputmode="decimal">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="astro-conv-actions">
|
|
<button type="button" class="btn btn-secondary" id="distanceClear">Leeren</button>
|
|
<button type="button" class="btn btn-primary" id="distanceMoon">Mondentfernung</button>
|
|
<button type="button" class="btn btn-primary" id="distanceSun">1 AU</button>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card astro-conv-panel astro-conv-tab-panel" id="redshiftTab" role="tabpanel">
|
|
<h2>Rotverschiebung (z)</h2>
|
|
|
|
<div class="astro-conv-grid" id="redshiftConverter">
|
|
<div class="astro-conv-field">
|
|
<label for="redshiftZ">Rotverschiebung z</label>
|
|
<input class="astro-conv-input" id="redshiftZ" type="text" inputmode="decimal" value="0,1">
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="redshiftFactor"><span class="astro-conv-label-help" title="Der Faktor, um den sich Wellenlaengen durch die Expansion des Universums verlaengern.">Faktor (1 + z)</span></label>
|
|
<input class="astro-conv-input" id="redshiftFactor" type="text" inputmode="decimal" readonly>
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="redshiftScaleFactor"><span class="astro-conv-label-help" title="Der kosmologische Skalenfaktor a = 1 / (1 + z). Er beschreibt, wie gross das Universum zum Zeitpunkt der Aussendung relativ zu heute war.">Skalenfaktor a</span></label>
|
|
<input class="astro-conv-input" id="redshiftScaleFactor" type="text" inputmode="decimal" readonly>
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="redshiftVelocityApprox"><span class="astro-conv-label-help" title="Einfache Naeherung fuer kleine Rotverschiebungen: v ungefaehr gleich z mal Lichtgeschwindigkeit.">Geschwindigkeit naeherungsweise (km/s)</span></label>
|
|
<input class="astro-conv-input" id="redshiftVelocityApprox" type="text" inputmode="decimal" readonly>
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="redshiftVelocityRel"><span class="astro-conv-label-help" title="Relativistisch korrigierte Geschwindigkeit aus der Doppler-Formel. Sinnvoller als die lineare Naeherung bei groesserem z.">Geschwindigkeit relativistisch (km/s)</span></label>
|
|
<input class="astro-conv-input" id="redshiftVelocityRel" type="text" inputmode="decimal" readonly>
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="redshiftVelocityC"><span class="astro-conv-label-help" title="Dieselbe relativistische Geschwindigkeit, aber als Anteil der Lichtgeschwindigkeit in Prozent.">Geschwindigkeit relativistisch (% c)</span></label>
|
|
<input class="astro-conv-input" id="redshiftVelocityC" type="text" inputmode="decimal" readonly>
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="redshiftRestNm">Ruhewellenlaenge (nm)</label>
|
|
<input class="astro-conv-input" id="redshiftRestNm" type="text" inputmode="decimal" value="656,3">
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="redshiftObservedNm">Beobachtete Wellenlaenge (nm)</label>
|
|
<input class="astro-conv-input" id="redshiftObservedNm" type="text" inputmode="decimal" readonly>
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="redshiftFrequencyFactor"><span class="astro-conv-label-help" title="Die beobachtete Frequenz ist um diesen Faktor kleiner als die urspruengliche Frequenz.">Frequenzfaktor 1 / (1 + z)</span></label>
|
|
<input class="astro-conv-input" id="redshiftFrequencyFactor" type="text" inputmode="decimal" readonly>
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="redshiftLookbackGyr"><span class="astro-conv-label-help" title="Zeit, die das Licht seit seiner Aussendung bis heute unterwegs war. Gyr steht fuer Milliarden Jahre.">Rueckblickzeit (Gyr)</span></label>
|
|
<input class="astro-conv-input" id="redshiftLookbackGyr" type="text" inputmode="decimal" readonly>
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="redshiftComovingMpc"><span class="astro-conv-label-help" title="Kosmologische Entfernung im mitbewegten Koordinatensystem. Sie bleibt fuer mit der Expansion mitbewegte Objekte zeitlich konstant.">Mitbewegte Entfernung (Mpc)</span></label>
|
|
<input class="astro-conv-input" id="redshiftComovingMpc" type="text" inputmode="decimal" readonly>
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="redshiftLuminosityMpc"><span class="astro-conv-label-help" title="Entfernung, die man bei der beobachteten Helligkeit eines Objekts in einem expandierenden Universum verwendet. Wichtig fuer Standardkerzen.">Leuchtkraftentfernung (Mpc)</span></label>
|
|
<input class="astro-conv-input" id="redshiftLuminosityMpc" type="text" inputmode="decimal" readonly>
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="redshiftAngularDiameterMpc"><span class="astro-conv-label-help" title="Entfernung, die den beobachteten Winkel mit der echten Groesse eines Objekts verknuepft. Wichtig fuer scheinbare Groessen am Himmel.">Winkeldurchmesser-Entfernung (Mpc)</span></label>
|
|
<input class="astro-conv-input" id="redshiftAngularDiameterMpc" type="text" inputmode="decimal" readonly>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="astro-conv-actions">
|
|
<button type="button" class="btn btn-secondary" id="redshiftClear">Leeren</button>
|
|
<button type="button" class="btn btn-primary" id="redshiftGalaxy">z = 0,1</button>
|
|
<button type="button" class="btn btn-primary" id="redshiftQuasar">z = 6</button>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card astro-conv-panel astro-conv-tab-panel" id="timeTab" role="tabpanel">
|
|
<h2>Zeit-Umrechner</h2>
|
|
|
|
<div class="astro-conv-grid" id="timeConverter">
|
|
<div class="astro-conv-field">
|
|
<label for="timeUtc">UTC</label>
|
|
<input class="astro-conv-input" id="timeUtc" type="text" value="2026-04-06 12:00:00" autocomplete="off">
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="timeUt1">UT1</label>
|
|
<input class="astro-conv-input" id="timeUt1" type="text" readonly>
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="timeTai">TAI</label>
|
|
<input class="astro-conv-input" id="timeTai" type="text" readonly>
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="timeJd">Julianisches Datum (JD)</label>
|
|
<input class="astro-conv-input" id="timeJd" type="text" inputmode="decimal">
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="timeMjd">Modifiziertes Julianisches Datum (MJD)</label>
|
|
<input class="astro-conv-input" id="timeMjd" type="text" inputmode="decimal">
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="timeUnix">Unix-Zeit</label>
|
|
<input class="astro-conv-input" id="timeUnix" type="text" inputmode="decimal">
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="timeTrueSolar">
|
|
<span
|
|
class="astro-conv-label-help"
|
|
title="<?= h($timeLocationLongitude !== null
|
|
? 'Wahre Ortszeit fuer den Standard-Standort aus dem Header. Berechnet aus UTC, Laengengrad und Gleichung der Zeit.'
|
|
: 'Wahre Ortszeit ist nur verfuegbar, wenn ein Standard-Standort mit Laengengrad hinterlegt ist.') ?>"
|
|
>Wahre Ortszeit<?= $timeLocationName !== '' ? ' (' . h($timeLocationName) . ')' : '' ?></span>
|
|
</label>
|
|
<input
|
|
class="astro-conv-input"
|
|
id="timeTrueSolar"
|
|
type="text"
|
|
value="<?= h($timeLocationLongitude !== null ? '' : 'Kein Standard-Standort verfuegbar') ?>"
|
|
readonly
|
|
>
|
|
</div>
|
|
|
|
<div class="astro-conv-field">
|
|
<label for="timeSidereal">
|
|
<span
|
|
class="astro-conv-label-help"
|
|
title="<?= h($timeLocationLongitude !== null
|
|
? 'Lokale Sternzeit fuer den Standard-Standort aus dem Header. Berechnet aus Julianischem Datum und Laengengrad.'
|
|
: 'Sternzeit ist nur verfuegbar, wenn ein Standard-Standort mit Laengengrad hinterlegt ist.') ?>"
|
|
>Sternzeit<?= $timeLocationName !== '' ? ' (' . h($timeLocationName) . ')' : '' ?></span>
|
|
</label>
|
|
<input
|
|
class="astro-conv-input"
|
|
id="timeSidereal"
|
|
type="text"
|
|
value="<?= h($timeLocationLongitude !== null ? '' : 'Kein Standard-Standort verfuegbar') ?>"
|
|
readonly
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="astro-conv-actions">
|
|
<button type="button" class="btn btn-secondary" id="timeClear">Leeren</button>
|
|
<button type="button" class="btn btn-primary" id="timeNow">Jetzt</button>
|
|
<button type="button" class="btn btn-primary" id="timeJ2000">J2000</button>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<script src="js/number_to_german_words.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/de.js"></script>
|
|
<script>
|
|
(function () {
|
|
const CONVERSIONS_ENDPOINT = window.location.href;
|
|
const USER_TIME_LOCATION = {
|
|
name: <?= json_encode($timeLocationName, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>,
|
|
longitude: <?= $timeLocationLongitude !== null ? json_encode($timeLocationLongitude) : 'null' ?>
|
|
};
|
|
const SPEED_OF_LIGHT_KM_S = 299792.458;
|
|
const H0 = 70;
|
|
const OMEGA_M = 0.3;
|
|
const OMEGA_LAMBDA = 0.7;
|
|
const MPC_IN_KM = 3.0856775814913673e19;
|
|
const SECONDS_PER_GYR = 365.25 * 24 * 3600 * 1e9;
|
|
const KM_PER_AU = 149597870.7;
|
|
const KM_PER_LIGHT_MINUTE = 17987547.48;
|
|
const KM_PER_LIGHT_HOUR = 1079252848.8;
|
|
const KM_PER_LY = 9460730472580.8;
|
|
const KM_PER_PC = 30856775814913.67;
|
|
const KM_PER_MPC = KM_PER_PC * 1000000;
|
|
|
|
const distanceFields = {
|
|
km: document.getElementById('distanceKm'),
|
|
au: document.getElementById('distanceAu'),
|
|
lm: document.getElementById('distanceLightMinute'),
|
|
lh: document.getElementById('distanceLightHour'),
|
|
ly: document.getElementById('distanceLy'),
|
|
pc: document.getElementById('distancePc'),
|
|
mpc: document.getElementById('distanceMpc')
|
|
};
|
|
|
|
const redshiftFields = {
|
|
z: document.getElementById('redshiftZ'),
|
|
factor: document.getElementById('redshiftFactor'),
|
|
scaleFactor: document.getElementById('redshiftScaleFactor'),
|
|
velocityApprox: document.getElementById('redshiftVelocityApprox'),
|
|
velocityRel: document.getElementById('redshiftVelocityRel'),
|
|
velocityC: document.getElementById('redshiftVelocityC'),
|
|
restNm: document.getElementById('redshiftRestNm'),
|
|
observedNm: document.getElementById('redshiftObservedNm'),
|
|
frequencyFactor: document.getElementById('redshiftFrequencyFactor'),
|
|
lookbackGyr: document.getElementById('redshiftLookbackGyr'),
|
|
comovingMpc: document.getElementById('redshiftComovingMpc'),
|
|
luminosityMpc: document.getElementById('redshiftLuminosityMpc'),
|
|
angularDiameterMpc: document.getElementById('redshiftAngularDiameterMpc')
|
|
};
|
|
|
|
const timeFields = {
|
|
utc: document.getElementById('timeUtc'),
|
|
ut1: document.getElementById('timeUt1'),
|
|
tai: document.getElementById('timeTai'),
|
|
jd: document.getElementById('timeJd'),
|
|
mjd: document.getElementById('timeMjd'),
|
|
unix: document.getElementById('timeUnix'),
|
|
trueSolar: document.getElementById('timeTrueSolar'),
|
|
sidereal: document.getElementById('timeSidereal')
|
|
};
|
|
let isSyncingTimePicker = false;
|
|
let timeUtcPicker = null;
|
|
let distanceRequestId = 0;
|
|
let redshiftRequestId = 0;
|
|
let timeRequestId = 0;
|
|
|
|
const DECIMAL_DIVISION_PRECISION = 32;
|
|
const pow10Cache = [1n];
|
|
const decimalConstants = {
|
|
zero: null,
|
|
one: null,
|
|
hundred: null,
|
|
thousand: null,
|
|
million: null,
|
|
speedOfLightKmS: null,
|
|
secondsPerGyr: null,
|
|
mpcInKm: null,
|
|
kmPerAu: null,
|
|
kmPerLightMinute: null,
|
|
kmPerLightHour: null,
|
|
kmPerLy: null,
|
|
kmPerPc: null,
|
|
kmPerMpc: null,
|
|
jdUnixEpoch: null,
|
|
mjdOffset: null,
|
|
msPerDay: null,
|
|
msPerSecond: null,
|
|
};
|
|
|
|
const getPow10 = function (exponent) {
|
|
for (let i = pow10Cache.length; i <= exponent; i += 1) {
|
|
pow10Cache[i] = pow10Cache[i - 1] * 10n;
|
|
}
|
|
|
|
return pow10Cache[exponent];
|
|
};
|
|
|
|
const normalizeDecimal = function (decimal) {
|
|
let digits = decimal.digits;
|
|
let scale = decimal.scale;
|
|
|
|
if (digits === 0n) {
|
|
return { sign: 1, digits: 0n, scale: 0 };
|
|
}
|
|
|
|
while (scale > 0 && digits % 10n === 0n) {
|
|
digits /= 10n;
|
|
scale -= 1;
|
|
}
|
|
|
|
return {
|
|
sign: decimal.sign < 0 ? -1 : 1,
|
|
digits,
|
|
scale
|
|
};
|
|
};
|
|
|
|
const decimalFromParts = function (sign, digits, scale) {
|
|
return normalizeDecimal({
|
|
sign: sign < 0 ? -1 : 1,
|
|
digits: digits < 0n ? -digits : digits,
|
|
scale: Math.max(0, scale | 0)
|
|
});
|
|
};
|
|
|
|
const decimalFromInteger = function (value) {
|
|
const bigintValue = typeof value === 'bigint' ? value : BigInt(value);
|
|
return decimalFromParts(bigintValue < 0n ? -1 : 1, bigintValue < 0n ? -bigintValue : bigintValue, 0);
|
|
};
|
|
|
|
const parseDecimalInput = function (value) {
|
|
if (typeof value !== 'string') {
|
|
return null;
|
|
}
|
|
|
|
const normalized = value
|
|
.trim()
|
|
.replace(/\s+/g, '')
|
|
.replace(/\./g, '')
|
|
.replace(',', '.');
|
|
|
|
if (normalized === '') {
|
|
return null;
|
|
}
|
|
|
|
const match = normalized.match(/^([+-])?(\d+)(?:\.(\d+))?$/);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
|
|
const sign = match[1] === '-' ? -1 : 1;
|
|
const integerPart = match[2];
|
|
const fractionalPart = match[3] || '';
|
|
return decimalFromParts(sign, BigInt(integerPart + fractionalPart), fractionalPart.length);
|
|
};
|
|
|
|
const parseDecimalConstant = function (value) {
|
|
const normalized = String(value).trim();
|
|
const match = normalized.match(/^([+-])?(\d+)(?:\.(\d+))?$/);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
|
|
const sign = match[1] === '-' ? -1 : 1;
|
|
const integerPart = match[2];
|
|
const fractionalPart = match[3] || '';
|
|
return decimalFromParts(sign, BigInt(integerPart + fractionalPart), fractionalPart.length);
|
|
};
|
|
|
|
const decimalToEnglishString = function (decimal) {
|
|
const normalized = normalizeDecimal(decimal);
|
|
const digitsText = normalized.digits.toString();
|
|
const signText = normalized.sign < 0 && normalized.digits !== 0n ? '-' : '';
|
|
|
|
if (normalized.scale === 0) {
|
|
return signText + digitsText;
|
|
}
|
|
|
|
const splitIndex = digitsText.length - normalized.scale;
|
|
if (splitIndex > 0) {
|
|
return signText + digitsText.slice(0, splitIndex) + '.' + digitsText.slice(splitIndex);
|
|
}
|
|
|
|
return signText + '0.' + '0'.repeat(-splitIndex) + digitsText;
|
|
};
|
|
|
|
const decimalToNumber = function (decimal) {
|
|
return Number(decimalToEnglishString(decimal));
|
|
};
|
|
|
|
const decimalFromNumber = function (value, fractionDigits) {
|
|
if (!Number.isFinite(value)) {
|
|
return null;
|
|
}
|
|
|
|
const digits = Math.max(0, Number(fractionDigits) || 0);
|
|
return parseDecimalConstant(Number(value).toFixed(digits));
|
|
};
|
|
|
|
const decimalIsZero = function (decimal) {
|
|
return normalizeDecimal(decimal).digits === 0n;
|
|
};
|
|
|
|
const decimalAdd = function (left, right) {
|
|
const scale = Math.max(left.scale, right.scale);
|
|
const leftDigits = BigInt(left.sign) * left.digits * getPow10(scale - left.scale);
|
|
const rightDigits = BigInt(right.sign) * right.digits * getPow10(scale - right.scale);
|
|
const result = leftDigits + rightDigits;
|
|
|
|
return decimalFromParts(result < 0n ? -1 : 1, result < 0n ? -result : result, scale);
|
|
};
|
|
|
|
const decimalSubtract = function (left, right) {
|
|
return decimalAdd(left, {
|
|
sign: right.sign * -1,
|
|
digits: right.digits,
|
|
scale: right.scale
|
|
});
|
|
};
|
|
|
|
const decimalMultiply = function (left, right) {
|
|
return decimalFromParts(left.sign * right.sign, left.digits * right.digits, left.scale + right.scale);
|
|
};
|
|
|
|
const decimalDivide = function (left, right, precision) {
|
|
if (decimalIsZero(right)) {
|
|
return null;
|
|
}
|
|
|
|
const digits = Math.max(0, precision | 0);
|
|
const numerator = left.digits * getPow10(digits + right.scale);
|
|
const denominator = right.digits * getPow10(left.scale);
|
|
const quotient = numerator / denominator;
|
|
|
|
return decimalFromParts(left.sign * right.sign, quotient, digits);
|
|
};
|
|
|
|
const decimalSquare = function (value) {
|
|
return decimalMultiply(value, value);
|
|
};
|
|
|
|
const decimalRoundToBigInt = function (decimal) {
|
|
const normalized = normalizeDecimal(decimal);
|
|
if (normalized.scale === 0) {
|
|
return BigInt(normalized.sign) * normalized.digits;
|
|
}
|
|
|
|
const divisor = getPow10(normalized.scale);
|
|
let integerPart = normalized.digits / divisor;
|
|
const remainder = normalized.digits % divisor;
|
|
|
|
if (remainder * 2n >= divisor) {
|
|
integerPart += 1n;
|
|
}
|
|
|
|
return BigInt(normalized.sign) * integerPart;
|
|
};
|
|
|
|
const formatDecimal = function (decimal, maxFractionDigits) {
|
|
if (!decimal) {
|
|
return '';
|
|
}
|
|
|
|
const normalized = normalizeDecimal(decimal);
|
|
const limit = Math.max(0, Number(maxFractionDigits) || 0);
|
|
const sign = normalized.sign < 0 && normalized.digits !== 0n ? '-' : '';
|
|
const scale = Math.min(normalized.scale, limit);
|
|
const truncatedDigits = normalized.scale > scale
|
|
? normalized.digits / getPow10(normalized.scale - scale)
|
|
: normalized.digits;
|
|
const digitsText = truncatedDigits.toString();
|
|
|
|
let integerPart = '';
|
|
let fractionalPart = '';
|
|
|
|
if (scale === 0) {
|
|
integerPart = digitsText;
|
|
} else {
|
|
const splitIndex = digitsText.length - scale;
|
|
if (splitIndex > 0) {
|
|
integerPart = digitsText.slice(0, splitIndex);
|
|
fractionalPart = digitsText.slice(splitIndex);
|
|
} else {
|
|
integerPart = '0';
|
|
fractionalPart = '0'.repeat(-splitIndex) + digitsText;
|
|
}
|
|
}
|
|
|
|
integerPart = integerPart.replace(/^0+(?=\d)/, '') || '0';
|
|
integerPart = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
|
fractionalPart = fractionalPart.replace(/0+$/, '');
|
|
|
|
return fractionalPart !== ''
|
|
? `${sign}${integerPart},${fractionalPart}`
|
|
: `${sign}${integerPart}`;
|
|
};
|
|
|
|
const convertDistanceDecimal = function (value, sourceUnit, targetUnit) {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
|
|
if (sourceUnit === targetUnit) {
|
|
return value;
|
|
}
|
|
|
|
if (sourceUnit === 'mpc' && targetUnit === 'pc') {
|
|
return decimalMultiply(value, decimalConstants.million);
|
|
}
|
|
|
|
if (sourceUnit === 'pc' && targetUnit === 'mpc') {
|
|
return decimalDivide(value, decimalConstants.million, DECIMAL_DIVISION_PRECISION);
|
|
}
|
|
|
|
const decimalUnitToKm = {
|
|
km: decimalFromInteger(1n),
|
|
au: decimalConstants.kmPerAu,
|
|
lm: decimalConstants.kmPerLightMinute,
|
|
lh: decimalConstants.kmPerLightHour,
|
|
ly: decimalConstants.kmPerLy,
|
|
pc: decimalConstants.kmPerPc,
|
|
mpc: decimalConstants.kmPerMpc
|
|
};
|
|
|
|
const valueInKm = decimalMultiply(value, decimalUnitToKm[sourceUnit]);
|
|
return decimalDivide(valueInKm, decimalUnitToKm[targetUnit], DECIMAL_DIVISION_PRECISION);
|
|
};
|
|
|
|
const formatValue = function (value) {
|
|
if (!value) {
|
|
return '';
|
|
}
|
|
|
|
return formatDecimal(value, 10);
|
|
};
|
|
|
|
const formatJulianDateValue = function (value) {
|
|
const formatted = formatValue(value);
|
|
if (formatted === '') {
|
|
return '';
|
|
}
|
|
|
|
return formatted.includes(',') ? formatted : `${formatted},0`;
|
|
};
|
|
|
|
const cosmologyE = function (z) {
|
|
return Math.sqrt(OMEGA_M * Math.pow(1 + z, 3) + OMEGA_LAMBDA);
|
|
};
|
|
|
|
const adaptiveSimpson = function (fn, start, end, epsilon, maxDepth) {
|
|
const simpson = function (left, right, leftValue, middleValue, rightValue) {
|
|
return (right - left) * (leftValue + (4 * middleValue) + rightValue) / 6;
|
|
};
|
|
|
|
const recurse = function (left, right, eps, whole, leftValue, middleValue, rightValue, depth) {
|
|
const middle = (left + right) / 2;
|
|
const leftMiddle = (left + middle) / 2;
|
|
const rightMiddle = (middle + right) / 2;
|
|
const leftMiddleValue = fn(leftMiddle);
|
|
const rightMiddleValue = fn(rightMiddle);
|
|
const leftArea = simpson(left, middle, leftValue, leftMiddleValue, middleValue);
|
|
const rightArea = simpson(middle, right, middleValue, rightMiddleValue, rightValue);
|
|
const delta = leftArea + rightArea - whole;
|
|
|
|
if (depth <= 0 || Math.abs(delta) <= 15 * eps) {
|
|
return leftArea + rightArea + (delta / 15);
|
|
}
|
|
|
|
return recurse(left, middle, eps / 2, leftArea, leftValue, leftMiddleValue, middleValue, depth - 1)
|
|
+ recurse(middle, right, eps / 2, rightArea, middleValue, rightMiddleValue, rightValue, depth - 1);
|
|
};
|
|
|
|
const leftValue = fn(start);
|
|
const rightValue = fn(end);
|
|
const middle = (start + end) / 2;
|
|
const middleValue = fn(middle);
|
|
const whole = simpson(start, end, leftValue, middleValue, rightValue);
|
|
|
|
if (!Number.isFinite(whole)) {
|
|
return Number.NaN;
|
|
}
|
|
|
|
return recurse(start, end, epsilon, whole, leftValue, middleValue, rightValue, maxDepth);
|
|
};
|
|
|
|
decimalConstants.zero = decimalFromInteger(0n);
|
|
decimalConstants.one = decimalFromInteger(1n);
|
|
decimalConstants.hundred = decimalFromInteger(100n);
|
|
decimalConstants.thousand = decimalFromInteger(1000n);
|
|
decimalConstants.million = decimalFromInteger(1000000n);
|
|
decimalConstants.speedOfLightKmS = parseDecimalConstant('299792.458');
|
|
decimalConstants.secondsPerGyr = parseDecimalConstant('31557600000000000');
|
|
decimalConstants.mpcInKm = parseDecimalConstant('30856775814913673000');
|
|
decimalConstants.kmPerAu = parseDecimalConstant('149597870.7');
|
|
decimalConstants.kmPerLightMinute = parseDecimalConstant('17987547.48');
|
|
decimalConstants.kmPerLightHour = parseDecimalConstant('1079252848.8');
|
|
decimalConstants.kmPerLy = parseDecimalConstant('9460730472580.8');
|
|
decimalConstants.kmPerPc = parseDecimalConstant('30856775814913.67');
|
|
decimalConstants.kmPerMpc = parseDecimalConstant('30856775814913670000');
|
|
decimalConstants.jdUnixEpoch = parseDecimalConstant('2440587.5');
|
|
decimalConstants.mjdOffset = parseDecimalConstant('2400000.5');
|
|
decimalConstants.msPerDay = decimalFromInteger(86400000n);
|
|
decimalConstants.msPerSecond = decimalFromInteger(1000n);
|
|
|
|
const updateTooltip = function (field) {
|
|
const wrapper = field.closest('.astro-conv-field');
|
|
const tooltipId = field.getAttribute('data-tooltip-id');
|
|
const tooltip = tooltipId ? document.getElementById(tooltipId) : null;
|
|
const rawValue = String(field.value || '').trim();
|
|
|
|
if (!wrapper || !tooltip || rawValue === '') {
|
|
if (wrapper) {
|
|
wrapper.classList.remove('is-tooltip-open');
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (typeof window.numberToGermanText !== 'function') {
|
|
return;
|
|
}
|
|
|
|
tooltip.textContent = window.numberToGermanText(rawValue);
|
|
};
|
|
|
|
const attachTooltip = function (field, key) {
|
|
const wrapper = field.closest('.astro-conv-field');
|
|
if (!wrapper) {
|
|
return;
|
|
}
|
|
|
|
const tooltip = document.createElement('div');
|
|
tooltip.className = 'astro-conv-tooltip';
|
|
tooltip.id = 'tooltip-' + key;
|
|
wrapper.appendChild(tooltip);
|
|
field.setAttribute('data-tooltip-id', tooltip.id);
|
|
};
|
|
|
|
const wireTooltipHover = function (field) {
|
|
field.addEventListener('mouseenter', function () {
|
|
updateTooltip(field);
|
|
if (field.value.trim() === '') {
|
|
return;
|
|
}
|
|
|
|
const wrapper = field.closest('.astro-conv-field');
|
|
if (wrapper) {
|
|
wrapper.classList.add('is-tooltip-open');
|
|
}
|
|
});
|
|
|
|
field.addEventListener('mouseleave', function () {
|
|
const wrapper = field.closest('.astro-conv-field');
|
|
if (wrapper) {
|
|
wrapper.classList.remove('is-tooltip-open');
|
|
}
|
|
});
|
|
};
|
|
|
|
const distanceFieldEntries = Object.entries(distanceFields);
|
|
distanceFieldEntries.forEach(function ([unit, field]) {
|
|
attachTooltip(field, 'distance-' + unit);
|
|
wireTooltipHover(field);
|
|
});
|
|
|
|
const redshiftFieldEntries = Object.entries(redshiftFields);
|
|
redshiftFieldEntries.forEach(function ([unit, field]) {
|
|
attachTooltip(field, 'redshift-' + unit);
|
|
wireTooltipHover(field);
|
|
});
|
|
|
|
const timeFieldEntries = Object.entries(timeFields);
|
|
timeFieldEntries.forEach(function ([unit, field]) {
|
|
attachTooltip(field, 'time-' + unit);
|
|
wireTooltipHover(field);
|
|
});
|
|
|
|
const updateDistanceFrom = async function (sourceUnit) {
|
|
const rawValue = distanceFields[sourceUnit].value.trim();
|
|
if (rawValue === '') {
|
|
Object.values(distanceFields).forEach(function (field, index) {
|
|
if (Object.keys(distanceFields)[index] !== sourceUnit) {
|
|
field.value = '';
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
const requestId = ++distanceRequestId;
|
|
try {
|
|
const result = await requestConversion({
|
|
kind: 'distance',
|
|
sourceUnit,
|
|
sourceValue: rawValue
|
|
});
|
|
|
|
if (requestId !== distanceRequestId || !result.valid) {
|
|
return;
|
|
}
|
|
|
|
setFieldValues(distanceFields, result.fields || {});
|
|
} catch (error) {
|
|
return;
|
|
}
|
|
};
|
|
|
|
distanceFieldEntries.forEach(function ([unit, field]) {
|
|
field.addEventListener('input', function () {
|
|
updateDistanceFrom(unit);
|
|
});
|
|
});
|
|
|
|
document.getElementById('distanceClear').addEventListener('click', function () {
|
|
Object.values(distanceFields).forEach(function (field) {
|
|
field.value = '';
|
|
});
|
|
distanceFields.km.focus();
|
|
});
|
|
|
|
document.getElementById('distanceMoon').addEventListener('click', function () {
|
|
distanceFields.km.value = '384400';
|
|
updateDistanceFrom('km');
|
|
});
|
|
|
|
document.getElementById('distanceSun').addEventListener('click', function () {
|
|
distanceFields.au.value = '1';
|
|
updateDistanceFrom('au');
|
|
});
|
|
|
|
const updateRedshift = async function () {
|
|
const zValue = redshiftFields.z.value.trim();
|
|
const restNmValue = redshiftFields.restNm.value.trim();
|
|
if (zValue === '' || restNmValue === '') {
|
|
return;
|
|
}
|
|
|
|
const requestId = ++redshiftRequestId;
|
|
try {
|
|
const result = await requestConversion({
|
|
kind: 'redshift',
|
|
z: zValue,
|
|
restNm: restNmValue
|
|
});
|
|
|
|
if (requestId !== redshiftRequestId || !result.valid) {
|
|
return;
|
|
}
|
|
|
|
setFieldValues(redshiftFields, result.fields || {});
|
|
} catch (error) {
|
|
return;
|
|
}
|
|
};
|
|
|
|
['z', 'restNm'].forEach(function (key) {
|
|
redshiftFields[key].addEventListener('input', updateRedshift);
|
|
});
|
|
|
|
document.getElementById('redshiftClear').addEventListener('click', function () {
|
|
Object.values(redshiftFields).forEach(function (field) {
|
|
field.value = '';
|
|
});
|
|
redshiftFields.z.focus();
|
|
});
|
|
|
|
document.getElementById('redshiftGalaxy').addEventListener('click', function () {
|
|
redshiftFields.z.value = '0,1';
|
|
redshiftFields.restNm.value = '656,3';
|
|
updateRedshift();
|
|
});
|
|
|
|
document.getElementById('redshiftQuasar').addEventListener('click', function () {
|
|
redshiftFields.z.value = '6';
|
|
redshiftFields.restNm.value = '121,567';
|
|
updateRedshift();
|
|
});
|
|
|
|
const parseUtcDateTime = function (value) {
|
|
const match = String(value).trim().match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d{1,3}))?)?)?$/);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
|
|
const year = Number(match[1]);
|
|
const month = Number(match[2]);
|
|
const day = Number(match[3]);
|
|
const hour = Number(match[4] || '0');
|
|
const minute = Number(match[5] || '0');
|
|
const second = Number(match[6] || '0');
|
|
const millisecond = Number((match[7] || '0').padEnd(3, '0'));
|
|
const date = new Date(Date.UTC(year, month - 1, day, hour, minute, second, millisecond));
|
|
|
|
return Number.isNaN(date.getTime()) ? null : date;
|
|
};
|
|
|
|
const formatUtcDateTime = function (date) {
|
|
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
|
return '';
|
|
}
|
|
|
|
const parts = [
|
|
date.getUTCFullYear().toString().padStart(4, '0'),
|
|
(date.getUTCMonth() + 1).toString().padStart(2, '0'),
|
|
date.getUTCDate().toString().padStart(2, '0')
|
|
];
|
|
|
|
const timeParts = [
|
|
date.getUTCHours().toString().padStart(2, '0'),
|
|
date.getUTCMinutes().toString().padStart(2, '0'),
|
|
date.getUTCSeconds().toString().padStart(2, '0')
|
|
];
|
|
|
|
return parts.join('-') + ' ' + timeParts.join(':') + '.' + date.getUTCMilliseconds().toString().padStart(3, '0');
|
|
};
|
|
|
|
const formatUtcDateTimeGerman = function (date) {
|
|
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
|
return '';
|
|
}
|
|
|
|
const parts = [
|
|
date.getUTCDate().toString().padStart(2, '0'),
|
|
(date.getUTCMonth() + 1).toString().padStart(2, '0'),
|
|
date.getUTCFullYear().toString().padStart(4, '0')
|
|
];
|
|
|
|
const timeParts = [
|
|
date.getUTCHours().toString().padStart(2, '0'),
|
|
date.getUTCMinutes().toString().padStart(2, '0'),
|
|
date.getUTCSeconds().toString().padStart(2, '0')
|
|
];
|
|
|
|
return parts.join('.') + ' ' + timeParts.join(':') + ',' + date.getUTCMilliseconds().toString().padStart(3, '0');
|
|
};
|
|
|
|
const parseUtcDateTimeGerman = function (value) {
|
|
const match = String(value).trim().match(/^(\d{2})\.(\d{2})\.(\d{4})(?:[ ](\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d{1,3}))?)?)?$/);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
|
|
const day = Number(match[1]);
|
|
const month = Number(match[2]);
|
|
const year = Number(match[3]);
|
|
const hour = Number(match[4] || '0');
|
|
const minute = Number(match[5] || '0');
|
|
const second = Number(match[6] || '0');
|
|
const millisecond = Number((match[7] || '0').padEnd(3, '0'));
|
|
const date = new Date(Date.UTC(year, month - 1, day, hour, minute, second, millisecond));
|
|
|
|
return Number.isNaN(date.getTime()) ? null : date;
|
|
};
|
|
|
|
const syncTimePickerFromDate = function (date) {
|
|
if (!timeUtcPicker) {
|
|
return;
|
|
}
|
|
|
|
isSyncingTimePicker = true;
|
|
timeUtcPicker.setDate(date, false);
|
|
if (timeUtcPicker.altInput) {
|
|
timeUtcPicker.altInput.value = formatUtcDateTimeGerman(date);
|
|
}
|
|
isSyncingTimePicker = false;
|
|
};
|
|
|
|
if (window.flatpickr) {
|
|
timeUtcPicker = window.flatpickr(timeFields.utc, {
|
|
enableTime: true,
|
|
enableSeconds: true,
|
|
time_24hr: true,
|
|
allowInput: true,
|
|
altInput: true,
|
|
altFormat: 'd.m.Y H:i:S',
|
|
dateFormat: 'Y-m-d H:i:S',
|
|
locale: window.flatpickr.l10ns.de,
|
|
defaultDate: parseUtcDateTime(timeFields.utc.value),
|
|
parseDate: function (value, format) {
|
|
if (format === 'd.m.Y H:i:S') {
|
|
return parseUtcDateTimeGerman(value);
|
|
}
|
|
|
|
return parseUtcDateTime(value);
|
|
},
|
|
formatDate: function (date, format) {
|
|
if (format === 'd.m.Y H:i:S') {
|
|
return formatUtcDateTimeGerman(date);
|
|
}
|
|
|
|
return formatUtcDateTime(date);
|
|
},
|
|
onValueUpdate: function (selectedDates, dateStr, instance) {
|
|
if (isSyncingTimePicker) {
|
|
return;
|
|
}
|
|
|
|
timeFields.utc.value = dateStr;
|
|
updateTimeFrom('utc');
|
|
|
|
if (instance.altInput) {
|
|
instance.altInput.setAttribute('placeholder', 'TT.MM.JJJJ HH:MM:SS,mmm');
|
|
}
|
|
}
|
|
});
|
|
|
|
if (timeUtcPicker.altInput) {
|
|
timeUtcPicker.altInput.classList.add('astro-conv-input');
|
|
timeUtcPicker.altInput.setAttribute('placeholder', 'TT.MM.JJJJ HH:MM:SS,mmm');
|
|
}
|
|
}
|
|
|
|
const dateToJulianDateDecimal = function (date) {
|
|
const unixMsDecimal = decimalFromInteger(BigInt(date.getTime()));
|
|
return decimalAdd(
|
|
decimalDivide(unixMsDecimal, decimalConstants.msPerDay, DECIMAL_DIVISION_PRECISION),
|
|
decimalConstants.jdUnixEpoch
|
|
);
|
|
};
|
|
|
|
const normalizeDegrees = function (degrees) {
|
|
let normalized = degrees % 360;
|
|
if (normalized < 0) {
|
|
normalized += 360;
|
|
}
|
|
|
|
return normalized;
|
|
};
|
|
|
|
const formatHoursToClock = function (hoursValue) {
|
|
const normalizedHours = ((hoursValue % 24) + 24) % 24;
|
|
const totalSeconds = Math.round(normalizedHours * 3600);
|
|
const hours = Math.floor(totalSeconds / 3600) % 24;
|
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
const seconds = totalSeconds % 60;
|
|
|
|
return [
|
|
hours.toString().padStart(2, '0'),
|
|
minutes.toString().padStart(2, '0'),
|
|
seconds.toString().padStart(2, '0')
|
|
].join(':');
|
|
};
|
|
|
|
const getEquationOfTimeMinutes = function (date) {
|
|
const startOfYearUtc = Date.UTC(date.getUTCFullYear(), 0, 1);
|
|
const dayOfYear = Math.floor((date.getTime() - startOfYearUtc) / 86400000) + 1;
|
|
const fractionalHour = date.getUTCHours() + (date.getUTCMinutes() / 60) + (date.getUTCSeconds() / 3600);
|
|
const gamma = (2 * Math.PI / 365) * (dayOfYear - 1 + ((fractionalHour - 12) / 24));
|
|
|
|
return 229.18 * (
|
|
0.000075
|
|
+ 0.001868 * Math.cos(gamma)
|
|
- 0.032077 * Math.sin(gamma)
|
|
- 0.014615 * Math.cos(2 * gamma)
|
|
- 0.040849 * Math.sin(2 * gamma)
|
|
);
|
|
};
|
|
|
|
const updateTrueSolarTime = function (date) {
|
|
if (!timeFields.trueSolar) {
|
|
return;
|
|
}
|
|
|
|
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
|
timeFields.trueSolar.value = '';
|
|
return;
|
|
}
|
|
|
|
if (!Number.isFinite(Number(USER_TIME_LOCATION.longitude))) {
|
|
timeFields.trueSolar.value = 'Kein Standard-Standort verfuegbar';
|
|
return;
|
|
}
|
|
|
|
const trueSolarOffsetMinutes = (Number(USER_TIME_LOCATION.longitude) * 4) + getEquationOfTimeMinutes(date);
|
|
const trueSolarDate = new Date(date.getTime() + Math.round(trueSolarOffsetMinutes * 60000));
|
|
timeFields.trueSolar.value = formatUtcDateTimeGerman(trueSolarDate);
|
|
};
|
|
|
|
const updateSiderealTime = function (jdDecimal) {
|
|
if (!timeFields.sidereal) {
|
|
return;
|
|
}
|
|
|
|
if (!jdDecimal) {
|
|
timeFields.sidereal.value = '';
|
|
return;
|
|
}
|
|
|
|
if (!Number.isFinite(Number(USER_TIME_LOCATION.longitude))) {
|
|
timeFields.sidereal.value = 'Kein Standard-Standort verfuegbar';
|
|
return;
|
|
}
|
|
|
|
const jd = decimalToNumber(jdDecimal);
|
|
const t = (jd - 2451545.0) / 36525;
|
|
const gmstDegrees = normalizeDegrees(
|
|
280.46061837
|
|
+ (360.98564736629 * (jd - 2451545.0))
|
|
+ (0.000387933 * t * t)
|
|
- ((t * t * t) / 38710000)
|
|
);
|
|
const lstDegrees = normalizeDegrees(gmstDegrees + Number(USER_TIME_LOCATION.longitude));
|
|
timeFields.sidereal.value = formatHoursToClock(lstDegrees / 15);
|
|
};
|
|
|
|
const julianDateDecimalToDate = function (jdDecimal) {
|
|
const unixMsDecimal = decimalMultiply(
|
|
decimalSubtract(jdDecimal, decimalConstants.jdUnixEpoch),
|
|
decimalConstants.msPerDay
|
|
);
|
|
const unixMs = decimalRoundToBigInt(unixMsDecimal);
|
|
return new Date(Number(unixMs));
|
|
};
|
|
|
|
const updateTimeFrom = async function (sourceUnit) {
|
|
const sourceField = timeFields[sourceUnit];
|
|
if (!sourceField) {
|
|
return;
|
|
}
|
|
|
|
const sourceValue = sourceField.value.trim();
|
|
if (sourceValue === '') {
|
|
return;
|
|
}
|
|
|
|
const requestId = ++timeRequestId;
|
|
try {
|
|
const result = await requestConversion({
|
|
kind: 'time',
|
|
sourceUnit,
|
|
sourceValue,
|
|
longitude: USER_TIME_LOCATION.longitude
|
|
});
|
|
|
|
if (requestId !== timeRequestId || !result.valid) {
|
|
return;
|
|
}
|
|
|
|
setFieldValues(timeFields, result.fields || {});
|
|
const utcDate = parseUtcDateTime(timeFields.utc.value);
|
|
if (utcDate) {
|
|
syncTimePickerFromDate(utcDate);
|
|
}
|
|
} catch (error) {
|
|
return;
|
|
}
|
|
};
|
|
|
|
const setFieldValues = function (fieldsMap, values) {
|
|
Object.entries(fieldsMap).forEach(function ([key, field]) {
|
|
if (!field) {
|
|
return;
|
|
}
|
|
|
|
field.value = typeof values[key] === 'string' ? values[key] : '';
|
|
updateTooltip(field);
|
|
});
|
|
};
|
|
|
|
const requestConversion = async function (payload) {
|
|
const formData = new FormData();
|
|
formData.set('ajax', '1');
|
|
|
|
Object.entries(payload).forEach(function ([key, value]) {
|
|
formData.set(key, value == null ? '' : String(value));
|
|
});
|
|
|
|
const response = await fetch(CONVERSIONS_ENDPOINT, {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: { 'Accept': 'application/json' }
|
|
});
|
|
|
|
const json = await response.json();
|
|
if (!response.ok || !json.ok) {
|
|
throw new Error(json.error || 'Umrechnung fehlgeschlagen.');
|
|
}
|
|
|
|
return json;
|
|
};
|
|
|
|
timeFields.utc.addEventListener('input', function () {
|
|
updateTimeFrom('utc');
|
|
});
|
|
['jd', 'mjd', 'unix'].forEach(function (key) {
|
|
timeFields[key].addEventListener('input', function () {
|
|
updateTimeFrom(key);
|
|
});
|
|
});
|
|
|
|
document.getElementById('timeClear').addEventListener('click', function () {
|
|
Object.values(timeFields).forEach(function (field) {
|
|
if (field !== timeFields.trueSolar && field !== timeFields.sidereal) {
|
|
field.value = '';
|
|
}
|
|
});
|
|
if (timeUtcPicker) {
|
|
isSyncingTimePicker = true;
|
|
timeUtcPicker.clear(false);
|
|
isSyncingTimePicker = false;
|
|
}
|
|
timeFields.trueSolar.value = Number.isFinite(Number(USER_TIME_LOCATION.longitude))
|
|
? ''
|
|
: 'Kein Standard-Standort verfuegbar';
|
|
timeFields.sidereal.value = Number.isFinite(Number(USER_TIME_LOCATION.longitude))
|
|
? ''
|
|
: 'Kein Standard-Standort verfuegbar';
|
|
if (timeUtcPicker && timeUtcPicker.altInput) {
|
|
timeUtcPicker.altInput.focus();
|
|
return;
|
|
}
|
|
timeFields.utc.focus();
|
|
});
|
|
|
|
document.getElementById('timeNow').addEventListener('click', function () {
|
|
timeFields.utc.value = formatUtcDateTime(new Date());
|
|
updateTimeFrom('utc');
|
|
});
|
|
|
|
document.getElementById('timeJ2000').addEventListener('click', function () {
|
|
timeFields.utc.value = '2000-01-01 12:00:00';
|
|
updateTimeFrom('utc');
|
|
});
|
|
|
|
const tabs = Array.from(document.querySelectorAll('.astro-conv-tab'));
|
|
const tabPanels = Array.from(document.querySelectorAll('.astro-conv-tab-panel'));
|
|
|
|
const activateTab = function (targetId) {
|
|
tabs.forEach(function (tab) {
|
|
const isActive = tab.getAttribute('data-tab-target') === targetId;
|
|
tab.classList.toggle('is-active', isActive);
|
|
tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
|
});
|
|
|
|
tabPanels.forEach(function (panel) {
|
|
panel.classList.toggle('is-active', panel.id === targetId);
|
|
});
|
|
};
|
|
|
|
tabs.forEach(function (tab) {
|
|
tab.addEventListener('click', function () {
|
|
activateTab(tab.getAttribute('data-tab-target'));
|
|
});
|
|
});
|
|
|
|
updateDistanceFrom('km');
|
|
updateRedshift();
|
|
updateTimeFrom('utc');
|
|
})();
|
|
</script>
|
|
|
|
<?php require __DIR__ . '/footer.php'; ?>
|