From dd0a390344242e5086f6b0ac25b2740870ebc3d0 Mon Sep 17 00:00:00 2001 From: Eskimue Date: Tue, 28 Apr 2026 13:42:51 +0200 Subject: [PATCH] Ephemeriden Karte verbessert --- public/ephemeriden.php | 153 +++++++++++++++++++++++++---------- public/py/ephemeriden_api.py | 21 +++-- 2 files changed, 126 insertions(+), 48 deletions(-) diff --git a/public/ephemeriden.php b/public/ephemeriden.php index f001f42..ff9111e 100644 --- a/public/ephemeriden.php +++ b/public/ephemeriden.php @@ -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';
- + + +
+ +
+ + @@ -894,8 +914,17 @@ require __DIR__ . '/header.php';
+
+ + +
+
@@ -924,9 +953,14 @@ const EPHEMERIDEN_CHART_CONSTELLATIONS = 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 = 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 = 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 = ? 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = } }); 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 = } }); + 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(); })(); diff --git a/public/py/ephemeriden_api.py b/public/py/ephemeriden_api.py index 7234cf7..e34d287 100644 --- a/public/py/ephemeriden_api.py +++ b/public/py/ephemeriden_api.py @@ -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, )