Astronomische Umrechnungen erweitert
This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
Binary file not shown.
+54
-1
@@ -12,6 +12,7 @@ if SCRIPT_DIR not in sys.path:
|
||||
sys.path.insert(0, SCRIPT_DIR)
|
||||
|
||||
import astronomy
|
||||
import astronomical_conversions
|
||||
|
||||
MOON_RADIUS_KM = 1737.4
|
||||
SYNODIC_MONTH = 29.530588853
|
||||
@@ -204,6 +205,53 @@ def action_current_solar_longitude(args: list[str]) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def action_astronomical_conversions(args: list[str]) -> dict:
|
||||
if len(args) < 1:
|
||||
fail(
|
||||
"Aktion astronomical_conversions erwartet mindestens 1 Argument: kind",
|
||||
extra={"argv": args},
|
||||
)
|
||||
|
||||
kind = str(args[0]).strip().lower()
|
||||
payload: dict[str, str | None] = {"kind": kind}
|
||||
|
||||
if kind == "distance":
|
||||
if len(args) != 3:
|
||||
fail(
|
||||
"Aktion astronomical_conversions fuer distance erwartet 3 Argumente: kind sourceUnit sourceValue",
|
||||
extra={"argv": args},
|
||||
)
|
||||
payload["sourceUnit"] = args[1]
|
||||
payload["sourceValue"] = args[2]
|
||||
elif kind == "redshift":
|
||||
if len(args) != 3:
|
||||
fail(
|
||||
"Aktion astronomical_conversions fuer redshift erwartet 3 Argumente: kind z restNm",
|
||||
extra={"argv": args},
|
||||
)
|
||||
payload["z"] = args[1]
|
||||
payload["restNm"] = args[2]
|
||||
elif kind == "time":
|
||||
if len(args) != 4:
|
||||
fail(
|
||||
"Aktion astronomical_conversions fuer time erwartet 4 Argumente: kind sourceUnit sourceValue longitude",
|
||||
extra={"argv": args},
|
||||
)
|
||||
payload["sourceUnit"] = args[1]
|
||||
payload["sourceValue"] = args[2]
|
||||
payload["longitude"] = args[3]
|
||||
else:
|
||||
fail(
|
||||
"Unbekannter Umrechnungsbereich fuer astronomical_conversions.",
|
||||
extra={"kind": kind, "argv": args},
|
||||
)
|
||||
|
||||
try:
|
||||
return astronomical_conversions.handle_request(payload)
|
||||
except ValueError as exc:
|
||||
fail(str(exc), extra={"kind": kind})
|
||||
|
||||
|
||||
def search_event(
|
||||
body: astronomy.Body,
|
||||
direction: astronomy.Direction,
|
||||
@@ -4145,6 +4193,11 @@ def main() -> None:
|
||||
print(json.dumps(result, ensure_ascii=True))
|
||||
return
|
||||
|
||||
if action == "astronomical_conversions":
|
||||
result = action_astronomical_conversions(args)
|
||||
print(json.dumps(result, ensure_ascii=True))
|
||||
return
|
||||
|
||||
if action == "moon_star_occultations":
|
||||
result = action_moon_star_occultations(args)
|
||||
print(json.dumps(result, ensure_ascii=True))
|
||||
@@ -4270,7 +4323,7 @@ def main() -> None:
|
||||
print(json.dumps(result, ensure_ascii=True))
|
||||
return
|
||||
|
||||
fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_bright_star_approaches_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "planet_parades_for_month", "planet_constellation_changes_for_month", "moon_star_occultations_for_month"]})
|
||||
fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "astronomical_conversions", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_bright_star_approaches_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "planet_parades_for_month", "planet_constellation_changes_for_month", "moon_star_occultations_for_month"]})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,409 @@
|
||||
#!/usr/bin/env python3
|
||||
import math
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP, localcontext
|
||||
|
||||
import astronomy
|
||||
try:
|
||||
from astropy.time import Time as AstropyTime
|
||||
ASTROPY_AVAILABLE = True
|
||||
except ImportError:
|
||||
AstropyTime = None
|
||||
ASTROPY_AVAILABLE = False
|
||||
|
||||
SPEED_OF_LIGHT_KM_S = 299792.458
|
||||
H0 = 70.0
|
||||
OMEGA_M = 0.3
|
||||
OMEGA_LAMBDA = 0.7
|
||||
MPC_IN_KM = 3.0856775814913673e19
|
||||
SECONDS_PER_GYR = 365.25 * 24 * 3600 * 1e9
|
||||
|
||||
DECIMAL_PI = Decimal("3.14159265358979323846264338327950288419716939937510")
|
||||
DECIMAL_180 = Decimal("180")
|
||||
DECIMAL_36525 = Decimal("36525")
|
||||
DECIMAL_2400000_5 = Decimal("2400000.5")
|
||||
DECIMAL_2440587_5 = Decimal("2440587.5")
|
||||
DECIMAL_86400 = Decimal("86400")
|
||||
DECIMAL_1000 = Decimal("1000")
|
||||
DECIMAL_ONE = Decimal("1")
|
||||
DECIMAL_HUNDRED = Decimal("100")
|
||||
|
||||
DISTANCE_TO_KM = {
|
||||
"km": Decimal("1"),
|
||||
"au": Decimal("149597870.7"),
|
||||
"lm": Decimal("17987547.48"),
|
||||
"lh": Decimal("1079252848.8"),
|
||||
"ly": Decimal("9460730472580.8"),
|
||||
"pc": Decimal("30856775814913.67"),
|
||||
"mpc": Decimal("30856775814913670000"),
|
||||
}
|
||||
|
||||
DISTANCE_ORDER = ["km", "au", "lm", "lh", "ly", "pc", "mpc"]
|
||||
REDSHIFT_ORDER = [
|
||||
"z",
|
||||
"restNm",
|
||||
"factor",
|
||||
"scaleFactor",
|
||||
"observedNm",
|
||||
"frequencyFactor",
|
||||
"velocityApprox",
|
||||
"velocityRel",
|
||||
"velocityC",
|
||||
"lookbackGyr",
|
||||
"comovingMpc",
|
||||
"luminosityMpc",
|
||||
"angularDiameterMpc",
|
||||
]
|
||||
TIME_ORDER = ["utc", "ut1", "tai", "jd", "mjd", "unix", "trueSolar", "sidereal"]
|
||||
LEAP_SECONDS = [
|
||||
("1972-01-01T00:00:00Z", 10),
|
||||
("1972-07-01T00:00:00Z", 11),
|
||||
("1973-01-01T00:00:00Z", 12),
|
||||
("1974-01-01T00:00:00Z", 13),
|
||||
("1975-01-01T00:00:00Z", 14),
|
||||
("1976-01-01T00:00:00Z", 15),
|
||||
("1977-01-01T00:00:00Z", 16),
|
||||
("1978-01-01T00:00:00Z", 17),
|
||||
("1979-01-01T00:00:00Z", 18),
|
||||
("1980-01-01T00:00:00Z", 19),
|
||||
("1981-07-01T00:00:00Z", 20),
|
||||
("1982-07-01T00:00:00Z", 21),
|
||||
("1983-07-01T00:00:00Z", 22),
|
||||
("1985-07-01T00:00:00Z", 23),
|
||||
("1988-01-01T00:00:00Z", 24),
|
||||
("1990-01-01T00:00:00Z", 25),
|
||||
("1991-01-01T00:00:00Z", 26),
|
||||
("1992-07-01T00:00:00Z", 27),
|
||||
("1993-07-01T00:00:00Z", 28),
|
||||
("1994-07-01T00:00:00Z", 29),
|
||||
("1996-01-01T00:00:00Z", 30),
|
||||
("1997-07-01T00:00:00Z", 31),
|
||||
("1999-01-01T00:00:00Z", 32),
|
||||
("2006-01-01T00:00:00Z", 33),
|
||||
("2009-01-01T00:00:00Z", 34),
|
||||
("2012-07-01T00:00:00Z", 35),
|
||||
("2015-07-01T00:00:00Z", 36),
|
||||
("2017-01-01T00:00:00Z", 37),
|
||||
]
|
||||
|
||||
|
||||
def parse_decimal_input(value: str) -> Decimal | None:
|
||||
text = str(value or "").strip()
|
||||
if text == "":
|
||||
return None
|
||||
|
||||
normalized = text.replace(" ", "").replace(".", "").replace(",", ".")
|
||||
if normalized.count(".") > 1:
|
||||
return None
|
||||
|
||||
try:
|
||||
return Decimal(normalized)
|
||||
except InvalidOperation:
|
||||
return None
|
||||
|
||||
|
||||
def format_decimal_german(value: Decimal | float | int | None, *, max_fraction_digits: int = 10, min_fraction_digits: int = 0) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
|
||||
decimal_value = value if isinstance(value, Decimal) else Decimal(str(value))
|
||||
quantize_digits = max(0, int(max_fraction_digits))
|
||||
|
||||
with localcontext() as context:
|
||||
context.prec = max(50, quantize_digits + 20)
|
||||
if quantize_digits > 0:
|
||||
quantum = Decimal("1").scaleb(-quantize_digits)
|
||||
decimal_value = decimal_value.quantize(quantum, rounding=ROUND_HALF_UP)
|
||||
|
||||
sign = "-" if decimal_value < 0 else ""
|
||||
decimal_value = abs(decimal_value)
|
||||
text = format(decimal_value, "f")
|
||||
integer_part, _, fractional_part = text.partition(".")
|
||||
|
||||
fractional_part = fractional_part.rstrip("0")
|
||||
if len(fractional_part) < min_fraction_digits:
|
||||
fractional_part = fractional_part + ("0" * (min_fraction_digits - len(fractional_part)))
|
||||
|
||||
integer_part = format(int(integer_part or "0"), ",").replace(",", ".")
|
||||
return f"{sign}{integer_part},{fractional_part}" if fractional_part else f"{sign}{integer_part}"
|
||||
|
||||
|
||||
def format_utc_datetime(dt_utc: datetime) -> str:
|
||||
return dt_utc.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def format_german_datetime(dt_utc: datetime) -> str:
|
||||
return dt_utc.astimezone(timezone.utc).strftime("%d.%m.%Y %H:%M:%S")
|
||||
|
||||
|
||||
def format_clock(hours_value: float) -> str:
|
||||
normalized = (hours_value % 24.0 + 24.0) % 24.0
|
||||
total_seconds = int(round(normalized * 3600.0))
|
||||
hours = (total_seconds // 3600) % 24
|
||||
minutes = (total_seconds % 3600) // 60
|
||||
seconds = total_seconds % 60
|
||||
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
||||
|
||||
|
||||
def leap_seconds_for_utc(dt_utc: datetime) -> int:
|
||||
count = 0
|
||||
for effective_iso, total_offset in LEAP_SECONDS:
|
||||
effective_dt = datetime.strptime(effective_iso, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
|
||||
if dt_utc >= effective_dt:
|
||||
count = total_offset
|
||||
else:
|
||||
break
|
||||
return count
|
||||
|
||||
|
||||
def parse_utc_datetime(value: str) -> datetime | None:
|
||||
text = str(value or "").strip()
|
||||
if text == "":
|
||||
return None
|
||||
|
||||
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d"):
|
||||
try:
|
||||
parsed = datetime.strptime(text, fmt)
|
||||
return parsed.replace(tzinfo=timezone.utc)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def dt_to_time(dt_utc: datetime) -> astronomy.Time:
|
||||
dt_utc = dt_utc.astimezone(timezone.utc)
|
||||
return astronomy.Time.Make(
|
||||
dt_utc.year,
|
||||
dt_utc.month,
|
||||
dt_utc.day,
|
||||
dt_utc.hour,
|
||||
dt_utc.minute,
|
||||
dt_utc.second + (dt_utc.microsecond / 1_000_000.0),
|
||||
)
|
||||
|
||||
|
||||
def time_to_datetime(time_value: astronomy.Time) -> datetime:
|
||||
year, month, day, hour, minute, second = time_value.Calendar()
|
||||
second_int = int(second)
|
||||
microsecond = int(round((second - second_int) * 1_000_000))
|
||||
|
||||
if microsecond >= 1_000_000:
|
||||
second_int += 1
|
||||
microsecond -= 1_000_000
|
||||
|
||||
return datetime(year, month, day, hour, minute, second_int, microsecond, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def julian_decimal_to_datetime(jd_decimal: Decimal) -> datetime:
|
||||
ut = float(jd_decimal - Decimal("2451545.0"))
|
||||
return time_to_datetime(astronomy.Time(ut))
|
||||
|
||||
|
||||
def datetime_to_julian_decimal(dt_utc: datetime) -> Decimal:
|
||||
time_value = dt_to_time(dt_utc)
|
||||
return Decimal(str(time_value.ut)) + Decimal("2451545.0")
|
||||
|
||||
|
||||
def cosmology_e(current_z: float) -> float:
|
||||
return math.sqrt((OMEGA_M * ((1 + current_z) ** 3)) + OMEGA_LAMBDA)
|
||||
|
||||
|
||||
def adaptive_simpson(fn, start: float, end: float, epsilon: float = 1e-10, max_depth: int = 20) -> float:
|
||||
def simpson(left: float, right: float, left_value: float, middle_value: float, right_value: float) -> float:
|
||||
return (right - left) * (left_value + 4 * middle_value + right_value) / 6
|
||||
|
||||
def recurse(left: float, right: float, eps: float, whole: float, left_value: float, middle_value: float, right_value: float, depth: int) -> float:
|
||||
middle = (left + right) / 2
|
||||
left_middle = (left + middle) / 2
|
||||
right_middle = (middle + right) / 2
|
||||
left_middle_value = fn(left_middle)
|
||||
right_middle_value = fn(right_middle)
|
||||
left_area = simpson(left, middle, left_value, left_middle_value, middle_value)
|
||||
right_area = simpson(middle, right, middle_value, right_middle_value, right_value)
|
||||
delta = left_area + right_area - whole
|
||||
|
||||
if depth <= 0 or abs(delta) <= 15 * eps:
|
||||
return left_area + right_area + (delta / 15)
|
||||
|
||||
return recurse(left, middle, eps / 2, left_area, left_value, left_middle_value, middle_value, depth - 1) + recurse(
|
||||
middle, right, eps / 2, right_area, middle_value, right_middle_value, right_value, depth - 1
|
||||
)
|
||||
|
||||
left_value = fn(start)
|
||||
right_value = fn(end)
|
||||
middle = (start + end) / 2
|
||||
middle_value = fn(middle)
|
||||
whole = simpson(start, end, left_value, middle_value, right_value)
|
||||
if not math.isfinite(whole):
|
||||
return math.nan
|
||||
|
||||
return recurse(start, end, epsilon, whole, left_value, middle_value, right_value, max_depth)
|
||||
|
||||
|
||||
def convert_distance(payload: dict) -> dict:
|
||||
source_unit = str(payload.get("sourceUnit") or "").strip().lower()
|
||||
source_value = parse_decimal_input(payload.get("sourceValue", ""))
|
||||
|
||||
if source_unit not in DISTANCE_TO_KM or source_value is None:
|
||||
return {"valid": False, "fields": {key: "" for key in DISTANCE_ORDER}}
|
||||
|
||||
value_in_km = source_value * DISTANCE_TO_KM[source_unit]
|
||||
fields: dict[str, str] = {}
|
||||
for unit in DISTANCE_ORDER:
|
||||
converted = value_in_km / DISTANCE_TO_KM[unit]
|
||||
fields[unit] = format_decimal_german(converted)
|
||||
|
||||
return {"valid": True, "fields": fields}
|
||||
|
||||
|
||||
def convert_redshift(payload: dict) -> dict:
|
||||
z_decimal = parse_decimal_input(payload.get("z", ""))
|
||||
rest_nm_decimal = parse_decimal_input(payload.get("restNm", ""))
|
||||
if z_decimal is None or rest_nm_decimal is None:
|
||||
return {"valid": False, "fields": {key: "" for key in REDSHIFT_ORDER}}
|
||||
|
||||
z = float(z_decimal)
|
||||
rest_nm = float(rest_nm_decimal)
|
||||
if not math.isfinite(z) or not math.isfinite(rest_nm) or z <= -1.0:
|
||||
return {"valid": False, "fields": {key: "" for key in REDSHIFT_ORDER}}
|
||||
|
||||
factor_decimal = DECIMAL_ONE + z_decimal
|
||||
scale_factor_decimal = DECIMAL_ONE / factor_decimal
|
||||
observed_nm_decimal = rest_nm_decimal * factor_decimal
|
||||
frequency_factor_decimal = DECIMAL_ONE / factor_decimal
|
||||
velocity_approx_decimal = z_decimal * Decimal(str(SPEED_OF_LIGHT_KM_S))
|
||||
factor_squared_decimal = factor_decimal * factor_decimal
|
||||
beta_rel_decimal = (factor_squared_decimal - DECIMAL_ONE) / (factor_squared_decimal + DECIMAL_ONE)
|
||||
velocity_rel_decimal = beta_rel_decimal * Decimal(str(SPEED_OF_LIGHT_KM_S))
|
||||
velocity_c_decimal = beta_rel_decimal * DECIMAL_HUNDRED
|
||||
|
||||
factor = float(factor_decimal)
|
||||
comoving_integral = adaptive_simpson(lambda current_z: 1 / cosmology_e(current_z), 0, z, 1e-10, 20)
|
||||
lookback_integral = adaptive_simpson(lambda current_z: 1 / ((1 + current_z) * cosmology_e(current_z)), 0, z, 1e-10, 20)
|
||||
hubble_time_seconds = MPC_IN_KM / H0
|
||||
comoving_mpc = (SPEED_OF_LIGHT_KM_S / H0) * comoving_integral
|
||||
luminosity_mpc = comoving_mpc * factor
|
||||
angular_diameter_mpc = comoving_mpc / factor
|
||||
lookback_gyr = (hubble_time_seconds * lookback_integral) / SECONDS_PER_GYR
|
||||
|
||||
fields = {
|
||||
"z": format_decimal_german(z_decimal),
|
||||
"restNm": format_decimal_german(rest_nm_decimal),
|
||||
"factor": format_decimal_german(factor_decimal),
|
||||
"scaleFactor": format_decimal_german(scale_factor_decimal),
|
||||
"observedNm": format_decimal_german(observed_nm_decimal),
|
||||
"frequencyFactor": format_decimal_german(frequency_factor_decimal),
|
||||
"velocityApprox": format_decimal_german(velocity_approx_decimal),
|
||||
"velocityRel": format_decimal_german(velocity_rel_decimal),
|
||||
"velocityC": format_decimal_german(velocity_c_decimal),
|
||||
"lookbackGyr": format_decimal_german(lookback_gyr),
|
||||
"comovingMpc": format_decimal_german(comoving_mpc),
|
||||
"luminosityMpc": format_decimal_german(luminosity_mpc),
|
||||
"angularDiameterMpc": format_decimal_german(angular_diameter_mpc),
|
||||
}
|
||||
return {"valid": True, "fields": fields}
|
||||
|
||||
|
||||
def convert_time(payload: dict) -> dict:
|
||||
source_unit = str(payload.get("sourceUnit") or "").strip()
|
||||
longitude = payload.get("longitude")
|
||||
longitude_value = None
|
||||
try:
|
||||
longitude_value = float(longitude) if longitude not in (None, "") else None
|
||||
except (TypeError, ValueError):
|
||||
longitude_value = None
|
||||
|
||||
dt_utc: datetime | None = None
|
||||
if source_unit == "utc":
|
||||
dt_utc = parse_utc_datetime(payload.get("sourceValue", ""))
|
||||
elif source_unit == "jd":
|
||||
jd_decimal = parse_decimal_input(payload.get("sourceValue", ""))
|
||||
if jd_decimal is not None:
|
||||
dt_utc = julian_decimal_to_datetime(jd_decimal)
|
||||
elif source_unit == "mjd":
|
||||
mjd_decimal = parse_decimal_input(payload.get("sourceValue", ""))
|
||||
if mjd_decimal is not None:
|
||||
dt_utc = julian_decimal_to_datetime(mjd_decimal + DECIMAL_2400000_5)
|
||||
elif source_unit == "unix":
|
||||
unix_decimal = parse_decimal_input(payload.get("sourceValue", ""))
|
||||
if unix_decimal is not None:
|
||||
unix_ms = int((unix_decimal * DECIMAL_1000).to_integral_value(rounding=ROUND_HALF_UP))
|
||||
dt_utc = datetime.fromtimestamp(unix_ms / 1000, tz=timezone.utc)
|
||||
|
||||
if dt_utc is None:
|
||||
return {"valid": False, "fields": {key: "" for key in TIME_ORDER}}
|
||||
|
||||
jd_decimal = datetime_to_julian_decimal(dt_utc)
|
||||
mjd_decimal = jd_decimal - DECIMAL_2400000_5
|
||||
unix_decimal = Decimal(str(dt_utc.timestamp()))
|
||||
|
||||
fields = {
|
||||
"utc": format_utc_datetime(dt_utc),
|
||||
"ut1": "",
|
||||
"tai": "",
|
||||
"jd": format_decimal_german(jd_decimal, min_fraction_digits=1),
|
||||
"mjd": format_decimal_german(mjd_decimal),
|
||||
"unix": format_decimal_german(unix_decimal),
|
||||
"trueSolar": "",
|
||||
"sidereal": "",
|
||||
}
|
||||
|
||||
if ASTROPY_AVAILABLE:
|
||||
astropy_time = AstropyTime(dt_utc, scale="utc")
|
||||
try:
|
||||
ut1_datetime = astropy_time.ut1.to_datetime(timezone=timezone.utc)
|
||||
fields["ut1"] = format_german_datetime(ut1_datetime)
|
||||
except Exception:
|
||||
fields["ut1"] = "Nicht verfuegbar"
|
||||
|
||||
try:
|
||||
tai_datetime = astropy_time.tai.to_datetime(timezone=timezone.utc)
|
||||
fields["tai"] = format_german_datetime(tai_datetime)
|
||||
except Exception:
|
||||
fields["tai"] = "Nicht verfuegbar"
|
||||
else:
|
||||
leap_seconds = leap_seconds_for_utc(dt_utc)
|
||||
fields["ut1"] = format_german_datetime(dt_utc) + " (nahezu UTC)"
|
||||
fields["tai"] = format_german_datetime(dt_utc + timedelta(seconds=leap_seconds))
|
||||
|
||||
if longitude_value is not None and math.isfinite(longitude_value):
|
||||
observer = astronomy.Observer(0.0, longitude_value, 0.0)
|
||||
time_value = dt_to_time(dt_utc)
|
||||
sidereal_hours = (astronomy.SiderealTime(time_value) + longitude_value / 15.0) % 24.0
|
||||
hour_angle = astronomy.HourAngle(astronomy.Body.Sun, time_value, observer)
|
||||
true_solar_hours = (hour_angle + 12.0) % 24.0
|
||||
utc_hours = dt_utc.hour + (dt_utc.minute / 60.0) + (dt_utc.second / 3600.0) + (dt_utc.microsecond / 3_600_000_000.0)
|
||||
delta_hours = true_solar_hours - utc_hours
|
||||
if delta_hours <= -12.0:
|
||||
delta_hours += 24.0
|
||||
elif delta_hours > 12.0:
|
||||
delta_hours -= 24.0
|
||||
|
||||
true_solar_dt = dt_utc + timedelta(hours=delta_hours)
|
||||
fields["trueSolar"] = format_german_datetime(true_solar_dt)
|
||||
fields["sidereal"] = format_clock(sidereal_hours)
|
||||
else:
|
||||
fields["trueSolar"] = "Kein Standard-Standort verfuegbar"
|
||||
fields["sidereal"] = "Kein Standard-Standort verfuegbar"
|
||||
|
||||
return {"valid": True, "fields": fields}
|
||||
|
||||
|
||||
def handle_request(payload: dict) -> dict:
|
||||
kind = str(payload.get("kind") or "").strip().lower()
|
||||
if kind == "distance":
|
||||
result = convert_distance(payload)
|
||||
elif kind == "redshift":
|
||||
result = convert_redshift(payload)
|
||||
elif kind == "time":
|
||||
result = convert_time(payload)
|
||||
else:
|
||||
raise ValueError("Unbekannter Umrechnungsbereich.")
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"action": "astronomical_conversions",
|
||||
"kind": kind,
|
||||
**result,
|
||||
}
|
||||
Reference in New Issue
Block a user