Files
skyview.astronomiemuseum.de/public/astronomical_conversions.php
T

1392 lines
50 KiB
PHP

<?php
declare(strict_types=1);
ini_set('display_errors', '1');
error_reporting(E_ALL);
session_start();
$loggedIn = isset($_SESSION['user_id']);
$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.75rem;
}
.astro-conv-tab {
padding: 0.75rem 1rem;
border-radius: 999px;
border: 1px solid rgba(130, 170, 255, 0.18);
background: rgba(255,255,255,0.04);
color: #dce8ff;
cursor: pointer;
}
.astro-conv-tab.is-active {
background: linear-gradient(180deg, rgba(82, 153, 255, 0.28), rgba(49, 112, 255, 0.18));
border-color: rgba(130, 170, 255, 0.4);
color: #ffffff;
}
.astro-conv-panel {
padding: 1.2rem;
border-radius: 1rem;
background:
radial-gradient(circle at top left, rgba(82, 153, 255, 0.12), transparent 36%),
linear-gradient(180deg, rgba(13, 19, 33, 0.96), rgba(8, 11, 20, 0.98));
border: 1px solid rgba(130, 170, 255, 0.16);
}
.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: #c7d6f4;
font-size: 0.94rem;
}
.astro-conv-label-help {
border-bottom: 1px dotted rgba(199, 214, 244, 0.45);
cursor: help;
}
.astro-conv-input {
width: 100%;
padding: 0.72rem 0.8rem;
border-radius: 0.8rem;
border: 1px solid rgba(255,255,255,0.12);
background: rgba(255,255,255,0.05);
color: #f8fbff;
font-size: 0.96rem;
}
.astro-conv-input:focus {
outline: none;
border-color: rgba(130, 170, 255, 0.6);
box-shadow: 0 0 0 3px rgba(82, 153, 255, 0.16);
}
.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(10, 15, 27, 0.96);
border: 1px solid rgba(130, 170, 255, 0.2);
color: #eef4ff;
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;
}
@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}))?)?$/);
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 date = new Date(Date.UTC(year, month - 1, day, hour, minute, second));
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(':');
};
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(':');
};
const parseUtcDateTimeGerman = function (value) {
const match = String(value).trim().match(/^(\d{2})\.(\d{2})\.(\d{4})(?:[ ](\d{2}):(\d{2})(?::(\d{2}))?)?$/);
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 date = new Date(Date.UTC(year, month - 1, day, hour, minute, second));
return Number.isNaN(date.getTime()) ? null : date;
};
const syncTimePickerFromDate = function (date) {
if (!timeUtcPicker) {
return;
}
isSyncingTimePicker = true;
timeUtcPicker.setDate(date, false);
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');
}
}
});
if (timeUtcPicker.altInput) {
timeUtcPicker.altInput.classList.add('astro-conv-input');
timeUtcPicker.altInput.setAttribute('placeholder', 'TT.MM.JJJJ HH:MM:SS');
}
}
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'; ?>