Ephemeriden Karte eingebaut

This commit is contained in:
2026-04-28 10:47:55 +02:00
parent 5adca790eb
commit 33f5dce638
2 changed files with 889 additions and 11 deletions
+873 -11
View File
@@ -19,6 +19,13 @@ $favoriteMinorplanetOptions = [];
$favoriteCometOptions = [];
$minorplanetPayloads = [];
$cometPayloads = [];
$chartStarData = [];
$chartLineStarData = [];
$chartConstellationLines = [];
$ephemerisChartConfigJson = 'null';
$ephemerisStarDataJson = '[]';
$ephemerisLineStarDataJson = '[]';
$ephemerisConstellationLinesJson = '[]';
function decodeMpcPackedDateChar(string $value): ?int
{
@@ -205,12 +212,96 @@ if ($loggedIn && isset($_SESSION['user_id'])) {
'inclinationDeg' => isset($comet['inclination_deg']) ? (float) $comet['inclination_deg'] : null,
];
}
$stmtStars = $pdo->query(
"SELECT hip, proper, bayer, con, spect, mag, ci, rarad, decrad
FROM star_hipparcos
WHERE mag IS NOT NULL
AND rarad IS NOT NULL
AND decrad IS NOT NULL
AND mag <= 6.0
ORDER BY mag ASC"
);
while ($row = $stmtStars->fetch()) {
$chartStarData[] = [
'hip' => $row['hip'] !== null ? (int) $row['hip'] : null,
'proper' => trim((string) ($row['proper'] ?? '')),
'bayer' => trim((string) ($row['bayer'] ?? '')),
'con' => trim((string) ($row['con'] ?? '')),
'spect' => trim((string) ($row['spect'] ?? '')),
'ra' => (float) $row['rarad'],
'dec' => (float) $row['decrad'],
'mag' => (float) $row['mag'],
'ci' => $row['ci'] !== null ? (float) $row['ci'] : null,
];
}
$constellationIndex = [];
$ensureConstellation = static function (string $id) use (&$constellationIndex): void {
if (!isset($constellationIndex[$id])) {
$constellationIndex[$id] = [
'id' => $id,
'lines' => [],
];
}
};
$lineStmt = $pdo->query(
"SELECT cl.constellation_id, cl.line_index, clp.point_index, clp.hip_number
FROM constellation_lines cl
INNER JOIN constellation_line_points clp ON clp.line_id = cl.id
ORDER BY cl.constellation_id, cl.line_index, clp.point_index"
);
foreach ($lineStmt as $row) {
$constellationId = (string) $row['constellation_id'];
$lineIndex = (int) $row['line_index'];
$ensureConstellation($constellationId);
$constellationIndex[$constellationId]['lines'][$lineIndex] ??= [];
$constellationIndex[$constellationId]['lines'][$lineIndex][] = (int) $row['hip_number'];
}
foreach ($constellationIndex as &$constellation) {
if ($constellation['lines']) {
ksort($constellation['lines']);
$constellation['lines'] = array_values($constellation['lines']);
}
}
unset($constellation);
$chartConstellationLines = array_values($constellationIndex);
$lineStarStmt = $pdo->query(
"SELECT DISTINCT sh.hip, sh.proper, sh.bayer, sh.con, sh.spect, sh.mag, sh.ci, sh.rarad, sh.decrad
FROM constellation_line_points clp
INNER JOIN star_hipparcos sh ON sh.hip = clp.hip_number
WHERE sh.rarad IS NOT NULL
AND sh.decrad IS NOT NULL"
);
while ($row = $lineStarStmt->fetch()) {
$chartLineStarData[] = [
'hip' => $row['hip'] !== null ? (int) $row['hip'] : null,
'proper' => trim((string) ($row['proper'] ?? '')),
'bayer' => trim((string) ($row['bayer'] ?? '')),
'con' => trim((string) ($row['con'] ?? '')),
'spect' => trim((string) ($row['spect'] ?? '')),
'ra' => (float) $row['rarad'],
'dec' => (float) $row['decrad'],
'mag' => $row['mag'] !== null ? (float) $row['mag'] : 99.0,
'ci' => $row['ci'] !== null ? (float) $row['ci'] : null,
];
}
} catch (Throwable $e) {
$defaultLocation = null;
$favoriteMinorplanetOptions = [];
$favoriteCometOptions = [];
$minorplanetPayloads = [];
$cometPayloads = [];
$chartStarData = [];
$chartLineStarData = [];
$chartConstellationLines = [];
}
}
@@ -343,6 +434,8 @@ if (is_array($defaultLocation) && $defaultLocation !== []) {
$defaultLocationName = $pageLocation['name'] ?? '';
$errorMessage = '';
$ephemerisRows = [];
$ephemerisChartRowsJson = '[]';
$ephemerisWindowStartIso = null;
$exportFormat = (string) ($_POST['export_format'] ?? '');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
@@ -350,8 +443,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$errorMessage = 'Kein gültiger Standard-Standort verfügbar. Bitte zuerst in den Einstellungen einen Standard-Standort setzen.';
} elseif (false && (str_starts_with($selectedBody, 'comet:') || str_starts_with($selectedBody, 'minorplanet:'))) {
$errorMessage = 'Favoriten-Kometen und Favoriten-Kleinplaneten sind jetzt in der Auswahl sichtbar. Die eigentliche Ephemeriden-Berechnung dafür binden wir als Nächstes an.';
} elseif (!ctype_digit($rangeValueInput) || (int) $rangeValueInput < 1 || (int) $rangeValueInput > 31) {
$errorMessage = 'Der Zeitraum muss als ganze Zahl zwischen 1 und 31 angegeben werden.';
} elseif (!ctype_digit($rangeValueInput) || (int) $rangeValueInput < 1 || (int) $rangeValueInput > 100) {
$errorMessage = 'Der Zeitraum muss als ganze Zahl zwischen 1 und 100 angegeben werden.';
} else {
$pythonAction = 'planet_ephemeris';
$pythonArgs = [
@@ -403,6 +496,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
);
} else {
$ephemerisRows = $pythonResult['data']['rows'] ?? [];
$ephemerisWindowStartIso = isset($pythonResult['data']['window']['local_start'])
? (string) $pythonResult['data']['window']['local_start']
: null;
if (!is_array($ephemerisRows)) {
$ephemerisRows = [];
}
@@ -442,6 +538,45 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
if ($ephemerisRows !== []) {
$chartRows = [];
foreach ($ephemerisRows as $row) {
$azimuthDeg = isset($row['azimuth_deg']) ? (float) $row['azimuth_deg'] : null;
$altitudeDeg = isset($row['altitude_deg']) ? (float) $row['altitude_deg'] : null;
if ($azimuthDeg === null || $altitudeDeg === null) {
continue;
}
$chartRows[] = [
'object_name' => (string) ($row['object_name'] ?? ''),
'date_local' => (string) ($row['date_local'] ?? ''),
'time_local' => (string) ($row['time_local'] ?? ''),
'local_iso' => (string) ($row['local_iso'] ?? ''),
'ra_decimal_hours' => isset($row['ra_decimal_hours']) ? (float) $row['ra_decimal_hours'] : null,
'dec_decimal_deg' => isset($row['dec_decimal_deg']) ? (float) $row['dec_decimal_deg'] : null,
'azimuth_deg' => $azimuthDeg,
'altitude_deg' => $altitudeDeg,
];
}
$ephemerisChartRowsJson = json_encode($chartRows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '[]';
$ephemerisChartConfigJson = json_encode([
'location' => [
'name' => $defaultLocationName,
'latitude' => $pageLocation['latitude'] ?? null,
'longitude' => $pageLocation['longitude'] ?? null,
'elevation' => $pageLocation['elevation'] ?? 0.0,
'timezone' => $pageLocation['timezone'] ?? 'Europe/Berlin',
],
'utcTimeIso' => $ephemerisWindowStartIso,
'starLimit' => 4.4,
'starLabelLimit' => 2.0,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: 'null';
$ephemerisStarDataJson = json_encode($chartStarData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '[]';
$ephemerisLineStarDataJson = json_encode($chartLineStarData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '[]';
$ephemerisConstellationLinesJson = json_encode($chartConstellationLines, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '[]';
}
require __DIR__ . '/header.php';
?>
@@ -483,6 +618,13 @@ require __DIR__ . '/header.php';
align-items: center;
}
.ephemeriden-results-actions {
display: flex;
align-items: center;
gap: 0.65rem;
flex-wrap: wrap;
}
.ephemeriden-message {
margin-bottom: 0.9rem;
padding: 0.85rem 1rem;
@@ -525,6 +667,55 @@ require __DIR__ . '/header.php';
white-space: normal;
}
.ephemeriden-chart-card[hidden] {
display: none;
}
.ephemeriden-chart-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 0.9rem;
}
.ephemeriden-chart-note {
color: var(--text-dim);
font-size: 0.92rem;
}
.ephemeriden-chart-controls {
display: flex;
align-items: end;
gap: 0.8rem;
flex-wrap: wrap;
}
.ephemeriden-chart-controls .form-group {
min-width: 220px;
margin: 0;
}
.ephemeriden-chart-stage {
display: flex;
justify-content: center;
width: 100%;
}
.ephemeriden-chart-stage canvas {
width: 100%;
max-width: 760px;
height: auto;
display: block;
}
.ephemeriden-chart-legend {
margin-top: 0.9rem;
color: var(--text-dim);
font-size: 0.9rem;
}
@media (max-width: 900px) {
.ephemeriden-fields-row {
grid-template-columns: repeat(2, minmax(180px, 1fr));
@@ -582,7 +773,7 @@ require __DIR__ . '/header.php';
id="ephemeridenRangeValue"
name="range_value"
min="1"
max="31"
max="100"
step="1"
value="<?= ephemeriden_h($rangeValueInput) ?>"
inputmode="numeric"
@@ -615,14 +806,17 @@ require __DIR__ . '/header.php';
</div>
</div>
<?php if ($ephemerisRows !== []): ?>
<form method="post" class="ephemeriden-export-form">
<input type="hidden" name="body" value="<?= ephemeriden_h($selectedBody) ?>">
<input type="hidden" name="interval_minutes" value="<?= ephemeriden_h($selectedInterval) ?>">
<input type="hidden" name="range_value" value="<?= ephemeriden_h($rangeValueInput) ?>">
<input type="hidden" name="range_unit" value="<?= ephemeriden_h($selectedRangeUnit) ?>">
<input type="hidden" name="export_format" value="csv">
<button type="submit" class="btn btn-secondary">Export CSV</button>
</form>
<div class="ephemeriden-results-actions">
<button type="button" class="btn btn-secondary" id="ephemeridenChartToggle" aria-expanded="false" aria-controls="ephemeridenChartCard">Karte anzeigen</button>
<form method="post" class="ephemeriden-export-form">
<input type="hidden" name="body" value="<?= ephemeriden_h($selectedBody) ?>">
<input type="hidden" name="interval_minutes" value="<?= ephemeriden_h($selectedInterval) ?>">
<input type="hidden" name="range_value" value="<?= ephemeriden_h($rangeValueInput) ?>">
<input type="hidden" name="range_unit" value="<?= ephemeriden_h($selectedRangeUnit) ?>">
<input type="hidden" name="export_format" value="csv">
<button type="submit" class="btn btn-secondary">Export CSV</button>
</form>
</div>
<?php endif; ?>
</div>
@@ -670,4 +864,672 @@ require __DIR__ . '/header.php';
</div>
</section>
<?php if ($ephemerisRows !== []): ?>
<section class="card mt-md ephemeriden-chart-card" id="ephemeridenChartCard" hidden>
<div class="ephemeriden-chart-head">
<div>
<h2>Ephemeridenkarte</h2>
<div class="ephemeriden-chart-note">
Polare Himmelsansicht für <strong><?= ephemeriden_h((string) ($bodyOptions[$selectedBody] ?? 'Objekt')) ?></strong> am Standort <strong><?= ephemeriden_h($defaultLocationName !== '' ? $defaultLocationName : 'nicht verfügbar') ?></strong>
</div>
</div>
<div class="ephemeriden-chart-controls">
<div class="form-group">
<label for="ephemeridenChartPoint">Kartenzeitpunkt</label>
<select id="ephemeridenChartPoint">
<?php foreach ($ephemerisRows as $rowIndex => $row): ?>
<option value="<?= ephemeriden_h((string) $rowIndex) ?>"><?= ephemeriden_h((string) ($row['date_local'] ?? '') . ' ' . (string) ($row['time_local'] ?? '')) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="ephemeridenChartTime">Kartenuhrzeit</label>
<input type="time" id="ephemeridenChartTime" step="60" value="<?= ephemeriden_h((string) ($ephemerisRows[0]['time_local'] ?? '00:00')) ?>">
</div>
<div class="form-group">
<label for="ephemeridenChartLabelStep">Beschriftung</label>
<select id="ephemeridenChartLabelStep">
<?php foreach ([2, 3, 4, 5, 6, 8, 10] as $labelStepOption): ?>
<option value="<?= ephemeriden_h((string) $labelStepOption) ?>"<?= $labelStepOption === 4 ? ' selected' : '' ?>>jeder <?= ephemeriden_h((string) $labelStepOption) ?>. Punkt</option>
<?php endforeach; ?>
</select>
</div>
<div class="ephemeriden-submit-group">
<button type="button" class="btn btn-secondary" id="ephemeridenChartApply">Karte neu berechnen</button>
</div>
</div>
</div>
<div class="ephemeriden-chart-stage">
<canvas id="ephemeridenPolarCanvas" width="900" height="900" aria-label="Polarkarte der berechneten Ephemeriden"></canvas>
</div>
<div class="ephemeriden-chart-legend">
Der äußere Kreis ist der Horizont, die Mitte ist der Zenit. Die Bahn wird in der Reihenfolge der Ephemeridenpunkte eingezeichnet.
</div>
</section>
<?php endif; ?>
<?php if ($ephemerisRows !== []): ?>
<script src="js/astronomy.browser.min.js"></script>
<script>
const EPHEMERIDEN_CHART_ROWS = <?= $ephemerisChartRowsJson ?>;
const EPHEMERIDEN_CHART_CONFIG = <?= $ephemerisChartConfigJson ?>;
const EPHEMERIDEN_CHART_STARS = <?= $ephemerisStarDataJson ?>;
const EPHEMERIDEN_CHART_LINE_STARS = <?= $ephemerisLineStarDataJson ?>;
const EPHEMERIDEN_CHART_CONSTELLATIONS = <?= $ephemerisConstellationLinesJson ?>;
(function () {
const toggleButton = document.getElementById('ephemeridenChartToggle');
const chartCard = document.getElementById('ephemeridenChartCard');
const canvas = document.getElementById('ephemeridenPolarCanvas');
const pointSelect = document.getElementById('ephemeridenChartPoint');
const timeInput = document.getElementById('ephemeridenChartTime');
const labelStepSelect = document.getElementById('ephemeridenChartLabelStep');
const applyButton = document.getElementById('ephemeridenChartApply');
if (!toggleButton || !chartCard || !canvas || !pointSelect || !timeInput || !labelStepSelect || !applyButton || !Array.isArray(EPHEMERIDEN_CHART_ROWS) || EPHEMERIDEN_CHART_ROWS.length === 0) {
return;
}
const ctx = canvas.getContext('2d');
const observer = window.Astronomy && EPHEMERIDEN_CHART_CONFIG && EPHEMERIDEN_CHART_CONFIG.location
? new Astronomy.Observer(
Number(EPHEMERIDEN_CHART_CONFIG.location.latitude),
Number(EPHEMERIDEN_CHART_CONFIG.location.longitude),
Number(EPHEMERIDEN_CHART_CONFIG.location.elevation || 0)
)
: null;
let occupiedBoxes = [];
const starByHip = new Map();
const constellationHipSet = new Set();
const displayStarMap = new Map();
for (const star of EPHEMERIDEN_CHART_LINE_STARS) {
if (Number.isFinite(Number(star.hip))) {
starByHip.set(Number(star.hip), star);
}
}
for (const star of EPHEMERIDEN_CHART_STARS) {
if (Number.isFinite(Number(star.hip))) {
displayStarMap.set(Number(star.hip), star);
} else {
displayStarMap.set(`star-${displayStarMap.size}`, star);
}
}
for (const constellation of EPHEMERIDEN_CHART_CONSTELLATIONS) {
if (!Array.isArray(constellation.lines)) {
continue;
}
for (const segment of constellation.lines) {
if (!Array.isArray(segment)) {
continue;
}
for (const hip of segment) {
if (Number.isFinite(Number(hip))) {
constellationHipSet.add(Number(hip));
}
}
}
}
for (const hip of constellationHipSet) {
if (!displayStarMap.has(hip) && starByHip.has(hip)) {
displayStarMap.set(hip, starByHip.get(hip));
}
}
function projectPoint(azimuthDeg, altitudeDeg, radius, centerX, centerY) {
if (!Number.isFinite(azimuthDeg) || !Number.isFinite(altitudeDeg) || altitudeDeg < 0) {
return null;
}
const rho = (90 - altitudeDeg) / 90;
const azimuthRad = azimuthDeg * Math.PI / 180;
return {
x: centerX - rho * radius * Math.sin(azimuthRad),
y: centerY - rho * radius * Math.cos(azimuthRad),
};
}
function clamp01(value) {
return Math.max(0, Math.min(1, value));
}
function resetLabelBoxes() {
occupiedBoxes = [];
}
function getSelectedIndex() {
const rawValue = Number(pointSelect.value);
if (!Number.isInteger(rawValue) || rawValue < 0 || rawValue >= EPHEMERIDEN_CHART_ROWS.length) {
return 0;
}
return rawValue;
}
function getSelectedRow() {
return EPHEMERIDEN_CHART_ROWS[getSelectedIndex()] || null;
}
function extractRowDateParts(localIso) {
const isoText = String(localIso || '');
const match = isoText.match(/^(\d{4}-\d{2}-\d{2})T\d{2}:\d{2}(?::\d{2})?(.+)$/);
if (!match) {
return null;
}
return {
datePart: match[1],
offsetPart: match[2],
};
}
function getChartTimeValue() {
const value = String(timeInput.value || '').trim();
return /^\d{2}:\d{2}$/.test(value) ? value : null;
}
function getLabelStepValue() {
const value = Number(labelStepSelect.value);
return Number.isInteger(value) && value >= 1 ? value : 4;
}
function syncChartTimeFromSelectedRow() {
const selectedRow = getSelectedRow();
if (!selectedRow || !selectedRow.time_local) {
return;
}
timeInput.value = String(selectedRow.time_local).slice(0, 5);
}
function getChartDate() {
const selectedRow = getSelectedRow();
const timeValue = getChartTimeValue();
if (!selectedRow || !selectedRow.local_iso || !timeValue) {
return null;
}
const dateParts = extractRowDateParts(selectedRow.local_iso);
if (!dateParts) {
return null;
}
const chartDate = new Date(`${dateParts.datePart}T${timeValue}:00${dateParts.offsetPart}`);
return Number.isNaN(chartDate.getTime()) ? null : chartDate;
}
function drawLabel(text, x, y, options = {}) {
if (!text) {
return false;
}
const font = options.font || '600 11px Rajdhani, sans-serif';
const color = options.color || 'rgba(232, 239, 255, 0.82)';
const align = options.align || 'left';
const baseline = options.baseline || 'middle';
const padding = options.padding ?? 2;
const rotationDeg = Number(options.rotationDeg || 0);
const backgroundColor = options.backgroundColor || '';
const force = options.force === true;
ctx.save();
ctx.font = font;
const metrics = ctx.measureText(text);
const textWidth = metrics.width;
const ascent = metrics.actualBoundingBoxAscent || 8;
const descent = metrics.actualBoundingBoxDescent || 4;
const textHeight = ascent + descent;
let localX = 0;
if (align === 'center') {
localX = -textWidth / 2;
} else if (align === 'right') {
localX = -textWidth;
}
let localY = -ascent;
if (baseline === 'middle') {
localY = -(ascent + descent) / 2;
} else if (baseline === 'bottom' || baseline === 'alphabetic') {
localY = -ascent - descent;
}
const rectX = localX - padding;
const rectY = localY - padding;
const rectWidth = textWidth + padding * 2;
const rectHeight = textHeight + padding * 2;
const rotationRad = rotationDeg * Math.PI / 180;
const corners = [
{ x: rectX, y: rectY },
{ x: rectX + rectWidth, y: rectY },
{ x: rectX + rectWidth, y: rectY + rectHeight },
{ x: rectX, y: rectY + rectHeight },
].map((corner) => ({
x: x + corner.x * Math.cos(rotationRad) - corner.y * Math.sin(rotationRad),
y: y + corner.x * Math.sin(rotationRad) + corner.y * Math.cos(rotationRad),
}));
const box = {
x: Math.min(...corners.map((corner) => corner.x)),
y: Math.min(...corners.map((corner) => corner.y)),
width: Math.max(...corners.map((corner) => corner.x)) - Math.min(...corners.map((corner) => corner.x)),
height: Math.max(...corners.map((corner) => corner.y)) - Math.min(...corners.map((corner) => corner.y)),
};
const overlaps = occupiedBoxes.some((other) =>
box.x < other.x + other.width &&
box.x + box.width > other.x &&
box.y < other.y + other.height &&
box.y + box.height > other.y
);
if (!overlaps || force) {
occupiedBoxes.push(box);
ctx.translate(x, y);
if (rotationDeg !== 0) {
ctx.rotate(rotationRad);
}
ctx.textAlign = align;
ctx.textBaseline = baseline;
if (backgroundColor !== '') {
ctx.fillStyle = backgroundColor;
ctx.fillRect(rectX, rectY, rectWidth, rectHeight);
}
ctx.fillStyle = color;
ctx.fillText(text, 0, 0);
ctx.restore();
return true;
}
ctx.restore();
return false;
}
function starHorizon(star) {
const chartDate = getChartDate();
if (!window.Astronomy || !observer || !chartDate) {
return null;
}
const raHours = Number(star.ra) * (12 / Math.PI);
const decDeg = Number(star.dec) * (180 / Math.PI);
return Astronomy.Horizon(chartDate, observer, raHours, decDeg, 'normal');
}
function ephemerisHorizon(row) {
const chartDate = getChartDate();
if (!window.Astronomy || !observer || !chartDate) {
return null;
}
const raHours = Number(row.ra_decimal_hours);
const decDeg = Number(row.dec_decimal_deg);
if (!Number.isFinite(raHours) || !Number.isFinite(decDeg)) {
return null;
}
return Astronomy.Horizon(chartDate, observer, raHours, decDeg, 'normal');
}
function starColor(star) {
if (typeof star.ci === 'number') {
if (star.ci < -0.1) return '#b9d4ff';
if (star.ci < 0.3) return '#dbe7ff';
if (star.ci < 0.7) return '#fff6e0';
if (star.ci < 1.1) return '#ffe0b5';
return '#ffc98a';
}
const spect = String(star.spect || '').trim().toUpperCase();
if (spect.startsWith('O') || spect.startsWith('B')) return '#b9d4ff';
if (spect.startsWith('A')) return '#dbe7ff';
if (spect.startsWith('F') || spect.startsWith('G')) return '#fff6e0';
if (spect.startsWith('K')) return '#ffd4a0';
if (spect.startsWith('M')) return '#ffbd85';
return '#f4f7ff';
}
function getStarLabel(star) {
const proper = String(star.proper || '').trim();
if (proper !== '') {
return proper;
}
const bayer = String(star.bayer || '').trim();
const con = String(star.con || '').trim();
if (bayer !== '') {
return con !== '' ? `${bayer} ${con}` : bayer;
}
return '';
}
function drawStars(centerX, centerY, radius) {
if (!window.Astronomy || !observer || !getChartDate() || !Array.isArray(EPHEMERIDEN_CHART_STARS)) {
return;
}
for (const star of displayStarMap.values()) {
const hip = Number(star.hip);
const isConstellationStar = Number.isFinite(hip) && constellationHipSet.has(hip);
if (!isConstellationStar && Number(star.mag) > Number(EPHEMERIDEN_CHART_CONFIG.starLimit || 4.4)) {
continue;
}
const hor = starHorizon(star);
if (!hor || hor.altitude < 0) {
continue;
}
const pt = projectPoint(hor.azimuth, hor.altitude, radius, centerX, centerY);
if (!pt) {
continue;
}
const magRange = Math.max(0.5, Number(EPHEMERIDEN_CHART_CONFIG.starLimit || 4.4) + 1.6);
const effectiveMagnitude = isConstellationStar
? Math.min(Number(star.mag), Number(EPHEMERIDEN_CHART_CONFIG.starLimit || 4.4) + 0.35)
: Number(star.mag);
const brightness = clamp01((Number(EPHEMERIDEN_CHART_CONFIG.starLimit || 4.4) - effectiveMagnitude + 1.6) / magRange);
const starRadius = (isConstellationStar ? 0.85 : 0.65) + Math.pow(brightness, 1.9) * 5.2;
const glowRadius = starRadius * (effectiveMagnitude <= 1 ? 2.8 : 1.9);
const color = starColor(star);
const glow = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, glowRadius);
glow.addColorStop(0, color + '66');
glow.addColorStop(0.35, color + '22');
glow.addColorStop(1, color + '00');
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(pt.x, pt.y, glowRadius, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(pt.x, pt.y, starRadius, 0, Math.PI * 2);
ctx.fill();
if (Number(star.mag) <= 0.8) {
ctx.strokeStyle = color + '44';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(pt.x - starRadius * 1.35, pt.y);
ctx.lineTo(pt.x + starRadius * 1.35, pt.y);
ctx.moveTo(pt.x, pt.y - starRadius * 1.35);
ctx.lineTo(pt.x, pt.y + starRadius * 1.35);
ctx.stroke();
}
if (Number(star.mag) <= Number(EPHEMERIDEN_CHART_CONFIG.starLabelLimit || 2.0)) {
const label = getStarLabel(star);
if (label !== '') {
drawLabel(label, pt.x + starRadius + 5, pt.y - 1, {
font: '600 11px Rajdhani, sans-serif',
color: 'rgba(232, 239, 255, 0.82)',
align: 'left',
baseline: 'middle',
padding: 2,
});
}
}
}
}
function drawConstellationLines(centerX, centerY, radius) {
if (!window.Astronomy || !observer || !getChartDate() || !Array.isArray(EPHEMERIDEN_CHART_CONSTELLATIONS)) {
return;
}
for (const constellation of EPHEMERIDEN_CHART_CONSTELLATIONS) {
if (!Array.isArray(constellation.lines)) {
continue;
}
for (const segment of constellation.lines) {
if (!Array.isArray(segment) || segment.length < 2) {
continue;
}
const points = [];
for (const hip of segment) {
const star = starByHip.get(Number(hip));
if (!star) {
continue;
}
const hor = starHorizon(star);
if (!hor || hor.altitude < 0) {
continue;
}
const pt = projectPoint(hor.azimuth, hor.altitude, radius, centerX, centerY);
if (!pt) {
continue;
}
points.push(pt);
}
if (points.length < 2) {
continue;
}
ctx.save();
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.strokeStyle = 'rgba(210, 214, 224, 0.16)';
ctx.lineWidth = 2.1;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let index = 1; index < points.length; index += 1) {
ctx.lineTo(points[index].x, points[index].y);
}
ctx.stroke();
ctx.strokeStyle = 'rgba(36, 40, 48, 0.34)';
ctx.lineWidth = 0.95;
ctx.beginPath();
ctx.moveTo(points[0].x + 0.18, points[0].y - 0.12);
for (let index = 1; index < points.length; index += 1) {
const jitterX = index % 2 === 0 ? 0.22 : -0.12;
const jitterY = index % 2 === 0 ? -0.14 : 0.2;
ctx.lineTo(points[index].x + jitterX, points[index].y + jitterY);
}
ctx.stroke();
ctx.restore();
}
}
}
function resizeCanvas() {
const cssSize = Math.max(320, Math.min(canvas.parentElement.clientWidth, 760));
const ratio = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(cssSize * ratio);
canvas.height = Math.round(cssSize * ratio);
canvas.style.width = cssSize + 'px';
canvas.style.height = cssSize + 'px';
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(ratio, ratio);
drawChart();
}
function drawCircleLabel(text, x, y) {
ctx.save();
ctx.font = '12px Space Mono, monospace';
ctx.fillStyle = 'rgba(232, 239, 255, 0.76)';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, x, y);
ctx.restore();
}
function drawChart() {
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const centerX = width / 2;
const centerY = height / 2;
const radius = Math.min(width, height) * 0.43;
const selectedRow = getSelectedRow();
resetLabelBoxes();
ctx.clearRect(0, 0, width, height);
const background = ctx.createRadialGradient(centerX, centerY, radius * 0.05, centerX, centerY, radius);
background.addColorStop(0, 'rgba(8, 12, 22, 0.98)');
background.addColorStop(0.68, 'rgba(5, 10, 18, 0.94)');
background.addColorStop(1, 'rgba(4, 8, 15, 0.98)');
ctx.fillStyle = background;
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = 'rgba(130, 170, 255, 0.18)';
ctx.lineWidth = 1;
[0, 30, 60].forEach((altitude) => {
const ringRadius = radius * ((90 - altitude) / 90);
ctx.beginPath();
ctx.arc(centerX, centerY, ringRadius, 0, Math.PI * 2);
ctx.stroke();
drawCircleLabel(`${altitude}°`, centerX, centerY - ringRadius - 12);
});
ctx.beginPath();
ctx.moveTo(centerX, centerY - radius);
ctx.lineTo(centerX, centerY + radius);
ctx.moveTo(centerX - radius, centerY);
ctx.lineTo(centerX + radius, centerY);
ctx.strokeStyle = 'rgba(130, 170, 255, 0.14)';
ctx.stroke();
drawCircleLabel('N', centerX, centerY - radius - 20);
drawCircleLabel('O', centerX + radius + 20, centerY);
drawCircleLabel('S', centerX, centerY + radius + 20);
drawCircleLabel('W', centerX - radius - 20, centerY);
drawCircleLabel('Zenit', centerX, centerY);
drawConstellationLines(centerX, centerY, radius);
drawStars(centerX, centerY, radius);
const visiblePoints = EPHEMERIDEN_CHART_ROWS
.map((row, rowIndex) => {
const horizon = ephemerisHorizon(row);
const azimuthDeg = horizon ? Number(horizon.azimuth) : Number(row.azimuth_deg);
const altitudeDeg = horizon ? Number(horizon.altitude) : Number(row.altitude_deg);
return {
index: rowIndex,
label: `${row.date_local}`,
azimuthDeg,
altitudeDeg,
point: projectPoint(azimuthDeg, altitudeDeg, radius, centerX, centerY),
};
})
.filter((entry) => entry.point !== null);
if (visiblePoints.length === 0) {
ctx.save();
ctx.font = '14px Space Mono, monospace';
ctx.fillStyle = 'rgba(232, 239, 255, 0.72)';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('Für die berechneten Punkte liegt das Objekt unter dem Horizont.', centerX, centerY);
ctx.restore();
return;
}
ctx.beginPath();
visiblePoints.forEach((entry, index) => {
if (index === 0) {
ctx.moveTo(entry.point.x, entry.point.y);
} else {
ctx.lineTo(entry.point.x, entry.point.y);
}
});
ctx.strokeStyle = 'rgba(246, 206, 97, 0.92)';
ctx.lineWidth = 2.6;
ctx.shadowColor = 'rgba(246, 206, 97, 0.32)';
ctx.shadowBlur = 10;
ctx.stroke();
ctx.shadowBlur = 0;
const labelStep = getLabelStepValue();
resetLabelBoxes();
visiblePoints.forEach((entry, index) => {
const isFirst = index === 0;
const isLast = index === visiblePoints.length - 1;
const markerRadius = isFirst || isLast ? 5 : 3.5;
ctx.beginPath();
ctx.arc(entry.point.x, entry.point.y, markerRadius, 0, Math.PI * 2);
ctx.fillStyle = isFirst ? '#7fd8ff' : (isLast ? '#ffb36b' : 'rgba(255, 236, 190, 0.96)');
ctx.fill();
if (isFirst || isLast || (index % labelStep === 0)) {
drawLabel(entry.label, entry.point.x + 10, entry.point.y - 8, {
font: '11px Space Mono, monospace',
color: 'rgba(232, 239, 255, 0.88)',
align: 'left',
baseline: 'bottom',
padding: 4,
rotationDeg: -45,
backgroundColor: 'rgba(7, 10, 18, 0.82)',
force: true,
});
}
});
if (selectedRow) {
ctx.save();
ctx.font = '600 12px Rajdhani, sans-serif';
ctx.fillStyle = 'rgba(232, 239, 255, 0.78)';
ctx.textAlign = 'right';
ctx.textBaseline = 'top';
const chartTimeValue = getChartTimeValue() || String(selectedRow.time_local || '').slice(0, 5);
ctx.fillText(`Sternhimmel für ${selectedRow.date_local} ${chartTimeValue}`, width - 18, 16);
ctx.restore();
}
}
toggleButton.addEventListener('click', () => {
const willOpen = chartCard.hidden;
chartCard.hidden = !willOpen;
toggleButton.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
toggleButton.textContent = willOpen ? 'Karte ausblenden' : 'Karte anzeigen';
if (willOpen) {
resizeCanvas();
chartCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
window.addEventListener('resize', () => {
if (!chartCard.hidden) {
resizeCanvas();
}
});
pointSelect.addEventListener('change', () => {
syncChartTimeFromSelectedRow();
});
applyButton.addEventListener('click', () => {
if (!chartCard.hidden) {
drawChart();
}
});
syncChartTimeFromSelectedRow();
})();
</script>
<?php endif; ?>
<?php require __DIR__ . '/footer.php'; ?>
+16
View File
@@ -347,6 +347,14 @@ def custom_altitude_deg(body_type: str, payload: dict, observer: astronomy.Obser
return float(horizontal.lat)
def custom_horizontal_coordinates(body_type: str, payload: dict, observer: astronomy.Observer, time_value: astronomy.Time) -> tuple[float, float]:
topocentric_vector = custom_topocentric_vector(body_type, payload, observer, time_value)
rotation = astronomy.Rotation_EQJ_HOR(time_value, observer)
horizontal_vector = astronomy.RotateVector(rotation, topocentric_vector)
horizontal = astronomy.HorizonFromVector(horizontal_vector, astronomy.Refraction.Normal)
return float(horizontal.lon), float(horizontal.lat)
def refine_custom_event(
body_type: str,
payload: dict,
@@ -509,14 +517,18 @@ def action_planet_ephemeris(args: list[str]) -> dict:
def row_builder(current_local: datetime, time_value: astronomy.Time, rise_label: str | None, set_label: str | None) -> dict:
eq = astronomy.Equator(body, time_value, observer, False, True)
horizontal = astronomy.Horizon(time_value, observer, float(eq.ra), float(eq.dec), astronomy.Refraction.Normal)
return {
"object_name": label,
"date_local": current_local.strftime("%d.%m.%Y"),
"time_local": current_local.strftime("%H:%M"),
"local_iso": current_local.isoformat(),
"ra": format_ra_hours(float(eq.ra)),
"ra_decimal_hours": round(float(eq.ra), 8),
"dec": format_dec_deg(float(eq.dec)),
"dec_decimal_deg": round(float(eq.dec), 8),
"azimuth_deg": round(float(horizontal.azimuth), 6),
"altitude_deg": round(float(horizontal.altitude), 6),
"rise": rise_label,
"set": set_label,
}
@@ -595,6 +607,7 @@ def action_small_body_ephemeris(args: list[str]) -> dict:
def row_builder(current_local: datetime, time_value: astronomy.Time, rise_label: str | None, set_label: str | None) -> dict:
try:
topocentric_vector = custom_topocentric_vector(body_type, payload, observer, time_value)
azimuth_deg, altitude_deg = custom_horizontal_coordinates(body_type, payload, observer, time_value)
except Exception as exc:
fail("Die Ephemeriden konnten fuer dieses Objekt nicht berechnet werden.", extra={"details": str(exc), "object": object_label})
@@ -603,10 +616,13 @@ def action_small_body_ephemeris(args: list[str]) -> dict:
"object_name": object_label,
"date_local": current_local.strftime("%d.%m.%Y"),
"time_local": current_local.strftime("%H:%M"),
"local_iso": current_local.isoformat(),
"ra": format_ra_hours(float(eq.ra)),
"ra_decimal_hours": round(float(eq.ra), 8),
"dec": format_dec_deg(float(eq.dec)),
"dec_decimal_deg": round(float(eq.dec), 8),
"azimuth_deg": round(float(azimuth_deg), 6),
"altitude_deg": round(float(altitude_deg), 6),
"rise": rise_label,
"set": set_label,
}