Astronomische Umrechnungen erweitert

This commit is contained in:
Eskimue
2026-04-11 13:52:18 +02:00
parent 0504c7d03b
commit 07c6d701c2
4 changed files with 911 additions and 94 deletions
+448 -93
View File
@@ -15,8 +15,99 @@ function h(?string $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
function runPythonApi(string $action, array $args): array
{
$scriptPath = __DIR__ . '/py/api.py';
$pythonCandidates = PHP_OS_FAMILY === 'Windows'
? ['python', 'py']
: ['/usr/bin/python3', '/usr/bin/python', 'python3', 'python'];
$escapedArguments = array_map('escapeshellarg', array_merge([$scriptPath, $action], $args));
$lastError = 'Python konnte nicht gestartet werden.';
foreach ($pythonCandidates as $pythonBinary) {
$command = $pythonBinary . ' ' . implode(' ', $escapedArguments) . ' 2>&1';
$output = [];
$resultCode = 0;
exec($command, $output, $resultCode);
$joined = trim(implode("\n", $output));
$decoded = json_decode($joined, true);
if (is_array($decoded)) {
return [
'ok' => (bool) ($decoded['ok'] ?? false),
'data' => $decoded,
'raw' => $joined,
'command' => $command,
];
}
if ($joined !== '') {
$lastError = $joined;
} elseif ($resultCode !== 0) {
$lastError = 'Fehlercode ' . $resultCode . ' bei ' . $pythonBinary;
}
}
return [
'ok' => false,
'error' => $lastError,
];
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string) ($_POST['ajax'] ?? '') === '1') {
header('Content-Type: application/json; charset=utf-8');
$kind = trim((string) ($_POST['kind'] ?? ''));
if ($kind === 'distance') {
$pythonResult = runPythonApi('astronomical_conversions', [
'distance',
trim((string) ($_POST['sourceUnit'] ?? '')),
trim((string) ($_POST['sourceValue'] ?? '')),
]);
} elseif ($kind === 'redshift') {
$pythonResult = runPythonApi('astronomical_conversions', [
'redshift',
trim((string) ($_POST['z'] ?? '')),
trim((string) ($_POST['restNm'] ?? '')),
]);
} elseif ($kind === 'time') {
$pythonResult = runPythonApi('astronomical_conversions', [
'time',
trim((string) ($_POST['sourceUnit'] ?? '')),
trim((string) ($_POST['sourceValue'] ?? '')),
trim((string) ($_POST['longitude'] ?? '')),
]);
} else {
echo json_encode([
'ok' => false,
'error' => 'Unbekannter Umrechnungsbereich.',
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
if (!$pythonResult['ok']) {
echo json_encode([
'ok' => false,
'error' => (string) ($pythonResult['error'] ?? 'Python-Fehler'),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
echo json_encode($pythonResult['data'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
?>
<?php require __DIR__ . '/header.php'; ?>
<?php
$timeLocationName = !empty($currentLocation['name']) ? (string) $currentLocation['name'] : '';
$timeLocationLongitude = isset($currentLocation['longitude']) && is_numeric($currentLocation['longitude'])
? (float) $currentLocation['longitude']
: null;
?>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<style>
.astro-conv-layout {
@@ -281,7 +372,17 @@ function h(?string $value): string
<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">
<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">
@@ -298,6 +399,42 @@ function h(?string $value): string
<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">
@@ -309,8 +446,15 @@ function h(?string $value): string
</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;
@@ -352,10 +496,19 @@ function h(?string $value): string
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')
unix: document.getElementById('timeUnix'),
trueSolar: document.getElementById('timeTrueSolar'),
sidereal: document.getElementById('timeSidereal')
};
let isSyncingTimePicker = false;
let timeUtcPicker = null;
let distanceRequestId = 0;
let redshiftRequestId = 0;
let timeRequestId = 0;
const DECIMAL_DIVISION_PRECISION = 32;
const pow10Cache = [1n];
@@ -627,6 +780,15 @@ function h(?string $value): string
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);
};
@@ -758,7 +920,7 @@ function h(?string $value): string
wireTooltipHover(field);
});
const updateDistanceFrom = function (sourceUnit) {
const updateDistanceFrom = async function (sourceUnit) {
const rawValue = distanceFields[sourceUnit].value.trim();
if (rawValue === '') {
Object.values(distanceFields).forEach(function (field, index) {
@@ -769,22 +931,22 @@ function h(?string $value): string
return;
}
const decimalValue = parseDecimalInput(rawValue);
if (!decimalValue) {
return;
}
const requestId = ++distanceRequestId;
try {
const result = await requestConversion({
kind: 'distance',
sourceUnit,
sourceValue: rawValue
});
distanceFields[sourceUnit].value = formatValue(decimalValue);
updateTooltip(distanceFields[sourceUnit]);
Object.entries(distanceFields).forEach(function ([unit, field]) {
if (unit === sourceUnit) {
if (requestId !== distanceRequestId || !result.valid) {
return;
}
field.value = formatValue(convertDistanceDecimal(decimalValue, sourceUnit, unit));
updateTooltip(field);
});
setFieldValues(distanceFields, result.fields || {});
} catch (error) {
return;
}
};
distanceFieldEntries.forEach(function ([unit, field]) {
@@ -810,60 +972,29 @@ function h(?string $value): string
updateDistanceFrom('au');
});
const updateRedshift = function () {
const zDecimal = parseDecimalInput(redshiftFields.z.value.trim());
const restNmDecimal = parseDecimalInput(redshiftFields.restNm.value.trim());
const z = zDecimal ? decimalToNumber(zDecimal) : Number.NaN;
const restNm = restNmDecimal ? decimalToNumber(restNmDecimal) : Number.NaN;
if (!zDecimal || !restNmDecimal || !Number.isFinite(z) || !Number.isFinite(restNm) || z <= -1) {
const updateRedshift = async function () {
const zValue = redshiftFields.z.value.trim();
const restNmValue = redshiftFields.restNm.value.trim();
if (zValue === '' || restNmValue === '') {
return;
}
const factorDecimal = decimalAdd(decimalConstants.one, zDecimal);
const factor = decimalToNumber(factorDecimal);
const scaleFactorDecimal = decimalDivide(decimalConstants.one, factorDecimal, DECIMAL_DIVISION_PRECISION);
const observedNmDecimal = decimalMultiply(restNmDecimal, factorDecimal);
const frequencyFactorDecimal = decimalDivide(decimalConstants.one, factorDecimal, DECIMAL_DIVISION_PRECISION);
const velocityApproxDecimal = decimalMultiply(zDecimal, decimalConstants.speedOfLightKmS);
const factorSquaredDecimal = decimalSquare(factorDecimal);
const betaRelDecimal = decimalDivide(
decimalSubtract(factorSquaredDecimal, decimalConstants.one),
decimalAdd(factorSquaredDecimal, decimalConstants.one),
DECIMAL_DIVISION_PRECISION
);
const velocityRelDecimal = decimalMultiply(betaRelDecimal, decimalConstants.speedOfLightKmS);
const velocityCDecimal = decimalMultiply(betaRelDecimal, decimalConstants.hundred);
const requestId = ++redshiftRequestId;
try {
const result = await requestConversion({
kind: 'redshift',
z: zValue,
restNm: restNmValue
});
const comovingIntegral = adaptiveSimpson(function (currentZ) {
return 1 / cosmologyE(currentZ);
}, 0, z, 1e-10, 20);
const lookbackIntegral = adaptiveSimpson(function (currentZ) {
return 1 / ((1 + currentZ) * cosmologyE(currentZ));
}, 0, z, 1e-10, 20);
const hubbleTimeSeconds = MPC_IN_KM / H0;
const comovingMpc = (SPEED_OF_LIGHT_KM_S / H0) * comovingIntegral;
const luminosityMpc = comovingMpc * factor;
const angularDiameterMpc = comovingMpc / factor;
const lookbackGyr = (hubbleTimeSeconds * lookbackIntegral) / SECONDS_PER_GYR;
if (requestId !== redshiftRequestId || !result.valid) {
return;
}
redshiftFields.z.value = formatValue(zDecimal);
redshiftFields.restNm.value = formatValue(restNmDecimal);
redshiftFields.factor.value = formatValue(factorDecimal);
redshiftFields.scaleFactor.value = formatValue(scaleFactorDecimal);
redshiftFields.observedNm.value = formatValue(observedNmDecimal);
redshiftFields.frequencyFactor.value = formatValue(frequencyFactorDecimal);
redshiftFields.velocityApprox.value = formatValue(velocityApproxDecimal);
redshiftFields.velocityRel.value = formatValue(velocityRelDecimal);
redshiftFields.velocityC.value = formatValue(velocityCDecimal);
redshiftFields.lookbackGyr.value = formatValue(decimalFromNumber(lookbackGyr, 18));
redshiftFields.comovingMpc.value = formatValue(decimalFromNumber(comovingMpc, 18));
redshiftFields.luminosityMpc.value = formatValue(decimalFromNumber(luminosityMpc, 18));
redshiftFields.angularDiameterMpc.value = formatValue(decimalFromNumber(angularDiameterMpc, 18));
Object.values(redshiftFields).forEach(function (field) {
updateTooltip(field);
});
setFieldValues(redshiftFields, result.fields || {});
} catch (error) {
return;
}
};
['z', 'restNm'].forEach(function (key) {
@@ -926,6 +1057,98 @@ function h(?string $value): string
return parts.join('-') + ' ' + timeParts.join(':');
};
const formatUtcDateTimeGerman = function (date) {
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
return '';
}
const parts = [
date.getUTCDate().toString().padStart(2, '0'),
(date.getUTCMonth() + 1).toString().padStart(2, '0'),
date.getUTCFullYear().toString().padStart(4, '0')
];
const timeParts = [
date.getUTCHours().toString().padStart(2, '0'),
date.getUTCMinutes().toString().padStart(2, '0'),
date.getUTCSeconds().toString().padStart(2, '0')
];
return parts.join('.') + ' ' + timeParts.join(':');
};
const parseUtcDateTimeGerman = function (value) {
const match = String(value).trim().match(/^(\d{2})\.(\d{2})\.(\d{4})(?:[ ](\d{2}):(\d{2})(?::(\d{2}))?)?$/);
if (!match) {
return null;
}
const day = Number(match[1]);
const month = Number(match[2]);
const year = Number(match[3]);
const hour = Number(match[4] || '0');
const minute = Number(match[5] || '0');
const second = Number(match[6] || '0');
const date = new Date(Date.UTC(year, month - 1, day, hour, minute, second));
return Number.isNaN(date.getTime()) ? null : date;
};
const syncTimePickerFromDate = function (date) {
if (!timeUtcPicker) {
return;
}
isSyncingTimePicker = true;
timeUtcPicker.setDate(date, false);
isSyncingTimePicker = false;
};
if (window.flatpickr) {
timeUtcPicker = window.flatpickr(timeFields.utc, {
enableTime: true,
enableSeconds: true,
time_24hr: true,
allowInput: true,
altInput: true,
altFormat: 'd.m.Y H:i:S',
dateFormat: 'Y-m-d H:i:S',
locale: window.flatpickr.l10ns.de,
defaultDate: parseUtcDateTime(timeFields.utc.value),
parseDate: function (value, format) {
if (format === 'd.m.Y H:i:S') {
return parseUtcDateTimeGerman(value);
}
return parseUtcDateTime(value);
},
formatDate: function (date, format) {
if (format === 'd.m.Y H:i:S') {
return formatUtcDateTimeGerman(date);
}
return formatUtcDateTime(date);
},
onValueUpdate: function (selectedDates, dateStr, instance) {
if (isSyncingTimePicker) {
return;
}
timeFields.utc.value = dateStr;
updateTimeFrom('utc');
if (instance.altInput) {
instance.altInput.setAttribute('placeholder', 'TT.MM.JJJJ HH:MM:SS');
}
}
});
if (timeUtcPicker.altInput) {
timeUtcPicker.altInput.classList.add('astro-conv-input');
timeUtcPicker.altInput.setAttribute('placeholder', 'TT.MM.JJJJ HH:MM:SS');
}
}
const dateToJulianDateDecimal = function (date) {
const unixMsDecimal = decimalFromInteger(BigInt(date.getTime()));
return decimalAdd(
@@ -934,6 +1157,91 @@ function h(?string $value): string
);
};
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),
@@ -943,43 +1251,73 @@ function h(?string $value): string
return new Date(Number(unixMs));
};
const updateTimeFrom = function (sourceUnit) {
let date = null;
if (sourceUnit === 'utc') {
date = parseUtcDateTime(timeFields.utc.value);
} else if (sourceUnit === 'jd') {
const jdDecimal = parseDecimalInput(timeFields.jd.value.trim());
date = jdDecimal ? julianDateDecimalToDate(jdDecimal) : null;
} else if (sourceUnit === 'mjd') {
const mjdDecimal = parseDecimalInput(timeFields.mjd.value.trim());
date = mjdDecimal ? julianDateDecimalToDate(decimalAdd(mjdDecimal, decimalConstants.mjdOffset)) : null;
} else if (sourceUnit === 'unix') {
const unixSecondsDecimal = parseDecimalInput(timeFields.unix.value.trim());
if (unixSecondsDecimal) {
const unixMs = decimalRoundToBigInt(decimalMultiply(unixSecondsDecimal, decimalConstants.msPerSecond));
date = new Date(Number(unixMs));
}
}
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
const updateTimeFrom = async function (sourceUnit) {
const sourceField = timeFields[sourceUnit];
if (!sourceField) {
return;
}
const jdDecimal = dateToJulianDateDecimal(date);
const mjdDecimal = decimalSubtract(jdDecimal, decimalConstants.mjdOffset);
const unixSecondsDecimal = decimalDivide(decimalFromInteger(BigInt(date.getTime())), decimalConstants.msPerSecond, 10);
const sourceValue = sourceField.value.trim();
if (sourceValue === '') {
return;
}
timeFields.utc.value = formatUtcDateTime(date);
timeFields.jd.value = formatValue(jdDecimal);
timeFields.mjd.value = formatValue(mjdDecimal);
timeFields.unix.value = formatValue(unixSecondsDecimal);
const requestId = ++timeRequestId;
try {
const result = await requestConversion({
kind: 'time',
sourceUnit,
sourceValue,
longitude: USER_TIME_LOCATION.longitude
});
Object.values(timeFields).forEach(function (field) {
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');
});
@@ -991,8 +1329,25 @@ function h(?string $value): string
document.getElementById('timeClear').addEventListener('click', function () {
Object.values(timeFields).forEach(function (field) {
field.value = '';
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();
});