Files
2026-07-24 13:13:32 +02:00

1912 lines
72 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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'] ?? '')),
]);
} elseif ($kind === 'tools') {
$pythonResult = runPythonApi('astronomical_conversions', [
'tools',
trim((string) ($_POST['tool'] ?? '')),
trim((string) ($_POST['dateTimeLocal'] ?? '')),
trim((string) ($_POST['latitude'] ?? '')),
trim((string) ($_POST['longitude'] ?? '')),
trim((string) ($_POST['timeZone'] ?? '')),
]);
} 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;
$toolLocationName = !empty($currentLocation['name']) ? (string) $currentLocation['name'] : 'Standard-Standort';
$toolLocationLatitude = isset($currentLocation['latitude']) && is_numeric($currentLocation['latitude'])
? (float) $currentLocation['latitude']
: 50.3597;
$toolLocationLongitude = isset($currentLocation['longitude']) && is_numeric($currentLocation['longitude'])
? (float) $currentLocation['longitude']
: 11.1907;
$toolLocationTimezone = !empty($currentLocation['timezone']) ? (string) $currentLocation['timezone'] : 'Europe/Berlin';
$toolDefaultLocalDateTime = (new DateTimeImmutable('now', new DateTimeZone($toolLocationTimezone)))->format('Y-m-d\TH:i');
?>
<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>
<button type="button" class="astro-conv-tab" data-tab-target="antennaTab" role="tab" aria-selected="false">Antennen</button>
<button type="button" class="astro-conv-tab" data-tab-target="toolsTab" role="tab" aria-selected="false">Diverse Tools</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>
<section class="card astro-conv-panel astro-conv-tab-panel" id="antennaTab" role="tabpanel">
<h2>Antennen-Umrechner</h2>
<p>Frequenz und Wellenlaenge ineinander umrechnen sowie Richtwerte fuer einen Halbwellendipol berechnen.</p>
<div class="astro-conv-grid" id="antennaConverter">
<div class="astro-conv-field">
<label for="antennaFrequencyMhz">Frequenz (MHz)</label>
<input class="astro-conv-input" id="antennaFrequencyMhz" type="text" inputmode="decimal" value="145">
</div>
<div class="astro-conv-field">
<label for="antennaWavelengthM">Wellenlaenge (m)</label>
<input class="astro-conv-input" id="antennaWavelengthM" type="text" inputmode="decimal">
</div>
<div class="astro-conv-field">
<label for="antennaWavelengthCm">Wellenlaenge (cm)</label>
<input class="astro-conv-input" id="antennaWavelengthCm" type="text" readonly>
</div>
<div class="astro-conv-field">
<label for="antennaHalfwaveTotalM"><span class="astro-conv-label-help" title="Theoretische Gesamtlaenge eines Halbwellendipols: Lambda / 2.">Dipol gesamt theoretisch (m)</span></label>
<input class="astro-conv-input" id="antennaHalfwaveTotalM" type="text" readonly>
</div>
<div class="astro-conv-field">
<label for="antennaHalfwavePracticalM"><span class="astro-conv-label-help" title="Praxisnaher Richtwert mit Verkuerzungsfaktor 0,95 fuer einen einfachen Drahtdipol.">Dipol gesamt praktisch (m)</span></label>
<input class="astro-conv-input" id="antennaHalfwavePracticalM" type="text" readonly>
</div>
<div class="astro-conv-field">
<label for="antennaLegLengthM"><span class="astro-conv-label-help" title="Laenge eines einzelnen Dipol-Schenkels. Praktischer Richtwert: Lambda x 0,95 / 4.">Ein Schenkel (m)</span></label>
<input class="astro-conv-input" id="antennaLegLengthM" type="text" readonly>
</div>
</div>
<div class="astro-conv-actions">
<button type="button" class="btn btn-secondary" id="antennaClear">Leeren</button>
<button type="button" class="btn btn-primary" id="antenna2m">2-m-Band</button>
<button type="button" class="btn btn-primary" id="antennaCb">CB-Funk</button>
</div>
</section>
<section class="card astro-conv-panel astro-conv-tab-panel" id="toolsTab" role="tabpanel">
<h2>Diverse Tools</h2>
<p>Sonnenstand für den Standard-Standort aus dem Header berechnen. Datum und Uhrzeit werden in der lokalen Zeitzone des Standorts interpretiert.</p>
<div class="astro-conv-grid" id="toolsConverter">
<div class="astro-conv-field">
<label for="toolsDateTimeLocal">Datum und Uhrzeit lokal</label>
<input class="astro-conv-input" id="toolsDateTimeLocal" type="datetime-local" value="<?= h($toolDefaultLocalDateTime) ?>">
</div>
<div class="astro-conv-field">
<label for="toolsLocationName">Standort</label>
<input class="astro-conv-input" id="toolsLocationName" type="text" value="<?= h($toolLocationName) ?>" readonly>
</div>
<div class="astro-conv-field">
<label for="toolsTimeZone">Zeitzone</label>
<input class="astro-conv-input" id="toolsTimeZone" type="text" value="<?= h($toolLocationTimezone) ?>" readonly>
</div>
<div class="astro-conv-field">
<label for="toolsLatitude">Breitengrad</label>
<input class="astro-conv-input" id="toolsLatitude" type="text" value="<?= h(number_format($toolLocationLatitude, 4, '.', '')) ?>" readonly>
</div>
<div class="astro-conv-field">
<label for="toolsLongitude">Längengrad</label>
<input class="astro-conv-input" id="toolsLongitude" type="text" value="<?= h(number_format($toolLocationLongitude, 4, '.', '')) ?>" readonly>
</div>
<div class="astro-conv-field">
<label for="toolsDateTimeUtc">UTC</label>
<input class="astro-conv-input" id="toolsDateTimeUtc" type="text" readonly>
</div>
<div class="astro-conv-field">
<label for="toolsSunAltitude">Sonnenhöhe Alt (°)</label>
<input class="astro-conv-input" id="toolsSunAltitude" type="text" readonly>
</div>
<div class="astro-conv-field">
<label for="toolsSunAzimuth">Azimut Azi (°)</label>
<input class="astro-conv-input" id="toolsSunAzimuth" type="text" readonly>
</div>
</div>
<div class="astro-conv-actions">
<button type="button" class="btn btn-secondary" id="toolsClear">Leeren</button>
<button type="button" class="btn btn-primary" id="toolsNow">Jetzt am Standort</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 USER_TOOL_LOCATION = {
name: <?= json_encode($toolLocationName, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>,
latitude: <?= json_encode($toolLocationLatitude) ?>,
longitude: <?= json_encode($toolLocationLongitude) ?>,
timeZone: <?= json_encode($toolLocationTimezone, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>
};
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')
};
const antennaFields = {
frequencyMhz: document.getElementById('antennaFrequencyMhz'),
wavelengthM: document.getElementById('antennaWavelengthM'),
wavelengthCm: document.getElementById('antennaWavelengthCm'),
halfwaveTotalM: document.getElementById('antennaHalfwaveTotalM'),
halfwavePracticalM: document.getElementById('antennaHalfwavePracticalM'),
legLengthM: document.getElementById('antennaLegLengthM')
};
const toolFields = {
dateTimeLocal: document.getElementById('toolsDateTimeLocal'),
locationName: document.getElementById('toolsLocationName'),
timeZone: document.getElementById('toolsTimeZone'),
latitude: document.getElementById('toolsLatitude'),
longitude: document.getElementById('toolsLongitude'),
dateTimeUtc: document.getElementById('toolsDateTimeUtc'),
altitude: document.getElementById('toolsSunAltitude'),
azimuth: document.getElementById('toolsSunAzimuth')
};
let isSyncingTimePicker = false;
let timeUtcPicker = null;
let distanceRequestId = 0;
let redshiftRequestId = 0;
let timeRequestId = 0;
let coordinateRequestId = 0;
let angleRequestId = 0;
let antennaRequestId = 0;
let toolRequestId = 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 antennaFieldEntries = Object.entries(antennaFields);
antennaFieldEntries.forEach(function ([unit, field]) {
attachTooltip(field, 'antenna-' + 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 formatNumber = function (value, maxFractionDigits) {
if (!Number.isFinite(value)) {
return '';
}
return value.toLocaleString('de-DE', {
maximumFractionDigits: maxFractionDigits,
useGrouping: true
});
};
const clearAntennaOutputs = function () {
['wavelengthCm', 'halfwaveTotalM', 'halfwavePracticalM', 'legLengthM'].forEach(function (key) {
antennaFields[key].value = '';
updateTooltip(antennaFields[key]);
});
};
const updateAntennaFrom = function (sourceUnit) {
const sourceField = antennaFields[sourceUnit];
if (!sourceField) {
return;
}
const rawValue = sourceField.value.trim();
if (rawValue === '') {
Object.entries(antennaFields).forEach(function ([key, field]) {
if (key !== sourceUnit) {
field.value = '';
updateTooltip(field);
}
});
return;
}
const parsedValue = parseDecimalInput(rawValue);
const numericValue = parsedValue ? decimalToNumber(parsedValue) : Number.NaN;
if (!Number.isFinite(numericValue) || numericValue <= 0) {
clearAntennaOutputs();
return;
}
const requestId = ++antennaRequestId;
const wavelengthMeters = sourceUnit === 'frequencyMhz'
? 299.792458 / numericValue
: numericValue;
const frequencyMhz = sourceUnit === 'wavelengthM'
? 299.792458 / numericValue
: numericValue;
if (requestId !== antennaRequestId || !Number.isFinite(wavelengthMeters) || wavelengthMeters <= 0) {
clearAntennaOutputs();
return;
}
const practicalFactor = 0.95;
antennaFields.frequencyMhz.value = formatNumber(frequencyMhz, 6);
antennaFields.wavelengthM.value = formatNumber(wavelengthMeters, 6);
antennaFields.wavelengthCm.value = formatNumber(wavelengthMeters * 100, 2);
antennaFields.halfwaveTotalM.value = formatNumber(wavelengthMeters / 2, 4);
antennaFields.halfwavePracticalM.value = formatNumber((wavelengthMeters / 2) * practicalFactor, 4);
antennaFields.legLengthM.value = formatNumber((wavelengthMeters / 4) * practicalFactor, 4);
Object.values(antennaFields).forEach(function (field) {
updateTooltip(field);
});
};
['frequencyMhz', 'wavelengthM'].forEach(function (key) {
antennaFields[key].addEventListener('input', function () {
updateAntennaFrom(key);
});
});
document.getElementById('antennaClear').addEventListener('click', function () {
Object.values(antennaFields).forEach(function (field) {
field.value = '';
updateTooltip(field);
});
antennaFields.frequencyMhz.focus();
});
document.getElementById('antenna2m').addEventListener('click', function () {
antennaFields.frequencyMhz.value = '145';
updateAntennaFrom('frequencyMhz');
});
document.getElementById('antennaCb').addEventListener('click', function () {
antennaFields.frequencyMhz.value = '27,2';
updateAntennaFrom('frequencyMhz');
});
const clearToolOutputs = function () {
toolFields.dateTimeUtc.value = '';
toolFields.altitude.value = '';
toolFields.azimuth.value = '';
updateTooltip(toolFields.dateTimeUtc);
updateTooltip(toolFields.altitude);
updateTooltip(toolFields.azimuth);
};
const updateSolarTool = async function () {
const localDateTime = toolFields.dateTimeLocal.value.trim();
if (localDateTime === '') {
clearToolOutputs();
return;
}
const requestId = ++toolRequestId;
try {
const result = await requestConversion({
kind: 'tools',
tool: 'solar_altaz',
dateTimeLocal: localDateTime,
latitude: USER_TOOL_LOCATION.latitude,
longitude: USER_TOOL_LOCATION.longitude,
timeZone: USER_TOOL_LOCATION.timeZone
});
if (requestId !== toolRequestId || !result.valid) {
return;
}
toolFields.dateTimeUtc.value = typeof result.fields.utc === 'string' ? result.fields.utc : '';
toolFields.altitude.value = typeof result.fields.altitude === 'string' ? result.fields.altitude : '';
toolFields.azimuth.value = typeof result.fields.azimuth === 'string' ? result.fields.azimuth : '';
updateTooltip(toolFields.dateTimeUtc);
updateTooltip(toolFields.altitude);
updateTooltip(toolFields.azimuth);
} catch (error) {
clearToolOutputs();
}
};
toolFields.dateTimeLocal.addEventListener('input', updateSolarTool);
document.getElementById('toolsClear').addEventListener('click', function () {
toolFields.dateTimeLocal.value = '';
clearToolOutputs();
toolFields.dateTimeLocal.focus();
});
document.getElementById('toolsNow').addEventListener('click', function () {
const now = new Date();
const formatter = new Intl.DateTimeFormat('sv-SE', {
timeZone: USER_TOOL_LOCATION.timeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hourCycle: 'h23'
});
toolFields.dateTimeLocal.value = formatter.format(now).replace(' ', 'T');
updateSolarTool();
});
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');
updateAntennaFrom('frequencyMhz');
updateSolarTool();
})();
</script>
<?php require __DIR__ . '/footer.php'; ?>