Ephemeriden Karte verbessert

This commit is contained in:
Eskimue
2026-04-28 13:42:51 +02:00
parent 7990c9f321
commit dd0a390344
2 changed files with 126 additions and 48 deletions
+110 -43
View File
@@ -327,6 +327,8 @@ $intervalOptions = [
'360' => '6 Stunden',
'720' => '12 Stunden',
'1440' => '1 Tag',
'2880' => '2 Tage',
'4320' => '3 Tage',
'10080' => '7 Tage',
'20160' => '14 Tage',
'43200' => '30 Tage',
@@ -361,6 +363,12 @@ if ($rangeValueInput === '') {
$rangeValueInput = '7';
}
$startDtInput = trim((string) ($_POST['start_dt'] ?? ''));
// Validierung: muss leer sein (= jetzt) oder dem Format YYYY-MM-DDTHH:MM entsprechen
if ($startDtInput !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/', $startDtInput)) {
$startDtInput = '';
}
function ephemeriden_h(mixed $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
@@ -441,8 +449,6 @@ $exportFormat = (string) ($_POST['export_format'] ?? '');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($pageLocation === null || $pageLocation['latitude'] === null || $pageLocation['longitude'] === null) {
$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 > 100) {
$errorMessage = 'Der Zeitraum muss als ganze Zahl zwischen 1 und 100 angegeben werden.';
} else {
@@ -456,6 +462,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$selectedInterval,
$rangeValueInput,
$selectedRangeUnit,
$startDtInput,
];
if (isset($minorplanetPayloads[$selectedBody])) {
@@ -470,6 +477,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$selectedInterval,
$rangeValueInput,
$selectedRangeUnit,
$startDtInput,
];
} elseif (isset($cometPayloads[$selectedBody])) {
$pythonAction = 'small_body_ephemeris';
@@ -483,6 +491,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$selectedInterval,
$rangeValueInput,
$selectedRangeUnit,
$startDtInput,
];
}
@@ -767,7 +776,17 @@ require __DIR__ . '/header.php';
</div>
<div class="form-group">
<label for="ephemeridenRangeValue">Zeitraum ab heute</label>
<label for="ephemeridenStartDt">Startdatum &amp; Uhrzeit</label>
<input
type="datetime-local"
id="ephemeridenStartDt"
name="start_dt"
value="<?= ephemeriden_h($startDtInput) ?>"
>
</div>
<div class="form-group">
<label for="ephemeridenRangeValue">Zeitraum</label>
<input
type="number"
id="ephemeridenRangeValue"
@@ -813,6 +832,7 @@ require __DIR__ . '/header.php';
<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="start_dt" value="<?= ephemeriden_h($startDtInput) ?>">
<input type="hidden" name="export_format" value="csv">
<button type="submit" class="btn btn-secondary">Export CSV</button>
</form>
@@ -894,8 +914,17 @@ require __DIR__ . '/header.php';
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="ephemeridenChartLabelRotation">Textausrichtung</label>
<select id="ephemeridenChartLabelRotation">
<?php foreach (range(-165, 165, 15) as $deg): ?>
<option value="<?= ephemeriden_h((string) $deg) ?>"<?= $deg === -45 ? ' selected' : '' ?>><?= ephemeriden_h($deg > 0 ? "+$deg" : (string) $deg) ?>°</option>
<?php endforeach; ?>
</select>
</div>
<div class="ephemeriden-submit-group">
<button type="button" class="btn btn-secondary" id="ephemeridenChartApply">Karte neu berechnen</button>
<button type="button" class="btn btn-secondary" id="ephemeridenChartExport">Als PNG exportieren</button>
</div>
</div>
</div>
@@ -924,9 +953,14 @@ const EPHEMERIDEN_CHART_CONSTELLATIONS = <?= $ephemerisConstellationLinesJson ?>
const pointSelect = document.getElementById('ephemeridenChartPoint');
const timeInput = document.getElementById('ephemeridenChartTime');
const labelStepSelect = document.getElementById('ephemeridenChartLabelStep');
const labelRotationSelect = document.getElementById('ephemeridenChartLabelRotation');
const applyButton = document.getElementById('ephemeridenChartApply');
if (!toggleButton || !chartCard || !canvas || !pointSelect || !timeInput || !labelStepSelect || !applyButton || !Array.isArray(EPHEMERIDEN_CHART_ROWS) || EPHEMERIDEN_CHART_ROWS.length === 0) {
const logoImage = new Image();
logoImage.src = 'images/logoAM.png';
logoImage.onload = () => { if (!chartCard.hidden) { drawChart(); } };
if (!toggleButton || !chartCard || !canvas || !pointSelect || !timeInput || !labelStepSelect || !labelRotationSelect || !applyButton || !Array.isArray(EPHEMERIDEN_CHART_ROWS) || EPHEMERIDEN_CHART_ROWS.length === 0) {
return;
}
@@ -1039,6 +1073,11 @@ const EPHEMERIDEN_CHART_CONSTELLATIONS = <?= $ephemerisConstellationLinesJson ?>
return Number.isInteger(value) && value >= 1 ? value : 4;
}
function getLabelRotationValue() {
const value = Number(labelRotationSelect.value);
return Number.isFinite(value) ? value : 45;
}
function syncChartTimeFromSelectedRow() {
const selectedRow = getSelectedRow();
if (!selectedRow || !selectedRow.time_local) {
@@ -1210,7 +1249,7 @@ const EPHEMERIDEN_CHART_CONSTELLATIONS = <?= $ephemerisConstellationLinesJson ?>
return '';
}
function drawStars(centerX, centerY, radius) {
function drawStars(centerX, centerY, radius, scale) {
if (!window.Astronomy || !observer || !getChartDate() || !Array.isArray(EPHEMERIDEN_CHART_STARS)) {
return;
}
@@ -1238,7 +1277,7 @@ const EPHEMERIDEN_CHART_CONSTELLATIONS = <?= $ephemerisConstellationLinesJson ?>
? 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 starRadius = ((isConstellationStar ? 0.85 : 0.65) + Math.pow(brightness, 1.9) * 5.2) * scale;
const glowRadius = starRadius * (effectiveMagnitude <= 1 ? 2.8 : 1.9);
const color = starColor(star);
@@ -1258,7 +1297,7 @@ const EPHEMERIDEN_CHART_CONSTELLATIONS = <?= $ephemerisConstellationLinesJson ?>
if (Number(star.mag) <= 0.8) {
ctx.strokeStyle = color + '44';
ctx.lineWidth = 1;
ctx.lineWidth = scale;
ctx.beginPath();
ctx.moveTo(pt.x - starRadius * 1.35, pt.y);
ctx.lineTo(pt.x + starRadius * 1.35, pt.y);
@@ -1270,19 +1309,19 @@ const EPHEMERIDEN_CHART_CONSTELLATIONS = <?= $ephemerisConstellationLinesJson ?>
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',
drawLabel(label, pt.x + starRadius + 5 * scale, pt.y - 1, {
font: `600 ${Math.round(11 * scale)}px Rajdhani, sans-serif`,
color: 'rgba(232, 239, 255, 0.82)',
align: 'left',
baseline: 'middle',
padding: 2,
padding: 2 * scale,
});
}
}
}
}
function drawConstellationLines(centerX, centerY, radius) {
function drawConstellationLines(centerX, centerY, radius, scale) {
if (!window.Astronomy || !observer || !getChartDate() || !Array.isArray(EPHEMERIDEN_CHART_CONSTELLATIONS)) {
return;
}
@@ -1327,7 +1366,7 @@ const EPHEMERIDEN_CHART_CONSTELLATIONS = <?= $ephemerisConstellationLinesJson ?>
ctx.lineJoin = 'round';
ctx.strokeStyle = 'rgba(210, 214, 224, 0.16)';
ctx.lineWidth = 2.1;
ctx.lineWidth = 2.1 * scale;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let index = 1; index < points.length; index += 1) {
@@ -1336,7 +1375,7 @@ const EPHEMERIDEN_CHART_CONSTELLATIONS = <?= $ephemerisConstellationLinesJson ?>
ctx.stroke();
ctx.strokeStyle = 'rgba(36, 40, 48, 0.34)';
ctx.lineWidth = 0.95;
ctx.lineWidth = 0.95 * scale;
ctx.beginPath();
ctx.moveTo(points[0].x + 0.18, points[0].y - 0.12);
for (let index = 1; index < points.length; index += 1) {
@@ -1363,9 +1402,9 @@ const EPHEMERIDEN_CHART_CONSTELLATIONS = <?= $ephemerisConstellationLinesJson ?>
drawChart();
}
function drawCircleLabel(text, x, y) {
function drawCircleLabel(text, x, y, scale) {
ctx.save();
ctx.font = '12px Space Mono, monospace';
ctx.font = `${Math.round(12 * scale)}px Space Mono, monospace`;
ctx.fillStyle = 'rgba(232, 239, 255, 0.76)';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
@@ -1373,9 +1412,10 @@ const EPHEMERIDEN_CHART_CONSTELLATIONS = <?= $ephemerisConstellationLinesJson ?>
ctx.restore();
}
function drawChart() {
const width = canvas.clientWidth;
const height = canvas.clientHeight;
function drawChart(overrideSize) {
const width = overrideSize ?? canvas.clientWidth;
const height = overrideSize ?? canvas.clientHeight;
const scale = width / (canvas.clientWidth || width);
const centerX = width / 2;
const centerY = height / 2;
const radius = Math.min(width, height) * 0.43;
@@ -1383,6 +1423,8 @@ const EPHEMERIDEN_CHART_CONSTELLATIONS = <?= $ephemerisConstellationLinesJson ?>
resetLabelBoxes();
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#06080f';
ctx.fillRect(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)');
@@ -1394,13 +1436,13 @@ const EPHEMERIDEN_CHART_CONSTELLATIONS = <?= $ephemerisConstellationLinesJson ?>
ctx.fill();
ctx.strokeStyle = 'rgba(130, 170, 255, 0.18)';
ctx.lineWidth = 1;
ctx.lineWidth = scale;
[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);
drawCircleLabel(`${altitude}°`, centerX, centerY - ringRadius - 12 * scale, scale);
});
ctx.beginPath();
@@ -1411,13 +1453,13 @@ const EPHEMERIDEN_CHART_CONSTELLATIONS = <?= $ephemerisConstellationLinesJson ?>
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);
drawCircleLabel('N', centerX, centerY - radius - 20 * scale, scale);
drawCircleLabel('W', centerX + radius + 20 * scale, centerY, scale);
drawCircleLabel('S', centerX, centerY + radius + 20 * scale, scale);
drawCircleLabel('O', centerX - radius - 20 * scale, centerY, scale);
drawCircleLabel('Zenit', centerX, centerY, scale);
drawConstellationLines(centerX, centerY, radius, scale);
drawStars(centerX, centerY, radius, scale);
const visiblePoints = EPHEMERIDEN_CHART_ROWS
.map((row, rowIndex) => {
@@ -1455,46 +1497,45 @@ const EPHEMERIDEN_CHART_CONSTELLATIONS = <?= $ephemerisConstellationLinesJson ?>
}
});
ctx.strokeStyle = 'rgba(246, 206, 97, 0.92)';
ctx.lineWidth = 2.6;
ctx.lineWidth = 2.6 * scale;
ctx.shadowColor = 'rgba(246, 206, 97, 0.32)';
ctx.shadowBlur = 10;
ctx.shadowBlur = 10 * scale;
ctx.stroke();
ctx.shadowBlur = 0;
const labelStep = getLabelStepValue();
const labelRotation = getLabelRotationValue();
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.arc(entry.point.x, entry.point.y, 3.5 * scale, 0, Math.PI * 2);
ctx.fillStyle = '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',
drawLabel(entry.label, entry.point.x + 10 * scale, entry.point.y - 8 * scale, {
font: `${Math.round(11 * scale)}px Space Mono, monospace`,
color: 'rgba(232, 239, 255, 0.88)',
align: 'left',
baseline: 'bottom',
padding: 4,
rotationDeg: -45,
padding: 4 * scale,
rotationDeg: labelRotation,
backgroundColor: 'rgba(7, 10, 18, 0.82)',
force: true,
});
}
});
if (selectedRow) {
if (logoImage.complete && logoImage.naturalWidth > 0) {
const logoH = Math.round(width * 0.07);
const logoW = Math.round(logoH * logoImage.naturalWidth / logoImage.naturalHeight);
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.globalAlpha = 0.82;
ctx.drawImage(logoImage, width - logoW - 14, height - logoH - 14, logoW, logoH);
ctx.restore();
}
}
@@ -1527,6 +1568,32 @@ const EPHEMERIDEN_CHART_CONSTELLATIONS = <?= $ephemerisConstellationLinesJson ?>
}
});
const exportButton = document.getElementById('ephemeridenChartExport');
if (exportButton) {
exportButton.addEventListener('click', () => {
const exportSize = 2000;
const sw = canvas.width;
const sh = canvas.height;
canvas.width = exportSize;
canvas.height = exportSize;
ctx.setTransform(1, 0, 0, 1, 0, 0);
drawChart(exportSize);
const link = document.createElement('a');
link.download = 'ephemeriden.png';
link.href = canvas.toDataURL('image/png');
link.click();
canvas.width = sw;
canvas.height = sh;
ctx.setTransform(1, 0, 0, 1, 0, 0);
const ratio = Math.min(window.devicePixelRatio || 1, 2);
ctx.scale(ratio, ratio);
drawChart();
});
}
syncChartTimeFromSelectedRow();
})();
</script>
+16 -5
View File
@@ -419,13 +419,20 @@ def build_rows_response(
row_builder,
rise_set_builder,
action_name: str,
start_dt_str: str = "",
) -> dict:
try:
tz = ZoneInfo(timezone_name)
except Exception as exc:
fail("Zeitzone ist ungueltig.", extra={"details": str(exc)})
local_start = datetime.now(tz).replace(second=0, microsecond=0)
if start_dt_str:
try:
local_start = datetime.strptime(start_dt_str, "%Y-%m-%dT%H:%M").replace(tzinfo=tz)
except ValueError as exc:
fail("Startdatum ist ungueltig.", extra={"details": str(exc), "start_dt_str": start_dt_str})
else:
local_start = datetime.now(tz).replace(second=0, microsecond=0)
local_end = add_calendar_unit(local_start, range_value, range_unit)
rows = []
@@ -483,9 +490,9 @@ def build_rows_response(
def action_planet_ephemeris(args: list[str]) -> dict:
if len(args) != 8:
if len(args) not in {8, 9}:
fail(
"Aktion planet_ephemeris erwartet 8 Argumente: latitude longitude elevation timezone body intervalMinutes rangeValue rangeUnit",
"Aktion planet_ephemeris erwartet 8 oder 9 Argumente: latitude longitude elevation timezone body intervalMinutes rangeValue rangeUnit [startDt]",
extra={"argv": args},
)
@@ -502,6 +509,7 @@ def action_planet_ephemeris(args: list[str]) -> dict:
fail("Intervall oder Zeitraum ist ungueltig.", extra={"details": str(exc), "argv": args})
range_unit = str(args[7]).strip()
start_dt_str = str(args[8]).strip() if len(args) == 9 else ""
if interval_minutes <= 0:
fail("Intervall muss groesser als 0 sein.", extra={"interval_minutes": interval_minutes})
@@ -559,13 +567,14 @@ def action_planet_ephemeris(args: list[str]) -> dict:
row_builder=row_builder,
rise_set_builder=rise_set_builder,
action_name="planet_ephemeris",
start_dt_str=start_dt_str,
)
def action_small_body_ephemeris(args: list[str]) -> dict:
if len(args) != 9:
if len(args) not in {9, 10}:
fail(
"Aktion small_body_ephemeris erwartet 9 Argumente: latitude longitude elevation timezone bodyType payload intervalMinutes rangeValue rangeUnit",
"Aktion small_body_ephemeris erwartet 9 oder 10 Argumente: latitude longitude elevation timezone bodyType payload intervalMinutes rangeValue rangeUnit [startDt]",
extra={"argv": args},
)
@@ -588,6 +597,7 @@ def action_small_body_ephemeris(args: list[str]) -> dict:
fail("Intervall oder Zeitraum ist ungueltig.", extra={"details": str(exc), "argv": args})
range_unit = str(args[8]).strip()
start_dt_str = str(args[9]).strip() if len(args) == 10 else ""
if body_type not in {"minorplanet", "comet"}:
fail("Objekttyp ist ungueltig.", extra={"body_type": body_type})
@@ -649,6 +659,7 @@ def action_small_body_ephemeris(args: list[str]) -> dict:
row_builder=row_builder,
rise_set_builder=rise_set_builder,
action_name="small_body_ephemeris",
start_dt_str=start_dt_str,
)