1606 lines
59 KiB
PHP
1606 lines
59 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
ini_set('display_errors', '1');
|
||
error_reporting(E_ALL);
|
||
session_start();
|
||
require_once __DIR__ . '/python_api.php';
|
||
|
||
$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');
|
||
}
|
||
|
||
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'] ?? '')),
|
||
]);
|
||
} elseif ($kind === 'coordinates') {
|
||
$pythonResult = runPythonApi('astronomical_conversions', [
|
||
'coordinates',
|
||
trim((string) ($_POST['sourceSystem'] ?? '')),
|
||
trim((string) ($_POST['value1'] ?? '')),
|
||
trim((string) ($_POST['value2'] ?? '')),
|
||
]);
|
||
} elseif ($kind === 'angle') {
|
||
$pythonResult = runPythonApi('astronomical_conversions', [
|
||
'angle',
|
||
trim((string) ($_POST['sourceUnit'] ?? '')),
|
||
trim((string) ($_POST['sourceValue'] ?? '')),
|
||
]);
|
||
} 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>
|
||
<button type="button" class="astro-conv-tab" data-tab-target="coordinatesTab" role="tab" aria-selected="false">Koordinaten</button>
|
||
<button type="button" class="astro-conv-tab" data-tab-target="angleTab" role="tab" aria-selected="false">Winkel</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>
|
||
|
||
<section class="card astro-conv-panel astro-conv-tab-panel" id="coordinatesTab" role="tabpanel">
|
||
<h2>Koordinaten-Umrechner</h2>
|
||
<p>Umrechnung zwischen äquatorialen und galaktischen Koordinaten auf Basis des J2000-Systems. Eingabe und Ausgabe erfolgen in Dezimalgrad.</p>
|
||
|
||
<div class="astro-conv-grid" id="coordinatesConverter">
|
||
<div class="astro-conv-field">
|
||
<label for="coordRa"><span class="astro-conv-label-help" title="Rektaszension α im äquatorialen System, in Dezimalgrad von 0° bis 360°.">Rektaszension α (°)</span></label>
|
||
<input class="astro-conv-input" id="coordRa" type="text" inputmode="decimal" value="279,234735">
|
||
</div>
|
||
|
||
<div class="astro-conv-field">
|
||
<label for="coordDec"><span class="astro-conv-label-help" title="Deklination δ im äquatorialen System, in Dezimalgrad von -90° bis +90°.">Deklination δ (°)</span></label>
|
||
<input class="astro-conv-input" id="coordDec" type="text" inputmode="decimal" value="38,783689">
|
||
</div>
|
||
|
||
<div class="astro-conv-field">
|
||
<label for="coordGalLon"><span class="astro-conv-label-help" title="Galaktische Länge l in Dezimalgrad von 0° bis 360°.">Galaktische Länge l (°)</span></label>
|
||
<input class="astro-conv-input" id="coordGalLon" type="text" inputmode="decimal">
|
||
</div>
|
||
|
||
<div class="astro-conv-field">
|
||
<label for="coordGalLat"><span class="astro-conv-label-help" title="Galaktische Breite b in Dezimalgrad von -90° bis +90°.">Galaktische Breite b (°)</span></label>
|
||
<input class="astro-conv-input" id="coordGalLat" type="text" inputmode="decimal">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="astro-conv-actions">
|
||
<button type="button" class="btn btn-secondary" id="coordinatesClear">Leeren</button>
|
||
<button type="button" class="btn btn-primary" id="coordinatesVega">Vega</button>
|
||
<button type="button" class="btn btn-primary" id="coordinatesGalacticCenter">Galaktisches Zentrum</button>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="card astro-conv-panel astro-conv-tab-panel" id="angleTab" role="tabpanel">
|
||
<h2>Winkel-Umrechner</h2>
|
||
<p>Umrechnung zwischen Grad, Bogenminute, Bogensekunde und Radiant.</p>
|
||
|
||
<div class="astro-conv-grid" id="angleConverter">
|
||
<div class="astro-conv-field">
|
||
<label for="angleDeg">Grad (°)</label>
|
||
<input class="astro-conv-input" id="angleDeg" type="text" inputmode="decimal" value="1">
|
||
</div>
|
||
|
||
<div class="astro-conv-field">
|
||
<label for="angleArcmin">Bogenminute (′)</label>
|
||
<input class="astro-conv-input" id="angleArcmin" type="text" inputmode="decimal">
|
||
</div>
|
||
|
||
<div class="astro-conv-field">
|
||
<label for="angleArcsec">Bogensekunde (″)</label>
|
||
<input class="astro-conv-input" id="angleArcsec" type="text" inputmode="decimal">
|
||
</div>
|
||
|
||
<div class="astro-conv-field">
|
||
<label for="angleRad">Radiant (rad)</label>
|
||
<input class="astro-conv-input" id="angleRad" type="text" inputmode="decimal">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="astro-conv-actions">
|
||
<button type="button" class="btn btn-secondary" id="angleClear">Leeren</button>
|
||
<button type="button" class="btn btn-primary" id="angleDegree">1 Grad</button>
|
||
<button type="button" class="btn btn-primary" id="angleArcsecond">1 Bogensekunde</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')
|
||
};
|
||
const coordinateFields = {
|
||
ra: document.getElementById('coordRa'),
|
||
dec: document.getElementById('coordDec'),
|
||
galLon: document.getElementById('coordGalLon'),
|
||
galLat: document.getElementById('coordGalLat')
|
||
};
|
||
const angleFields = {
|
||
deg: document.getElementById('angleDeg'),
|
||
arcmin: document.getElementById('angleArcmin'),
|
||
arcsec: document.getElementById('angleArcsec'),
|
||
rad: document.getElementById('angleRad')
|
||
};
|
||
let isSyncingTimePicker = false;
|
||
let timeUtcPicker = null;
|
||
let distanceRequestId = 0;
|
||
let redshiftRequestId = 0;
|
||
let timeRequestId = 0;
|
||
let coordinateRequestId = 0;
|
||
let angleRequestId = 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 coordinateFieldEntries = Object.entries(coordinateFields);
|
||
coordinateFieldEntries.forEach(function ([unit, field]) {
|
||
attachTooltip(field, 'coordinates-' + unit);
|
||
wireTooltipHover(field);
|
||
});
|
||
|
||
const angleFieldEntries = Object.entries(angleFields);
|
||
angleFieldEntries.forEach(function ([unit, field]) {
|
||
attachTooltip(field, 'angle-' + 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 clearCoordinateGroup = function (keys) {
|
||
keys.forEach(function (key) {
|
||
if (coordinateFields[key]) {
|
||
coordinateFields[key].value = '';
|
||
updateTooltip(coordinateFields[key]);
|
||
}
|
||
});
|
||
};
|
||
|
||
const updateCoordinatesFrom = async function (sourceSystem) {
|
||
const isEquatorial = sourceSystem === 'equatorial';
|
||
const firstKey = isEquatorial ? 'ra' : 'galLon';
|
||
const secondKey = isEquatorial ? 'dec' : 'galLat';
|
||
const targetKeys = isEquatorial ? ['galLon', 'galLat'] : ['ra', 'dec'];
|
||
const firstValue = coordinateFields[firstKey].value.trim();
|
||
const secondValue = coordinateFields[secondKey].value.trim();
|
||
|
||
if (firstValue === '' || secondValue === '') {
|
||
clearCoordinateGroup(targetKeys);
|
||
return;
|
||
}
|
||
|
||
const requestId = ++coordinateRequestId;
|
||
try {
|
||
const result = await requestConversion({
|
||
kind: 'coordinates',
|
||
sourceSystem,
|
||
value1: firstValue,
|
||
value2: secondValue
|
||
});
|
||
|
||
if (requestId !== coordinateRequestId || !result.valid) {
|
||
return;
|
||
}
|
||
|
||
setFieldValues(coordinateFields, result.fields || {});
|
||
} catch (error) {
|
||
return;
|
||
}
|
||
};
|
||
|
||
['ra', 'dec'].forEach(function (key) {
|
||
coordinateFields[key].addEventListener('input', function () {
|
||
updateCoordinatesFrom('equatorial');
|
||
});
|
||
});
|
||
|
||
['galLon', 'galLat'].forEach(function (key) {
|
||
coordinateFields[key].addEventListener('input', function () {
|
||
updateCoordinatesFrom('galactic');
|
||
});
|
||
});
|
||
|
||
document.getElementById('coordinatesClear').addEventListener('click', function () {
|
||
Object.values(coordinateFields).forEach(function (field) {
|
||
field.value = '';
|
||
updateTooltip(field);
|
||
});
|
||
coordinateFields.ra.focus();
|
||
});
|
||
|
||
document.getElementById('coordinatesVega').addEventListener('click', function () {
|
||
coordinateFields.ra.value = '279,234735';
|
||
coordinateFields.dec.value = '38,783689';
|
||
updateCoordinatesFrom('equatorial');
|
||
});
|
||
|
||
document.getElementById('coordinatesGalacticCenter').addEventListener('click', function () {
|
||
coordinateFields.galLon.value = '0';
|
||
coordinateFields.galLat.value = '0';
|
||
updateCoordinatesFrom('galactic');
|
||
});
|
||
|
||
const updateAngleFrom = async function (sourceUnit) {
|
||
const rawValue = angleFields[sourceUnit].value.trim();
|
||
if (rawValue === '') {
|
||
Object.entries(angleFields).forEach(function ([key, field]) {
|
||
if (key !== sourceUnit) {
|
||
field.value = '';
|
||
updateTooltip(field);
|
||
}
|
||
});
|
||
return;
|
||
}
|
||
|
||
const requestId = ++angleRequestId;
|
||
try {
|
||
const result = await requestConversion({
|
||
kind: 'angle',
|
||
sourceUnit,
|
||
sourceValue: rawValue
|
||
});
|
||
|
||
if (requestId !== angleRequestId || !result.valid) {
|
||
return;
|
||
}
|
||
|
||
setFieldValues(angleFields, result.fields || {});
|
||
} catch (error) {
|
||
return;
|
||
}
|
||
};
|
||
|
||
angleFieldEntries.forEach(function ([unit, field]) {
|
||
field.addEventListener('input', function () {
|
||
updateAngleFrom(unit);
|
||
});
|
||
});
|
||
|
||
document.getElementById('angleClear').addEventListener('click', function () {
|
||
Object.values(angleFields).forEach(function (field) {
|
||
field.value = '';
|
||
updateTooltip(field);
|
||
});
|
||
angleFields.deg.focus();
|
||
});
|
||
|
||
document.getElementById('angleDegree').addEventListener('click', function () {
|
||
angleFields.deg.value = '1';
|
||
updateAngleFrom('deg');
|
||
});
|
||
|
||
document.getElementById('angleArcsecond').addEventListener('click', function () {
|
||
angleFields.arcsec.value = '1';
|
||
updateAngleFrom('arcsec');
|
||
});
|
||
|
||
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');
|
||
updateCoordinatesFrom('equatorial');
|
||
updateAngleFrom('deg');
|
||
})();
|
||
</script>
|
||
|
||
<?php require __DIR__ . '/footer.php'; ?>
|