From 23ad1973fbf517ba52c5f968012e1cef512e75d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 15 Sep 2026 20:14:04 +0200 Subject: [PATCH] Beobachtungsvorschlaege um Objektbilder und Namen erweitern --- config/skyview.sql | 1 + docs/BEOBACHTUNGSVORSCHLAEGE_FILTER.md | 2 +- public/ajax/observation_suggestions.php | 5 ++ public/beobachtungsvorschlaege.php | 29 ++++++++--- public/js/observation_suggestions.js | 58 +++++++++++++++++++++- public/observation_suggestions_helpers.php | 38 +++++++++++++- public/py/observation_suggestions_api.py | 4 +- 7 files changed, 125 insertions(+), 12 deletions(-) diff --git a/config/skyview.sql b/config/skyview.sql index dcb7def..0311f0d 100644 --- a/config/skyview.sql +++ b/config/skyview.sql @@ -270,6 +270,7 @@ CREATE TABLE IF NOT EXISTS `dso_objects` ( `cstar_names` text DEFAULT NULL, `identifiers` longtext DEFAULT NULL, `common_names` longtext DEFAULT NULL, + `common_names_german` longtext DEFAULT NULL, `ned_notes` longtext DEFAULT NULL, `openngc_notes` longtext DEFAULT NULL, `image` varchar(255) DEFAULT NULL, diff --git a/docs/BEOBACHTUNGSVORSCHLAEGE_FILTER.md b/docs/BEOBACHTUNGSVORSCHLAEGE_FILTER.md index 3e515da..4606bb1 100644 --- a/docs/BEOBACHTUNGSVORSCHLAEGE_FILTER.md +++ b/docs/BEOBACHTUNGSVORSCHLAEGE_FILTER.md @@ -33,7 +33,7 @@ Nur explizit zugeordnete DSO-Typen berücksichtigen. Insbesondere `Dup` (Dublett ## Größe und Helligkeit - Der Größenfilter gilt ausschließlich für die fünf Deep-Sky-Gruppen in der Tabelle, niemals für Einzelsterne, Doppelsterne, Veränderliche, Planeten, Mond oder Kometen. -- Vorgabe für die maximale Deep-Sky-Größe: 500 Bogensekunden; im Formular änderbar oder durch Leeren aufhebbar. +- Die maximale Deep-Sky-Größe wird im Formular in Bogenminuten eingegeben und ist auf 60′ begrenzt; ein leeres Feld hebt die jeweilige Grenze auf. Intern wird weiterhin in Bogensekunden gerechnet. - Als Größenmaß dient die große Achse `major_axis`: Bogenminuten × 60 = Bogensekunden. Der Import übernimmt diesen Wert unverändert; die Einheit ist in der [OpenNGC-Katalogbeschreibung](https://github.com/mattiaverga/OpenNGC/blob/master/NGC_guide.txt) dokumentiert. Fehlende oder nicht positive Größen werden bei aktiver Größengrenze ausgelassen. - Für Deep Sky die visuelle Helligkeit `v_mag` verwenden. Andere Filterbänder nicht stillschweigend als V-Helligkeit einsetzen. `cstar_v_mag` ist die Helligkeit des Zentralsterns, nicht des gesamten Nebels. - Vorgabe: 12 mag oder heller, also numerisch `v_mag <= 12`. diff --git a/public/ajax/observation_suggestions.php b/public/ajax/observation_suggestions.php index f37ef15..dc805a5 100644 --- a/public/ajax/observation_suggestions.php +++ b/public/ajax/observation_suggestions.php @@ -53,6 +53,11 @@ try { if (!is_string($value) || strlen($value) > 32) { observationReply(['ok' => false, 'error' => 'Ungültige Filterangabe.'], 422); } + if (in_array($key, ['object_size_min', 'object_size_max'], true) && $value !== '') { + if (is_numeric($value)) { + $value = (string) ((float) $value * 60); + } + } $filters[$key] = $value; } foreach (['object_types' => ['moon', 'planets', 'open_clusters', 'globular_clusters', 'galaxies', 'nebulae', diff --git a/public/beobachtungsvorschlaege.php b/public/beobachtungsvorschlaege.php index 856277a..302858c 100644 --- a/public/beobachtungsvorschlaege.php +++ b/public/beobachtungsvorschlaege.php @@ -46,6 +46,15 @@ $_SESSION['observation_csrf'] ??= bin2hex(random_bytes(32)); .observation-results-table td { vertical-align: top; } .observation-results-table small { display: block; color: var(--text-dim); max-width: 22rem; } .observation-results-table td:nth-child(n+3) { white-space: nowrap; } +.observation-object-image { display: block; width: 4rem; height: 4rem; object-fit: cover; border-radius: .3rem; } +.observation-image-dialog { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: min(92vw, 900px); max-width: none; max-height: 92vh; margin: 0; padding: 0; border: 1px solid rgba(226,193,93,.55); border-radius: var(--radius); background: linear-gradient(145deg, rgba(34,40,68,.99), rgba(7,10,25,.99) 55%, rgba(24,20,42,.99)); color: var(--text); box-shadow: 0 2px 0 rgba(255,255,255,.12) inset, 0 0 0 1px rgba(0,0,0,.7), 0 18px 0 -10px rgba(200,168,75,.16), 0 34px 0 -18px rgba(200,168,75,.1), 0 35px 95px rgba(0,0,0,.85); animation: observation-dialog-in .22s ease-out; } +.observation-image-dialog::backdrop { background: rgba(0,0,8,.86); backdrop-filter: blur(5px); } +.observation-image-dialog-content { position: relative; padding: 3.2rem 1rem 1rem; transform-style: preserve-3d; } +.observation-image-dialog-title { position: absolute; top: 1rem; left: 1rem; margin: 0; color: var(--gold); font-size: 1rem; } +.observation-image-dialog-close { position: absolute; top: .55rem; right: .7rem; border: 0; background: transparent; color: var(--text-dim); font-size: 1.8rem; line-height: 1; cursor: pointer; } +.observation-image-dialog-close:hover { color: var(--gold); } +.observation-image-dialog-image { display: block; max-width: 100%; max-height: 82vh; margin: 0 auto; object-fit: contain; border: 1px solid rgba(226,193,93,.3); border-radius: .35rem; box-shadow: 0 12px 25px rgba(0,0,0,.58), 0 2px 0 rgba(255,255,255,.1) inset; transform: translateZ(12px); } +@keyframes observation-dialog-in { from { opacity: 0; transform: translate(-50%, -50%) scale(.92) rotateX(4deg); } to { opacity: 1; transform: translate(-50%, -50%) scale(1) rotateX(0); } } .observation-sort { border: 0; background: transparent; color: inherit; font: inherit; text-transform: inherit; letter-spacing: inherit; text-align: left; cursor: pointer; padding: 0.35rem 0; } .observation-sort:hover { color: var(--gold); } .observation-sort:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; } @@ -91,18 +100,18 @@ $_SESSION['observation_csrf'] ??= bin2hex(random_bytes(32));

Eigenschaften der Objekte

- Scheinbare Größe – nur Deep Sky (Bogensekunden) + Scheinbare Größe – nur Deep Sky (Bogenminuten)
- - + +
- - + +
-

Dieser Filter gilt nur für Galaxien, Sternhaufen und Nebel einschließlich planetarischer Nebel. Verglichen wird die größte Katalogausdehnung. Bei gesetzter Größengrenze werden Deep-Sky-Objekte ohne Größenangabe ausgelassen. 60″ = 1′.

+

Dieser Filter gilt nur für Galaxien, Sternhaufen und Nebel einschließlich planetarischer Nebel. Verglichen wird die größte Katalogausdehnung. Bei gesetzter Größengrenze werden Deep-Sky-Objekte ohne Größenangabe ausgelassen. Maximal 60′.

Helligkeit @@ -206,6 +215,7 @@ $_SESSION['observation_csrf'] ??= bin2hex(random_bytes(32)); + @@ -228,6 +238,13 @@ $_SESSION['observation_csrf'] ??= bin2hex(random_bytes(32)); + +
+ +

+ +
+
diff --git a/public/js/observation_suggestions.js b/public/js/observation_suggestions.js index ee0ff02..4b71dac 100644 --- a/public/js/observation_suggestions.js +++ b/public/js/observation_suggestions.js @@ -9,6 +9,10 @@ const tbody = document.getElementById('observation-rows'); const previous = document.getElementById('observation-previous'); const next = document.getElementById('observation-next'); + const imageDialog = document.getElementById('observation-image-dialog'); + const imageDialogImage = document.getElementById('observation-image-dialog-image'); + const imageDialogTitle = document.getElementById('observation-image-dialog-title'); + const imageDialogClose = document.getElementById('observation-image-dialog-close'); const pageSize = 50; let data = null; let displayedRows = []; @@ -117,16 +121,66 @@ } row.append(td); } + function closeImageDialog() { + if (imageDialog?.open) imageDialog.close(); + } + function openImageDialog(url, title) { + if (!imageDialog || !imageDialogImage || !imageDialogTitle || typeof imageDialog.showModal !== 'function') { + window.open(url, '_blank', 'noopener'); + return; + } + imageDialogTitle.textContent = title; + imageDialogImage.src = url; + imageDialogImage.alt = `Bild von ${title}`; + imageDialog.showModal(); + imageDialogClose?.focus(); + } + imageDialogClose?.addEventListener('click', closeImageDialog); + imageDialog?.addEventListener('click', event => { + if (event.target === imageDialog) closeImageDialog(); + }); function render() { tbody.replaceChildren(); const fragment = document.createDocumentFragment(); for (const item of displayedRows.slice(page * pageSize, (page + 1) * pageSize)) { const row = document.createElement('tr'); row.dataset.objectId = item.id; - cell(row, item.name, item.note); + const imageCell = document.createElement('td'); + if (item.image) { + const link = document.createElement('a'); + link.href = item.image; + link.title = `Bild von ${item.name} vergrößern`; + link.addEventListener('click', event => { + event.preventDefault(); + openImageDialog(item.image, item.name); + }); + const image = document.createElement('img'); + image.src = item.image; + image.alt = `Bild von ${item.name}`; + image.loading = 'lazy'; + image.className = 'observation-object-image'; + link.append(image); + imageCell.append(link); + } else { + imageCell.textContent = '—'; + } + row.append(imageCell); + const nameCell = document.createElement('td'); + nameCell.textContent = item.name; + if (item.common_name) { + const commonName = document.createElement('small'); + commonName.textContent = item.common_name; + nameCell.append(commonName); + } + if (item.note) { + const note = document.createElement('small'); + note.textContent = item.note; + nameCell.append(note); + } + row.append(nameCell); cell(row, item.type); cell(row, item.best.magnitude === null ? 'Helligkeit unbekannt' : `${number(item.best.magnitude)} mag`, item.magnitude_note); - cell(row, item.size_arcsec === null ? '—' : `${number(item.size_arcsec)}″`); + cell(row, item.size_arcsec === null ? '—' : `${number(item.size_arcsec / 60, 2)}′`); const windows = document.createElement('td'); for (const interval of item.intervals) { const line = document.createElement('div'); diff --git a/public/observation_suggestions_helpers.php b/public/observation_suggestions_helpers.php index 1ab8689..17e7f03 100644 --- a/public/observation_suggestions_helpers.php +++ b/public/observation_suggestions_helpers.php @@ -2,6 +2,26 @@ declare(strict_types=1); /** Catalog values are server-owned; the client only supplies selection filters. */ +function observationDsoImage(array $row): ?string +{ + $catalogs = [ + ['messier', 'messier', 'm'], + ['ngc', 'ngc', 'ngc'], + ['ic', 'ic', 'ic'], + ]; + foreach ($catalogs as [$field, $folder, $prefix]) { + $number = preg_replace('/\D+/', '', trim((string) ($row[$field] ?? ''))); + if ($number === '' || !ctype_digit($number)) { + continue; + } + $relative = 'images/dso/' . $folder . '/' . $prefix . $number . '.png'; + if (is_file(__DIR__ . '/' . $relative)) { + return $relative; + } + } + return null; +} + function observationCatalog(PDO $pdo, array $types): array { $mapping = [ @@ -21,16 +41,30 @@ function observationCatalog(PDO $pdo, array $types): array $objects = []; if ($codes !== []) { $placeholders = implode(',', array_fill(0, count($codes), '?')); - $stmt = $pdo->prepare("SELECT id, name, messier, object_type_abrev, ra_deg, dec_deg, major_axis, v_mag + $stmt = $pdo->prepare("SELECT id, name, messier, ngc, ic, common_names_german, common_names, object_type_abrev, ra_deg, dec_deg, major_axis, v_mag FROM dso_objects WHERE object_type_abrev IN ($placeholders) AND ra_deg IS NOT NULL AND dec_deg IS NOT NULL"); $stmt->execute(array_keys($codes)); foreach ($stmt as $row) { $messier = trim((string) ($row['messier'] ?? '')); + $messierLabel = preg_replace('/^M\s*/i', 'M', $messier); + if ($messierLabel !== '' && !preg_match('/^M/i', $messierLabel)) { + $messierLabel = 'M' . $messierLabel; + } + $commonName = trim((string) ($row['common_names_german'] ?? '')); + if ($commonName === '') { + $commonName = trim((string) ($row['common_names'] ?? '')); + } + $displayName = $row['name']; + if ($messierLabel !== '') { + $displayName .= ' / ' . $messierLabel; + } $objects[] = [ 'id' => 'dso-' . $row['id'], - 'name' => $row['name'] . ($messier !== '' ? ' · M ' . $messier : ''), + 'name' => $displayName, + 'common_name' => $commonName, 'group' => $codes[$row['object_type_abrev']], + 'image' => observationDsoImage($row), 'ra_deg' => (float) $row['ra_deg'], 'dec_deg' => (float) $row['dec_deg'], // OpenNGC MajAx is in arcminutes; GUI and Python use arcseconds. 'size_arcsec' => $row['major_axis'] !== null && (float) $row['major_axis'] > 0 diff --git a/public/py/observation_suggestions_api.py b/public/py/observation_suggestions_api.py index 36cd170..6f4bd3b 100644 --- a/public/py/observation_suggestions_api.py +++ b/public/py/observation_suggestions_api.py @@ -44,7 +44,7 @@ def validate(payload): raw = payload['filters'] result = {} for key, low, high, optional in ( - ('object_size_min', 0, 1296000, True), ('object_size_max', 0, 1296000, True), + ('object_size_min', 0, 3600, True), ('object_size_max', 0, 3600, True), ('object_magnitude', -40, 40, True), ('object_altitude_min', 0, 90, False), ('object_altitude_max', 0, 90, False), ('object_azimuth_start', 0, 360, False), ('object_azimuth_end', 0, 360, False), ('sun_altitude_max', -90, 0, False), @@ -269,6 +269,8 @@ def calculate(payload): if best: results.append({'id': obj['id'], 'name': obj['name'], 'group': group, 'type': LABELS[group], 'size_arcsec': obj.get('size_arcsec'), 'best': best, 'intervals': intervals, + 'image': obj.get('image'), + 'common_name': obj.get('common_name'), 'magnitude_note': 'Modellschätzung' if moving else 'Katalogwert', 'note': obj.get('note', '')}) except (ValueError, TypeError, ZeroDivisionError, OverflowError):
Klick auf eine Überschrift setzt das Hauptkriterium. Umschalt + Klick ergänzt weitere Kriterien; erneuter Klick kehrt die Richtung um. Das nächste Kriterium entscheidet bei gleichen Werten. Fehlende Werte folgen innerhalb der jeweiligen Sortiergruppe am Ende.
Bild