Merge branch 'main' of https://github.com/Eskimue/Solve-Field-GUI
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"host": "10.42.42.102",
|
||||||
|
"port": 22,
|
||||||
|
"user": "tm",
|
||||||
|
"password": "heute1234"
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
# Solve-Field-GUI
|
||||||
|
|
||||||
|
Grafische Windows-Oberfläche für `solve-field` aus Astrometry.net mit Ausführung auf einem Linux-Server per SSH.
|
||||||
|
|
||||||
|
Die Software richtet sich an Anwender, die Astrometrie komfortabel über eine GUI steuern möchten, ohne die `solve-field`-Befehle jedes Mal von Hand zusammenzubauen.
|
||||||
|
|
||||||
|
## Was die Software macht
|
||||||
|
|
||||||
|
Die Anwendung läuft lokal unter Windows und verbindet sich per SSH mit einem entfernten Linux-Server.
|
||||||
|
|
||||||
|
Dort kann sie:
|
||||||
|
|
||||||
|
- Bilddateien auf den Server hochladen
|
||||||
|
- `solve-field` mit vielen Parametern starten
|
||||||
|
- die Ausgabe live im Log anzeigen
|
||||||
|
- Ergebnisdateien im Remote-Verzeichnis anzeigen
|
||||||
|
- einzelne oder mehrere Ergebnisdateien wieder lokal herunterladen
|
||||||
|
- Serverfunktionen wie SExtractor-Prüfung und Diagnose ausführen
|
||||||
|
|
||||||
|
## Hauptfunktionen
|
||||||
|
|
||||||
|
- SSH-Verbindung zu einem Linux-Server mit Benutzername, Passwort und Port
|
||||||
|
- Upload lokaler Bilddateien per SFTP
|
||||||
|
- grafische Konfiguration fast aller wichtigen `solve-field`-Parameter
|
||||||
|
- automatische Befehlsvorschau des erzeugten `solve-field`-Aufrufs
|
||||||
|
- Unterstützung für Maßstab, RA/Dek, Suchradius, Downsampling, CPU-Limit, Tweak-Order und viele weitere Optionen
|
||||||
|
- Unterstützung für Source Extractor per `--use-source-extractor`
|
||||||
|
- automatische Berechnung von `--plot-scale` aus der lokalen Bildgröße
|
||||||
|
- serverseitige Diagnose für `solve-field`, `astrometry.cfg`, Indexdateien, SExtractor und Speicherplatz
|
||||||
|
- Remote-Ergebnis-Tab mit Dateiliste
|
||||||
|
- Mehrfachdownload ausgewählter Ergebnisdateien
|
||||||
|
- Profilspeicherung und Profilladen für komplette GUI-Einstellungen
|
||||||
|
|
||||||
|
## Unterstützte Dateitypen
|
||||||
|
|
||||||
|
Als Eingabedatei lokal:
|
||||||
|
|
||||||
|
- FITS
|
||||||
|
- FIT
|
||||||
|
- FTS
|
||||||
|
- JPG / JPEG
|
||||||
|
- PNG
|
||||||
|
- TIFF / TIF
|
||||||
|
|
||||||
|
Für die automatische Plot-Skalierung kann die Anwendung Bildgrößen direkt aus mehreren Formaten lesen, darunter FITS, PNG, JPG, GIF und BMP.
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
## Lokal unter Windows
|
||||||
|
|
||||||
|
- Python 3
|
||||||
|
- `asyncssh`
|
||||||
|
- Netzwerkzugriff auf den Linux-Server per SSH
|
||||||
|
|
||||||
|
Installation von `asyncssh`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install asyncssh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Auf dem Linux-Server
|
||||||
|
|
||||||
|
- Astrometry.net mit `solve-field`
|
||||||
|
- passende Indexdateien
|
||||||
|
- SSH-Zugang
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
|
||||||
|
- `source-extractor`, wenn SExtractor zur Quellenerkennung genutzt werden soll
|
||||||
|
|
||||||
|
## Starten
|
||||||
|
|
||||||
|
Direkt mit Python:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python astrometry_gui.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Oder unter Windows über:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
astrometry_gui.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bedienung
|
||||||
|
|
||||||
|
## 1. SSH eintragen
|
||||||
|
|
||||||
|
Oben in der GUI:
|
||||||
|
|
||||||
|
- Host
|
||||||
|
- Port
|
||||||
|
- Benutzer
|
||||||
|
- Passwort
|
||||||
|
|
||||||
|
Mit `Verbinden & testen` kann geprüft werden, ob der Server erreichbar ist und `solve-field` vorhanden ist.
|
||||||
|
|
||||||
|
## 2. Bild auswählen und hochladen
|
||||||
|
|
||||||
|
Im Tab `Input / Output`:
|
||||||
|
|
||||||
|
- lokale Datei auswählen
|
||||||
|
- Remote-Verzeichnis festlegen
|
||||||
|
- Datei per `Hochladen` auf den Server übertragen
|
||||||
|
|
||||||
|
Nach dem Upload wird der Remote-Dateipfad automatisch als Eingabedatei gesetzt.
|
||||||
|
|
||||||
|
## 3. Solver-Parameter einstellen
|
||||||
|
|
||||||
|
Je nach Bedarf können unter anderem gesetzt werden:
|
||||||
|
|
||||||
|
- Maßstabsbereich
|
||||||
|
- RA / Dek / Radius
|
||||||
|
- CPU-Limit
|
||||||
|
- Downsampling
|
||||||
|
- Plot-Skalierung
|
||||||
|
- Tweak-Order
|
||||||
|
- Verifikation
|
||||||
|
- Invertierung
|
||||||
|
- SExtractor-Nutzung
|
||||||
|
|
||||||
|
## 4. Befehl prüfen und starten
|
||||||
|
|
||||||
|
Im Tab `Befehlsvorschau` sieht man den tatsächlich erzeugten `solve-field`-Befehl.
|
||||||
|
|
||||||
|
Mit `solve-field starten` wird der Befehl auf dem Server ausgeführt.
|
||||||
|
|
||||||
|
## Tabs im Überblick
|
||||||
|
|
||||||
|
## Input / Output
|
||||||
|
|
||||||
|
- lokale Bilddatei
|
||||||
|
- Remote-Verzeichnis
|
||||||
|
- Upload
|
||||||
|
- Eingabedatei auf dem Server
|
||||||
|
- Ausgabeoptionen wie `--wcs`, `--corr`, `--rdls`, `--match`, `--new-fits`
|
||||||
|
|
||||||
|
## Maßstab & Position
|
||||||
|
|
||||||
|
- `--scale-low`
|
||||||
|
- `--scale-high`
|
||||||
|
- `--scale`
|
||||||
|
- `--scale-err`
|
||||||
|
- `--ra`
|
||||||
|
- `--dec`
|
||||||
|
- `--radius`
|
||||||
|
|
||||||
|
## Solver-Parameter
|
||||||
|
|
||||||
|
- `--depth`
|
||||||
|
- `--cpulimit`
|
||||||
|
- `--plot-scale`
|
||||||
|
- `--downsample`
|
||||||
|
- `--tweak-order`
|
||||||
|
- `--odds-*`
|
||||||
|
- `--objs`
|
||||||
|
- `--pixel-error`
|
||||||
|
- zahlreiche Flags wie `--no-plots`, `--invert`, `--no-verify`
|
||||||
|
|
||||||
|
Zusätzlich gibt es einen Plot-Helfer:
|
||||||
|
|
||||||
|
- automatische Berechnung von `plot-scale`
|
||||||
|
- Zielkantenlänge einstellbar
|
||||||
|
|
||||||
|
## Erweitert
|
||||||
|
|
||||||
|
- manuelle Indexdateien
|
||||||
|
- zusätzliche freie Kommandozeilenargumente
|
||||||
|
|
||||||
|
## Befehlsvorschau
|
||||||
|
|
||||||
|
- zeigt den generierten Befehl
|
||||||
|
- kann aktualisiert werden
|
||||||
|
- kann in die Zwischenablage kopiert werden
|
||||||
|
|
||||||
|
## Server Setup
|
||||||
|
|
||||||
|
- SExtractor prüfen
|
||||||
|
- SExtractor installieren
|
||||||
|
- Server-Diagnose ausführen
|
||||||
|
|
||||||
|
## Remote-Ergebnisse
|
||||||
|
|
||||||
|
- zeigt Dateien im aktuellen Remote-Verzeichnis
|
||||||
|
- lädt den Inhalt beim Öffnen des Tabs automatisch neu
|
||||||
|
- erlaubt Auswahl und Download von einer oder mehreren Dateien
|
||||||
|
|
||||||
|
## Remote-Ergebnisse herunterladen
|
||||||
|
|
||||||
|
Im Tab `Remote-Ergebnisse`:
|
||||||
|
|
||||||
|
- eine oder mehrere Dateien markieren
|
||||||
|
- `Herunterladen` klicken
|
||||||
|
- Zielordner auswählen
|
||||||
|
|
||||||
|
Standardmäßig wird als Zielordner der Ordner der ursprünglich lokal ausgewählten Bilddatei vorgeschlagen.
|
||||||
|
|
||||||
|
## Remote-Verzeichnis leeren
|
||||||
|
|
||||||
|
Im Tab `Input / Output` gibt es die Funktion:
|
||||||
|
|
||||||
|
- `Remote-Inhalt löschen`
|
||||||
|
|
||||||
|
Dabei wird nur der Inhalt des Remote-Verzeichnisses gelöscht, nicht das Verzeichnis selbst.
|
||||||
|
|
||||||
|
## Profile speichern und laden
|
||||||
|
|
||||||
|
Über die Menühleiste:
|
||||||
|
|
||||||
|
- `Profil -> Profil speichern…`
|
||||||
|
- `Profil -> Profil laden…`
|
||||||
|
|
||||||
|
Gespeichert werden:
|
||||||
|
|
||||||
|
- SSH-Daten
|
||||||
|
- Solver-Parameter
|
||||||
|
- Ein-/Ausgabe-Einstellungen
|
||||||
|
- Zusatzargumente
|
||||||
|
- Indexliste
|
||||||
|
|
||||||
|
Das Passwort wird nicht im Klartext gespeichert, sondern mit einer einfachen internen Verschlüsselung abgelegt.
|
||||||
|
|
||||||
|
## Typischer Workflow
|
||||||
|
|
||||||
|
1. Serververbindung eintragen
|
||||||
|
2. Lokales Bild auswählen
|
||||||
|
3. Datei hochladen
|
||||||
|
4. Maßstab und Solver-Parameter setzen
|
||||||
|
5. Optional SExtractor aktivieren
|
||||||
|
6. `solve-field` starten
|
||||||
|
7. Ergebnisse im Tab `Remote-Ergebnisse` prüfen
|
||||||
|
8. Gewünschte Dateien lokal herunterladen
|
||||||
|
|
||||||
|
## Hinweise
|
||||||
|
|
||||||
|
- Die GUI baut nur den Befehl und führt ihn per SSH aus. Die eigentliche Astrometrie läuft vollständig auf dem Server.
|
||||||
|
- Die installierte Astrometry-Version auf dem Server bestimmt, welche Optionen tatsächlich verfügbar sind.
|
||||||
|
- Für SExtractor muss auf dem Server in der Regel `source-extractor` installiert sein.
|
||||||
|
- Für gute Ergebnisse müssen die passenden Indexdateien serverseitig vorhanden und in `astrometry.cfg` korrekt eingebunden sein.
|
||||||
|
|
||||||
|
## Dateien im Projekt
|
||||||
|
|
||||||
|
- [astrometry_gui.py](C:/Users/ich/source/repos/Solve-Field-GUI/astrometry_gui.py)
|
||||||
|
- [astrometry_gui.bat](C:/Users/ich/source/repos/Solve-Field-GUI/astrometry_gui.bat)
|
||||||
|
|
||||||
|
## Lizenz / Nutzung
|
||||||
|
|
||||||
|
Dieses Repository enthält eine GUI zur Bedienung von Astrometry.net. Die Lizenz und Bedingungen von Astrometry.net sowie von eventuell installierten Serverpaketen gelten zusätzlich zu diesem Projekt.
|
||||||
Binary file not shown.
+573
-9
@@ -8,13 +8,17 @@ Abhängigkeiten:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
|
import tkinter.font as tkfont
|
||||||
from tkinter import ttk, filedialog, messagebox, scrolledtext
|
from tkinter import ttk, filedialog, messagebox, scrolledtext
|
||||||
import threading
|
import threading
|
||||||
import os
|
import os
|
||||||
import shlex
|
import shlex
|
||||||
import queue
|
import queue
|
||||||
import stat
|
import stat
|
||||||
|
import struct
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import asyncssh
|
import asyncssh
|
||||||
@@ -23,6 +27,9 @@ except ImportError:
|
|||||||
HAS_ASYNCSSH = False
|
HAS_ASYNCSSH = False
|
||||||
|
|
||||||
|
|
||||||
|
SSH_CREDENTIALS_JSON = "ssh_zugangsdaten.json"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Tooltip
|
# Tooltip
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -144,6 +151,26 @@ class AsyncSSH:
|
|||||||
progress_handler=_progress,
|
progress_handler=_progress,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def download(self, remote_path: str, local_path: str, progress_cb=None):
|
||||||
|
if self._conn is None:
|
||||||
|
await self.connect()
|
||||||
|
local_path = os.path.normpath(local_path)
|
||||||
|
|
||||||
|
async with self._conn.start_sftp_client() as sftp:
|
||||||
|
attrs = await sftp.stat(remote_path)
|
||||||
|
total = int(getattr(attrs, "size", 0) or 0)
|
||||||
|
done = 0
|
||||||
|
async with sftp.open(remote_path, "rb") as remote_file:
|
||||||
|
with open(local_path, "wb") as local_file:
|
||||||
|
while True:
|
||||||
|
chunk = await remote_file.read(1024 * 1024)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
local_file.write(chunk)
|
||||||
|
done += len(chunk)
|
||||||
|
if progress_cb:
|
||||||
|
progress_cb(done, total)
|
||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
if self._conn:
|
if self._conn:
|
||||||
self._conn.close()
|
self._conn.close()
|
||||||
@@ -159,7 +186,7 @@ class AstrometryGUI:
|
|||||||
|
|
||||||
def __init__(self, root: tk.Tk):
|
def __init__(self, root: tk.Tk):
|
||||||
self.root = root
|
self.root = root
|
||||||
self.root.title("Astrometry.net — solve-field GUI")
|
self.root.title("Astrometry.net — solve-field GUI vom Astronomiemuseum")
|
||||||
self.root.geometry("1000x840")
|
self.root.geometry("1000x840")
|
||||||
self.root.minsize(800, 620)
|
self.root.minsize(800, 620)
|
||||||
|
|
||||||
@@ -192,8 +219,10 @@ class AstrometryGUI:
|
|||||||
self.v_port = tk.IntVar(value=22)
|
self.v_port = tk.IntVar(value=22)
|
||||||
self.v_user = tk.StringVar(value="eskimue")
|
self.v_user = tk.StringVar(value="eskimue")
|
||||||
self.v_pass = tk.StringVar(value="Preside")
|
self.v_pass = tk.StringVar(value="Preside")
|
||||||
|
self._load_startup_credentials()
|
||||||
|
|
||||||
self.v_local_file = tk.StringVar()
|
self.v_local_file = tk.StringVar()
|
||||||
|
self.v_local_file.trace_add("write", self._on_local_file_changed)
|
||||||
self.v_remote_dir = tk.StringVar(value="/tmp/astrometry")
|
self.v_remote_dir = tk.StringVar(value="/tmp/astrometry")
|
||||||
self.v_input_file = tk.StringVar()
|
self.v_input_file = tk.StringVar()
|
||||||
self.v_input_file.trace_add("write", self._on_input_file_changed)
|
self.v_input_file.trace_add("write", self._on_input_file_changed)
|
||||||
@@ -220,6 +249,11 @@ class AstrometryGUI:
|
|||||||
|
|
||||||
self.v_depth = tk.StringVar()
|
self.v_depth = tk.StringVar()
|
||||||
self.v_cpulimit = tk.StringVar(value="300")
|
self.v_cpulimit = tk.StringVar(value="300")
|
||||||
|
self.v_plot_scale = tk.StringVar()
|
||||||
|
self.v_plot_scale_auto = tk.BooleanVar(value=True)
|
||||||
|
self.v_plot_target_long_edge = tk.StringVar(value="900")
|
||||||
|
self.v_plot_target_long_edge.trace_add("write", self._on_plot_scale_settings_changed)
|
||||||
|
self.v_plot_scale_auto.trace_add("write", self._on_plot_scale_settings_changed)
|
||||||
self.v_downsample = tk.StringVar(value="6")
|
self.v_downsample = tk.StringVar(value="6")
|
||||||
self.v_tweak_order = tk.StringVar(value="2")
|
self.v_tweak_order = tk.StringVar(value="2")
|
||||||
self.v_odds_solve = tk.StringVar()
|
self.v_odds_solve = tk.StringVar()
|
||||||
@@ -244,18 +278,43 @@ class AstrometryGUI:
|
|||||||
self.v_invert = tk.BooleanVar(value=False)
|
self.v_invert = tk.BooleanVar(value=False)
|
||||||
|
|
||||||
self.v_extra = tk.StringVar()
|
self.v_extra = tk.StringVar()
|
||||||
|
self.results_files: dict[str, dict] = {}
|
||||||
|
|
||||||
|
def _load_startup_credentials(self):
|
||||||
|
path = os.path.join(os.getcwd(), SSH_CREDENTIALS_JSON)
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self._load_credentials_from_file(path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _load_credentials_from_file(self, path: str):
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if "host" in data:
|
||||||
|
self.v_host.set(str(data["host"]))
|
||||||
|
if "port" in data:
|
||||||
|
self.v_port.set(int(data["port"]))
|
||||||
|
if "user" in data:
|
||||||
|
self.v_user.set(str(data["user"]))
|
||||||
|
if "password" in data:
|
||||||
|
self.v_pass.set(str(data["password"]))
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
# UI aufbauen
|
# UI aufbauen
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
|
|
||||||
def _build_ui(self):
|
def _build_ui(self):
|
||||||
|
self._build_menu()
|
||||||
|
|
||||||
top = ttk.Frame(self.root, padding=4)
|
top = ttk.Frame(self.root, padding=4)
|
||||||
top.pack(fill=tk.X)
|
top.pack(fill=tk.X)
|
||||||
self._build_conn_bar(top)
|
self._build_conn_bar(top)
|
||||||
|
|
||||||
nb = ttk.Notebook(self.root)
|
self.nb = ttk.Notebook(self.root)
|
||||||
nb.pack(fill=tk.BOTH, expand=True, padx=6, pady=(0, 4))
|
self.nb.pack(fill=tk.BOTH, expand=True, padx=6, pady=(0, 4))
|
||||||
|
self.nb.bind("<<NotebookTabChanged>>", self._on_tab_changed)
|
||||||
|
|
||||||
for title, builder in [
|
for title, builder in [
|
||||||
("Input / Output", self._tab_io),
|
("Input / Output", self._tab_io),
|
||||||
@@ -265,10 +324,14 @@ class AstrometryGUI:
|
|||||||
("Befehlsvorschau", self._tab_cmd),
|
("Befehlsvorschau", self._tab_cmd),
|
||||||
("⚙ Server Setup", self._tab_setup),
|
("⚙ Server Setup", self._tab_setup),
|
||||||
]:
|
]:
|
||||||
f = ttk.Frame(nb, padding=6)
|
f = ttk.Frame(self.nb, padding=6)
|
||||||
nb.add(f, text=title)
|
self.nb.add(f, text=title)
|
||||||
builder(f)
|
builder(f)
|
||||||
|
|
||||||
|
self.results_tab = ttk.Frame(self.nb, padding=6)
|
||||||
|
self._tab_results(self.results_tab)
|
||||||
|
self.nb.add(self.results_tab, text="Remote-Ergebnisse")
|
||||||
|
|
||||||
btn_bar = ttk.Frame(self.root, padding=4)
|
btn_bar = ttk.Frame(self.root, padding=4)
|
||||||
btn_bar.pack(fill=tk.X)
|
btn_bar.pack(fill=tk.X)
|
||||||
|
|
||||||
@@ -304,6 +367,22 @@ class AstrometryGUI:
|
|||||||
|
|
||||||
self._current_task: asyncio.Future | None = None
|
self._current_task: asyncio.Future | None = None
|
||||||
|
|
||||||
|
def _build_menu(self):
|
||||||
|
menubar = tk.Menu(self.root)
|
||||||
|
|
||||||
|
profile_menu = tk.Menu(menubar, tearoff=False)
|
||||||
|
profile_menu.add_command(label="Verbindung laden…", command=self._menu_load_credentials)
|
||||||
|
profile_menu.add_separator()
|
||||||
|
profile_menu.add_command(label="Profil speichern…", command=self._save_profile)
|
||||||
|
profile_menu.add_command(label="Profil laden…", command=self._load_profile)
|
||||||
|
menubar.add_cascade(label="Profil", menu=profile_menu)
|
||||||
|
|
||||||
|
self.root.config(menu=menubar)
|
||||||
|
|
||||||
|
def _on_tab_changed(self, _event=None):
|
||||||
|
if self.nb.select() == str(self.results_tab):
|
||||||
|
self._refresh_results_tab()
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
# Verbindungs-Leiste
|
# Verbindungs-Leiste
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
@@ -348,6 +427,9 @@ class AstrometryGUI:
|
|||||||
"Öffnet einen Datei-Dialog um die lokale Astro-Bild-Datei auszuwählen."
|
"Öffnet einen Datei-Dialog um die lokale Astro-Bild-Datei auszuwählen."
|
||||||
).grid(row=0, column=2, padx=4)
|
).grid(row=0, column=2, padx=4)
|
||||||
|
|
||||||
|
self.local_image_size_label = ttk.Label(uf, text="Größe: —", foreground="gray")
|
||||||
|
self.local_image_size_label.grid(row=0, column=3, sticky=tk.W, padx=4, pady=3)
|
||||||
|
|
||||||
ttk.Label(uf, text="Remote-Verzeichnis:").grid(row=1, column=0, sticky=tk.W, padx=4, pady=3)
|
ttk.Label(uf, text="Remote-Verzeichnis:").grid(row=1, column=0, sticky=tk.W, padx=4, pady=3)
|
||||||
tip(ttk.Entry(uf, textvariable=self.v_remote_dir),
|
tip(ttk.Entry(uf, textvariable=self.v_remote_dir),
|
||||||
"Verzeichnis auf dem Server wohin die Datei hochgeladen wird.\n"
|
"Verzeichnis auf dem Server wohin die Datei hochgeladen wird.\n"
|
||||||
@@ -358,9 +440,14 @@ class AstrometryGUI:
|
|||||||
"Überträgt die lokale Datei asynchron per SFTP auf den Server.\n"
|
"Überträgt die lokale Datei asynchron per SFTP auf den Server.\n"
|
||||||
"Nach dem Upload wird der Remote-Pfad automatisch eingetragen.")
|
"Nach dem Upload wird der Remote-Pfad automatisch eingetragen.")
|
||||||
self.upload_btn.grid(row=1, column=2, padx=4)
|
self.upload_btn.grid(row=1, column=2, padx=4)
|
||||||
|
tip(ttk.Button(uf, text="Remote-Inhalt l\u00f6schen", command=self._delete_remote_dir),
|
||||||
|
"L\u00f6scht nur den Inhalt des angegebenen Remote-Verzeichnisses auf dem Server.\n"
|
||||||
|
"Das Verzeichnis selbst bleibt erhalten.\n"
|
||||||
|
"Vorher erscheint eine Sicherheitsabfrage."
|
||||||
|
).grid(row=1, column=3, padx=4)
|
||||||
|
|
||||||
self.upload_progress = ttk.Progressbar(uf, mode="determinate", maximum=100)
|
self.upload_progress = ttk.Progressbar(uf, mode="determinate", maximum=100)
|
||||||
self.upload_progress.grid(row=2, column=0, columnspan=3, sticky=tk.EW, padx=4, pady=4)
|
self.upload_progress.grid(row=2, column=0, columnspan=4, sticky=tk.EW, padx=4, pady=4)
|
||||||
tip(self.upload_progress, "Fortschritt des Datei-Uploads in Prozent.")
|
tip(self.upload_progress, "Fortschritt des Datei-Uploads in Prozent.")
|
||||||
|
|
||||||
rf = ttk.LabelFrame(parent, text="Eingabedatei auf Remote-Rechner", padding=6)
|
rf = ttk.LabelFrame(parent, text="Eingabedatei auf Remote-Rechner", padding=6)
|
||||||
@@ -492,6 +579,10 @@ class AstrometryGUI:
|
|||||||
("CPU-Limit (--cpulimit, s):", self.v_cpulimit,
|
("CPU-Limit (--cpulimit, s):", self.v_cpulimit,
|
||||||
"Maximale Rechenzeit in Sekunden.\n"
|
"Maximale Rechenzeit in Sekunden.\n"
|
||||||
"300s ist ein guter Kompromiss. 0 = kein Limit."),
|
"300s ist ein guter Kompromiss. 0 = kein Limit."),
|
||||||
|
("Plot-Skalierung (--plot-scale):", self.v_plot_scale,
|
||||||
|
"Skaliert nur die erzeugten Plot-Bilder.\n"
|
||||||
|
"0.25 erzeugt kleinere Plots, 2.0 größere.\n"
|
||||||
|
"Hat keinen Einfluss auf die eigentliche Astrometrie."),
|
||||||
("Downsampling (--downsample):", self.v_downsample,
|
("Downsampling (--downsample):", self.v_downsample,
|
||||||
"Verkleinert das Bild vor der Verarbeitung.\n"
|
"Verkleinert das Bild vor der Verarbeitung.\n"
|
||||||
"6 = 1/6 der Auflösung. Beschleunigt die Quellerkennung stark.\n"
|
"6 = 1/6 der Auflösung. Beschleunigt die Quellerkennung stark.\n"
|
||||||
@@ -521,6 +612,23 @@ class AstrometryGUI:
|
|||||||
tooltip
|
tooltip
|
||||||
).grid(row=row, column=col * 2 + 1, sticky=tk.W, padx=(0, 4), pady=2)
|
).grid(row=row, column=col * 2 + 1, sticky=tk.W, padx=(0, 4), pady=2)
|
||||||
|
|
||||||
|
plotf = ttk.LabelFrame(parent, text="Plot-Helfer", padding=6)
|
||||||
|
plotf.pack(fill=tk.X, pady=(0, 8))
|
||||||
|
plotf.columnconfigure(3, weight=1)
|
||||||
|
|
||||||
|
tip(ttk.Checkbutton(plotf, text="Plot-Skalierung automatisch aus lokaler Bildgröße", variable=self.v_plot_scale_auto),
|
||||||
|
"Berechnet --plot-scale aus der langen Bildkante der lokal ausgewählten Datei.\n"
|
||||||
|
"Manuelle Änderungen am Feld sind weiterhin möglich."
|
||||||
|
).grid(row=0, column=0, columnspan=2, sticky=tk.W, padx=4, pady=2)
|
||||||
|
ttk.Label(plotf, text="Zielkantenlänge (px):").grid(row=0, column=2, sticky=tk.W, padx=4, pady=2)
|
||||||
|
tip(ttk.Entry(plotf, textvariable=self.v_plot_target_long_edge, width=10),
|
||||||
|
"Die lange Kante der erzeugten Plots wird grob auf diesen Wert skaliert.\n"
|
||||||
|
"Standard: 900 px."
|
||||||
|
).grid(row=0, column=3, sticky=tk.W, padx=4, pady=2)
|
||||||
|
tip(ttk.Button(plotf, text="Jetzt berechnen", command=self._recalculate_plot_scale),
|
||||||
|
"Liest die Größe der lokalen Datei und setzt --plot-scale neu."
|
||||||
|
).grid(row=0, column=4, sticky=tk.W, padx=4, pady=2)
|
||||||
|
|
||||||
ff = ttk.LabelFrame(parent, text="Schalter (Flags)", padding=6)
|
ff = ttk.LabelFrame(parent, text="Schalter (Flags)", padding=6)
|
||||||
ff.pack(fill=tk.X, pady=(0, 8))
|
ff.pack(fill=tk.X, pady=(0, 8))
|
||||||
|
|
||||||
@@ -547,7 +655,7 @@ class AstrometryGUI:
|
|||||||
("Weitermachen (--continue)", self.v_continue,
|
("Weitermachen (--continue)", self.v_continue,
|
||||||
"Überspringt bereits gelöste Bilder (prüft ob .solved-Datei existiert).\n"
|
"Überspringt bereits gelöste Bilder (prüft ob .solved-Datei existiert).\n"
|
||||||
"Ideal für unterbrochene Batch-Verarbeitungen."),
|
"Ideal für unterbrochene Batch-Verarbeitungen."),
|
||||||
("SExtractor nutzen (--use-sextractor)", self.v_sextractor,
|
("SExtractor nutzen (--use-source-extractor)", self.v_sextractor,
|
||||||
"Verwendet SExtractor statt des eingebauten Quellenerkennungs-Algorithmus.\n"
|
"Verwendet SExtractor statt des eingebauten Quellenerkennungs-Algorithmus.\n"
|
||||||
"Muss auf dem Server installiert sein. Oft besser bei schwierigen Bildern."),
|
"Muss auf dem Server installiert sein. Oft besser bei schwierigen Bildern."),
|
||||||
("Als FITS-Bild behandeln (--fits-image)", self.v_fits_image,
|
("Als FITS-Bild behandeln (--fits-image)", self.v_fits_image,
|
||||||
@@ -685,6 +793,37 @@ class AstrometryGUI:
|
|||||||
" • Freier Speicherplatz"
|
" • Freier Speicherplatz"
|
||||||
).pack(side=tk.LEFT, padx=4, pady=4)
|
).pack(side=tk.LEFT, padx=4, pady=4)
|
||||||
|
|
||||||
|
def _tab_results(self, parent):
|
||||||
|
head = ttk.Frame(parent)
|
||||||
|
head.pack(fill=tk.X, pady=(0, 4))
|
||||||
|
|
||||||
|
tip(ttk.Label(head, text="Remote-Ergebnisse nach solve-field:"),
|
||||||
|
"Zeigt den Inhalt des Remote-Verzeichnisses nach einem solve-field-Lauf an."
|
||||||
|
).pack(side=tk.LEFT)
|
||||||
|
tip(ttk.Button(head, text="Aktualisieren", command=self._refresh_results_tab),
|
||||||
|
"Liest den Inhalt des aktuell eingestellten Remote-Verzeichnisses erneut vom Server ein."
|
||||||
|
).pack(side=tk.RIGHT, padx=4)
|
||||||
|
tip(ttk.Button(head, text="Remote-Inhalt löschen", command=self._delete_remote_dir),
|
||||||
|
"Löscht den Inhalt des aktuell eingestellten Remote-Verzeichnisses auf dem Server."
|
||||||
|
).pack(side=tk.RIGHT, padx=4)
|
||||||
|
tip(ttk.Button(head, text="Herunterladen", command=self._download_selected_result),
|
||||||
|
"Lädt die ausgewählte Remote-Datei per SFTP herunter."
|
||||||
|
).pack(side=tk.RIGHT, padx=4)
|
||||||
|
|
||||||
|
self.results_info = ttk.Label(parent, text="Noch kein solve-field-Lauf abgeschlossen.", foreground="gray")
|
||||||
|
self.results_info.pack(anchor=tk.W, pady=(0, 4))
|
||||||
|
|
||||||
|
cols = ("name", "size", "remote")
|
||||||
|
self.results_tree = ttk.Treeview(parent, columns=cols, show="headings", height=16, selectmode="extended")
|
||||||
|
self.results_tree.heading("name", text="Datei")
|
||||||
|
self.results_tree.heading("size", text="Größe")
|
||||||
|
self.results_tree.heading("remote", text="Remote-Pfad")
|
||||||
|
self.results_tree.column("name", width=240, anchor=tk.W)
|
||||||
|
self.results_tree.column("size", width=100, anchor=tk.E)
|
||||||
|
self.results_tree.column("remote", width=520, anchor=tk.W)
|
||||||
|
self.results_tree.pack(fill=tk.BOTH, expand=True)
|
||||||
|
self.results_tree.bind("<Double-1>", lambda _e: self._download_selected_result())
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
# Befehl bauen
|
# Befehl bauen
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
@@ -713,7 +852,7 @@ class AstrometryGUI:
|
|||||||
flag("--resort", self.v_resort)
|
flag("--resort", self.v_resort)
|
||||||
flag("--continue", self.v_continue)
|
flag("--continue", self.v_continue)
|
||||||
flag("--no-delete-temp", self.v_no_del_temp)
|
flag("--no-delete-temp", self.v_no_del_temp)
|
||||||
flag("--use-sextractor", self.v_sextractor)
|
flag("--use-source-extractor", self.v_sextractor)
|
||||||
flag("--fits-image", self.v_fits_image)
|
flag("--fits-image", self.v_fits_image)
|
||||||
flag("--no-verify", self.v_no_verify)
|
flag("--no-verify", self.v_no_verify)
|
||||||
flag("--just-augment", self.v_just_augment)
|
flag("--just-augment", self.v_just_augment)
|
||||||
@@ -754,6 +893,7 @@ class AstrometryGUI:
|
|||||||
|
|
||||||
add("--depth", self.v_depth)
|
add("--depth", self.v_depth)
|
||||||
add("--cpulimit", self.v_cpulimit)
|
add("--cpulimit", self.v_cpulimit)
|
||||||
|
add("--plot-scale", self.v_plot_scale)
|
||||||
add("--downsample", self.v_downsample)
|
add("--downsample", self.v_downsample)
|
||||||
add("--odds-to-solve", self.v_odds_solve)
|
add("--odds-to-solve", self.v_odds_solve)
|
||||||
add("--odds-to-reject", self.v_odds_reject)
|
add("--odds-to-reject", self.v_odds_reject)
|
||||||
@@ -843,6 +983,195 @@ class AstrometryGUI:
|
|||||||
finally:
|
finally:
|
||||||
self.root.after(0, lambda: self.upload_btn.config(state=tk.NORMAL))
|
self.root.after(0, lambda: self.upload_btn.config(state=tk.NORMAL))
|
||||||
|
|
||||||
|
def _delete_remote_dir(self):
|
||||||
|
remote_dir = self.v_remote_dir.get().strip()
|
||||||
|
if not remote_dir:
|
||||||
|
messagebox.showwarning("Remote-Verzeichnis fehlt", "Bitte ein Remote-Verzeichnis angeben.")
|
||||||
|
return
|
||||||
|
if remote_dir == "/":
|
||||||
|
messagebox.showerror("Ung\u00fcltiges Verzeichnis", "Das Root-Verzeichnis '/' darf nicht gel\u00f6scht werden.")
|
||||||
|
return
|
||||||
|
if not messagebox.askyesno(
|
||||||
|
"Remote-Inhalt l\u00f6schen",
|
||||||
|
f"Soll nur der Inhalt dieses Verzeichnisses auf dem Server rekursiv gel\u00f6scht werden?\n\n{remote_dir}"
|
||||||
|
):
|
||||||
|
return
|
||||||
|
self._submit(self._async_delete_remote_dir(remote_dir))
|
||||||
|
|
||||||
|
async def _async_delete_remote_dir(self, remote_dir: str):
|
||||||
|
quoted_dir = shlex.quote(remote_dir)
|
||||||
|
cmd = (
|
||||||
|
f"dir={quoted_dir}; "
|
||||||
|
"if [ -z \"$dir\" ] || [ \"$dir\" = \"/\" ]; then "
|
||||||
|
"echo '[FEHLER] Unsicheres Remote-Verzeichnis.'; exit 1; "
|
||||||
|
"fi; "
|
||||||
|
"if [ ! -e \"$dir\" ]; then "
|
||||||
|
"echo '[INFO] Remote-Verzeichnis existiert nicht.'; exit 0; "
|
||||||
|
"fi; "
|
||||||
|
"find \"$dir\" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +; "
|
||||||
|
"echo \"[OK] Remote-Inhalt gel\u00f6scht: $dir\""
|
||||||
|
)
|
||||||
|
self._log(f"[INFO] L\u00f6sche Remote-Inhalt: {remote_dir}\n", "info")
|
||||||
|
try:
|
||||||
|
ssh = self._make_ssh()
|
||||||
|
await ssh.connect()
|
||||||
|
await ssh.exec_stream(cmd, self._log)
|
||||||
|
await ssh.close()
|
||||||
|
except Exception as exc:
|
||||||
|
self._log(f"[FEHLER] Remote-Inhalt l\u00f6schen: {exc}\n", "error")
|
||||||
|
|
||||||
|
def _refresh_results_tab(self):
|
||||||
|
remote_dir = self.v_remote_dir.get().strip()
|
||||||
|
self.root.config(cursor="watch")
|
||||||
|
self.nb.config(cursor="watch")
|
||||||
|
self.results_tree.config(cursor="watch")
|
||||||
|
self._submit(self._async_refresh_results_tab(remote_dir))
|
||||||
|
|
||||||
|
async def _async_refresh_results_tab(self, remote_dir: str, returncode: int | None = None):
|
||||||
|
if not remote_dir:
|
||||||
|
self.root.after(0, lambda: self._set_results_info("Kein Remote-Verzeichnis gesetzt."))
|
||||||
|
self.root.after(0, lambda: self._set_results_items([]))
|
||||||
|
return
|
||||||
|
|
||||||
|
quoted_dir = shlex.quote(remote_dir)
|
||||||
|
cmd = (
|
||||||
|
f"dir={quoted_dir}; "
|
||||||
|
"if [ ! -e \"$dir\" ]; then "
|
||||||
|
"echo 'Remote-Verzeichnis existiert nicht.'; exit 0; "
|
||||||
|
"fi; "
|
||||||
|
"find \"$dir\" -maxdepth 2 -type f -printf '%s\t%P\t%p\\n' | sort"
|
||||||
|
)
|
||||||
|
info = f"Remote-Verzeichnis: {remote_dir}"
|
||||||
|
if returncode is not None:
|
||||||
|
info += f" | solve-field Exit-Code: {returncode}"
|
||||||
|
self.root.after(0, lambda: self._set_results_info(info))
|
||||||
|
self.root.after(0, lambda: self._set_results_items([]))
|
||||||
|
|
||||||
|
entries: list[dict[str, str]] = []
|
||||||
|
|
||||||
|
def _capture(line: str):
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped or stripped.startswith("[Prozess beendet"):
|
||||||
|
return
|
||||||
|
if "\t" not in stripped:
|
||||||
|
return
|
||||||
|
size, rel_path, full_path = stripped.split("\t", 2)
|
||||||
|
entries.append({
|
||||||
|
"name": rel_path or os.path.basename(full_path),
|
||||||
|
"size": size,
|
||||||
|
"remote": full_path,
|
||||||
|
})
|
||||||
|
|
||||||
|
try:
|
||||||
|
ssh = self._make_ssh()
|
||||||
|
await ssh.connect()
|
||||||
|
await ssh.exec_stream(cmd, _capture)
|
||||||
|
await ssh.close()
|
||||||
|
self.root.after(0, lambda: self._set_results_items(entries))
|
||||||
|
self.root.after(0, self._clear_wait_cursor)
|
||||||
|
except Exception as exc:
|
||||||
|
self.root.after(0, lambda: self._set_results_info(f"[FEHLER] {exc}"))
|
||||||
|
self.root.after(0, lambda: self._set_results_items([]))
|
||||||
|
self.root.after(0, self._clear_wait_cursor)
|
||||||
|
|
||||||
|
def _set_results_info(self, text: str):
|
||||||
|
self.results_info.config(text=text, foreground="gray")
|
||||||
|
|
||||||
|
def _set_results_items(self, entries: list[dict[str, str]]):
|
||||||
|
self.results_files = {}
|
||||||
|
for item in self.results_tree.get_children():
|
||||||
|
self.results_tree.delete(item)
|
||||||
|
for i, entry in enumerate(entries):
|
||||||
|
item_id = f"result_{i}"
|
||||||
|
self.results_files[item_id] = entry
|
||||||
|
self.results_tree.insert(
|
||||||
|
"",
|
||||||
|
tk.END,
|
||||||
|
iid=item_id,
|
||||||
|
values=(
|
||||||
|
entry["name"],
|
||||||
|
self._format_size(entry["size"]),
|
||||||
|
entry["remote"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self._autosize_results_columns()
|
||||||
|
|
||||||
|
def _autosize_results_columns(self):
|
||||||
|
body_font = tkfont.nametofont("TkDefaultFont")
|
||||||
|
head_font = tkfont.nametofont("TkHeadingFont")
|
||||||
|
padding = 24
|
||||||
|
|
||||||
|
for col in ("name", "size", "remote"):
|
||||||
|
header = self.results_tree.heading(col, "text") or col
|
||||||
|
width = head_font.measure(header) + padding
|
||||||
|
for item_id in self.results_tree.get_children():
|
||||||
|
value = self.results_tree.set(item_id, col)
|
||||||
|
width = max(width, body_font.measure(str(value)) + padding)
|
||||||
|
self.results_tree.column(col, width=width)
|
||||||
|
|
||||||
|
def _clear_wait_cursor(self):
|
||||||
|
self.root.config(cursor="")
|
||||||
|
self.nb.config(cursor="")
|
||||||
|
self.results_tree.config(cursor="")
|
||||||
|
|
||||||
|
def _format_size(self, size_text: str) -> str:
|
||||||
|
try:
|
||||||
|
size = int(size_text)
|
||||||
|
except Exception:
|
||||||
|
return size_text
|
||||||
|
units = ["B", "KB", "MB", "GB", "TB"]
|
||||||
|
value = float(size)
|
||||||
|
for unit in units:
|
||||||
|
if value < 1024 or unit == units[-1]:
|
||||||
|
if unit == "B":
|
||||||
|
return f"{int(value)} {unit}"
|
||||||
|
return f"{value:.1f} {unit}"
|
||||||
|
value /= 1024
|
||||||
|
return size_text
|
||||||
|
|
||||||
|
def _download_selected_result(self):
|
||||||
|
selection = self.results_tree.selection()
|
||||||
|
if not selection:
|
||||||
|
messagebox.showwarning("Keine Datei ausgewählt", "Bitte zuerst eine oder mehrere Dateien im Ergebnis-Tab auswählen.")
|
||||||
|
return
|
||||||
|
entries = []
|
||||||
|
for item_id in selection:
|
||||||
|
entry = self.results_files.get(item_id)
|
||||||
|
if entry:
|
||||||
|
entries.append(entry)
|
||||||
|
if not entries:
|
||||||
|
messagebox.showwarning("Datei fehlt", "Die ausgewählten Dateien konnten nicht gefunden werden.")
|
||||||
|
return
|
||||||
|
source_dir = os.path.dirname(self.v_local_file.get().strip()) if self.v_local_file.get().strip() else ""
|
||||||
|
initial_dir = source_dir if source_dir and os.path.isdir(source_dir) else os.getcwd()
|
||||||
|
local_dir = filedialog.askdirectory(
|
||||||
|
title="Zielordner für Download auswählen",
|
||||||
|
initialdir=initial_dir,
|
||||||
|
)
|
||||||
|
if not local_dir:
|
||||||
|
return
|
||||||
|
downloads = []
|
||||||
|
for entry in entries:
|
||||||
|
suggested = os.path.basename(entry["remote"])
|
||||||
|
local_path = os.path.normpath(os.path.join(local_dir, suggested))
|
||||||
|
downloads.append((entry["remote"], local_path))
|
||||||
|
self._submit(self._async_download_results(downloads))
|
||||||
|
|
||||||
|
async def _async_download_results(self, downloads: list[tuple[str, str]]):
|
||||||
|
try:
|
||||||
|
ssh = self._make_ssh()
|
||||||
|
await ssh.connect()
|
||||||
|
for remote_path, local_path in downloads:
|
||||||
|
self._log(f"[INFO] Lade Ergebnis herunter: {remote_path} → {local_path}\n", "info")
|
||||||
|
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
||||||
|
await ssh.download(remote_path, local_path)
|
||||||
|
if not os.path.isfile(local_path):
|
||||||
|
raise FileNotFoundError(f"Datei nach Download nicht gefunden: {local_path}")
|
||||||
|
self._log(f"[OK] Download abgeschlossen: {local_path}\n", "ok")
|
||||||
|
await ssh.close()
|
||||||
|
except Exception as exc:
|
||||||
|
self._log(f"[FEHLER] Download: {exc}\n", "error")
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
if not self.v_input_file.get():
|
if not self.v_input_file.get():
|
||||||
messagebox.showwarning("Keine Eingabedatei", "Bitte einen Dateipfad angeben.")
|
messagebox.showwarning("Keine Eingabedatei", "Bitte einen Dateipfad angeben.")
|
||||||
@@ -855,10 +1184,13 @@ class AstrometryGUI:
|
|||||||
try:
|
try:
|
||||||
ssh = self._make_ssh()
|
ssh = self._make_ssh()
|
||||||
await ssh.connect()
|
await ssh.connect()
|
||||||
await ssh.exec_stream(cmd, self._log)
|
rc = await ssh.exec_stream(cmd, self._log)
|
||||||
await ssh.close()
|
await ssh.close()
|
||||||
|
self._current_task = None
|
||||||
|
await self._async_refresh_results_tab(self.v_remote_dir.get().strip(), rc)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._log(f"[FEHLER] {exc}\n", "error")
|
self._log(f"[FEHLER] {exc}\n", "error")
|
||||||
|
self._current_task = None
|
||||||
|
|
||||||
def _kill(self):
|
def _kill(self):
|
||||||
if self._current_task:
|
if self._current_task:
|
||||||
@@ -882,6 +1214,132 @@ class AstrometryGUI:
|
|||||||
if not current or (current.endswith(".fits") and current.startswith(out_dir)):
|
if not current or (current.endswith(".fits") and current.startswith(out_dir)):
|
||||||
self.v_new_fits.set(auto_path)
|
self.v_new_fits.set(auto_path)
|
||||||
|
|
||||||
|
def _on_local_file_changed(self, *_):
|
||||||
|
path = self.v_local_file.get().strip()
|
||||||
|
if not path or not os.path.isfile(path):
|
||||||
|
self.local_image_size_label.config(text="Größe: —")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
width, height = self._read_image_size(path)
|
||||||
|
self.local_image_size_label.config(text=f"Größe: {width} × {height}")
|
||||||
|
except Exception:
|
||||||
|
self.local_image_size_label.config(text="Größe: unbekannt")
|
||||||
|
if self.v_plot_scale_auto.get():
|
||||||
|
self._recalculate_plot_scale()
|
||||||
|
|
||||||
|
def _on_plot_scale_settings_changed(self, *_):
|
||||||
|
if self.v_plot_scale_auto.get():
|
||||||
|
self._recalculate_plot_scale()
|
||||||
|
|
||||||
|
def _recalculate_plot_scale(self):
|
||||||
|
path = self.v_local_file.get().strip()
|
||||||
|
if not path or not os.path.isfile(path):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
target = float(self.v_plot_target_long_edge.get().replace(",", "."))
|
||||||
|
if target <= 0:
|
||||||
|
return
|
||||||
|
width, height = self._read_image_size(path)
|
||||||
|
long_edge = max(width, height)
|
||||||
|
if long_edge <= 0:
|
||||||
|
return
|
||||||
|
scale = target / long_edge
|
||||||
|
self.v_plot_scale.set(f"{scale:.3f}".rstrip("0").rstrip("."))
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
def _read_image_size(self, path: str) -> tuple[int, int]:
|
||||||
|
ext = os.path.splitext(path)[1].lower()
|
||||||
|
if ext in (".fits", ".fit", ".fts"):
|
||||||
|
return self._read_fits_size(path)
|
||||||
|
if ext == ".png":
|
||||||
|
return self._read_png_size(path)
|
||||||
|
if ext in (".jpg", ".jpeg"):
|
||||||
|
return self._read_jpeg_size(path)
|
||||||
|
if ext == ".gif":
|
||||||
|
return self._read_gif_size(path)
|
||||||
|
if ext == ".bmp":
|
||||||
|
return self._read_bmp_size(path)
|
||||||
|
raise ValueError("Nicht unterstütztes Bildformat")
|
||||||
|
|
||||||
|
def _read_fits_size(self, path: str) -> tuple[int, int]:
|
||||||
|
width = height = None
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
while True:
|
||||||
|
block = f.read(2880)
|
||||||
|
if not block:
|
||||||
|
break
|
||||||
|
for i in range(0, len(block), 80):
|
||||||
|
card = block[i:i + 80].decode("ascii", errors="ignore")
|
||||||
|
key = card[:8].strip()
|
||||||
|
if key == "END":
|
||||||
|
if width and height:
|
||||||
|
return width, height
|
||||||
|
break
|
||||||
|
if "=" not in card:
|
||||||
|
continue
|
||||||
|
value = card[10:30].split("/")[0].strip()
|
||||||
|
if key == "NAXIS1":
|
||||||
|
width = int(value)
|
||||||
|
elif key == "NAXIS2":
|
||||||
|
height = int(value)
|
||||||
|
raise ValueError("FITS-Bildgröße nicht lesbar")
|
||||||
|
|
||||||
|
def _read_png_size(self, path: str) -> tuple[int, int]:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
header = f.read(24)
|
||||||
|
if header[:8] != b"\x89PNG\r\n\x1a\n":
|
||||||
|
raise ValueError("Ungültige PNG-Datei")
|
||||||
|
return struct.unpack(">II", header[16:24])
|
||||||
|
|
||||||
|
def _read_gif_size(self, path: str) -> tuple[int, int]:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
header = f.read(10)
|
||||||
|
if header[:6] not in (b"GIF87a", b"GIF89a"):
|
||||||
|
raise ValueError("Ungültige GIF-Datei")
|
||||||
|
return struct.unpack("<HH", header[6:10])
|
||||||
|
|
||||||
|
def _read_bmp_size(self, path: str) -> tuple[int, int]:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
header = f.read(26)
|
||||||
|
if header[:2] != b"BM":
|
||||||
|
raise ValueError("Ungültige BMP-Datei")
|
||||||
|
width, height = struct.unpack("<ii", header[18:26])
|
||||||
|
return abs(width), abs(height)
|
||||||
|
|
||||||
|
def _read_jpeg_size(self, path: str) -> tuple[int, int]:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
if f.read(2) != b"\xff\xd8":
|
||||||
|
raise ValueError("Ungültige JPEG-Datei")
|
||||||
|
while True:
|
||||||
|
marker_prefix = f.read(1)
|
||||||
|
if not marker_prefix:
|
||||||
|
break
|
||||||
|
if marker_prefix != b"\xff":
|
||||||
|
continue
|
||||||
|
marker = f.read(1)
|
||||||
|
while marker == b"\xff":
|
||||||
|
marker = f.read(1)
|
||||||
|
if marker in (b"\xd8", b"\xd9"):
|
||||||
|
continue
|
||||||
|
size_bytes = f.read(2)
|
||||||
|
if len(size_bytes) != 2:
|
||||||
|
break
|
||||||
|
segment_size = struct.unpack(">H", size_bytes)[0]
|
||||||
|
if marker in (
|
||||||
|
b"\xc0", b"\xc1", b"\xc2", b"\xc3",
|
||||||
|
b"\xc5", b"\xc6", b"\xc7",
|
||||||
|
b"\xc9", b"\xca", b"\xcb",
|
||||||
|
b"\xcd", b"\xce", b"\xcf",
|
||||||
|
):
|
||||||
|
data = f.read(5)
|
||||||
|
if len(data) != 5:
|
||||||
|
break
|
||||||
|
height, width = struct.unpack(">HH", data[1:5])
|
||||||
|
return width, height
|
||||||
|
f.seek(segment_size - 2, os.SEEK_CUR)
|
||||||
|
raise ValueError("JPEG-Bildgröße nicht lesbar")
|
||||||
|
|
||||||
def _browse_local(self):
|
def _browse_local(self):
|
||||||
path = filedialog.askopenfilename(
|
path = filedialog.askopenfilename(
|
||||||
title="Astro-Bild auswählen",
|
title="Astro-Bild auswählen",
|
||||||
@@ -892,6 +1350,8 @@ class AstrometryGUI:
|
|||||||
)
|
)
|
||||||
if path:
|
if path:
|
||||||
self.v_local_file.set(path)
|
self.v_local_file.set(path)
|
||||||
|
if messagebox.askyesno("Datei hochladen", "Soll die ausgewählte Datei jetzt direkt auf den Server hochgeladen werden?"):
|
||||||
|
self._upload()
|
||||||
|
|
||||||
def _refresh_cmd(self):
|
def _refresh_cmd(self):
|
||||||
cmd = self._build_command()
|
cmd = self._build_command()
|
||||||
@@ -902,6 +1362,110 @@ class AstrometryGUI:
|
|||||||
self.root.clipboard_clear()
|
self.root.clipboard_clear()
|
||||||
self.root.clipboard_append(self.cmd_text.get("1.0", tk.END).strip())
|
self.root.clipboard_append(self.cmd_text.get("1.0", tk.END).strip())
|
||||||
|
|
||||||
|
def _encrypt_text(self, text: str) -> str:
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
key = b"AstrometryGuiInternalKey-2026"
|
||||||
|
plain = text.encode("utf-8")
|
||||||
|
encrypted = bytes(b ^ key[i % len(key)] for i, b in enumerate(plain))
|
||||||
|
return base64.b64encode(encrypted).decode("ascii")
|
||||||
|
|
||||||
|
def _decrypt_text(self, token: str) -> str:
|
||||||
|
if not token:
|
||||||
|
return ""
|
||||||
|
key = b"AstrometryGuiInternalKey-2026"
|
||||||
|
encrypted = base64.b64decode(token.encode("ascii"))
|
||||||
|
plain = bytes(b ^ key[i % len(key)] for i, b in enumerate(encrypted))
|
||||||
|
return plain.decode("utf-8")
|
||||||
|
|
||||||
|
def _collect_profile_data(self) -> dict:
|
||||||
|
vars_data = {}
|
||||||
|
for name, value in self.__dict__.items():
|
||||||
|
if name.startswith("v_") and hasattr(value, "get"):
|
||||||
|
vars_data[name] = value.get()
|
||||||
|
if "v_pass" in vars_data:
|
||||||
|
vars_data["v_pass"] = ""
|
||||||
|
return {
|
||||||
|
"format": 2,
|
||||||
|
"variables": vars_data,
|
||||||
|
"secrets": {
|
||||||
|
"v_pass_enc": self._encrypt_text(self.v_pass.get()),
|
||||||
|
},
|
||||||
|
"idx_text": self.idx_text.get("1.0", tk.END).strip(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _apply_profile_data(self, data: dict):
|
||||||
|
vars_data = data.get("variables", {})
|
||||||
|
for name, value in vars_data.items():
|
||||||
|
var = getattr(self, name, None)
|
||||||
|
if var is not None and hasattr(var, "set"):
|
||||||
|
var.set(value)
|
||||||
|
|
||||||
|
secrets = data.get("secrets", {})
|
||||||
|
encrypted_pass = secrets.get("v_pass_enc") or secrets.get("v_pass_dpapi")
|
||||||
|
if encrypted_pass:
|
||||||
|
try:
|
||||||
|
self.v_pass.set(self._decrypt_text(encrypted_pass))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.idx_text.delete("1.0", tk.END)
|
||||||
|
idx_text = data.get("idx_text", "")
|
||||||
|
if idx_text:
|
||||||
|
self.idx_text.insert("1.0", idx_text)
|
||||||
|
|
||||||
|
self._refresh_cmd()
|
||||||
|
|
||||||
|
def _save_profile(self):
|
||||||
|
profile_dir = os.path.join(os.getcwd(), "profiles")
|
||||||
|
os.makedirs(profile_dir, exist_ok=True)
|
||||||
|
path = filedialog.asksaveasfilename(
|
||||||
|
title="Profil speichern",
|
||||||
|
initialdir=profile_dir,
|
||||||
|
defaultextension=".json",
|
||||||
|
filetypes=[("JSON-Dateien", "*.json"), ("Alle Dateien", "*.*")],
|
||||||
|
)
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
data = self._collect_profile_data()
|
||||||
|
try:
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
self._log(f"[OK] Profil gespeichert: {path}\n", "ok")
|
||||||
|
except Exception as exc:
|
||||||
|
messagebox.showerror("Profil speichern", f"Speichern fehlgeschlagen:\n{exc}")
|
||||||
|
|
||||||
|
def _load_profile(self):
|
||||||
|
profile_dir = os.path.join(os.getcwd(), "profiles")
|
||||||
|
path = filedialog.askopenfilename(
|
||||||
|
title="Profil laden",
|
||||||
|
initialdir=profile_dir if os.path.isdir(profile_dir) else os.getcwd(),
|
||||||
|
filetypes=[("JSON-Dateien", "*.json"), ("Alle Dateien", "*.*")],
|
||||||
|
)
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
self._apply_profile_data(data)
|
||||||
|
self._log(f"[OK] Profil geladen: {path}\n", "ok")
|
||||||
|
except Exception as exc:
|
||||||
|
messagebox.showerror("Profil laden", f"Laden fehlgeschlagen:\n{exc}")
|
||||||
|
|
||||||
|
def _menu_load_credentials(self):
|
||||||
|
path = filedialog.askopenfilename(
|
||||||
|
title="Zugangsdaten laden",
|
||||||
|
initialdir=os.getcwd(),
|
||||||
|
filetypes=[("JSON-Dateien", "*.json"), ("Alle Dateien", "*.*")],
|
||||||
|
)
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self._load_credentials_from_file(path)
|
||||||
|
self._log(f"[OK] Zugangsdaten geladen: {path}\n", "ok")
|
||||||
|
except Exception as exc:
|
||||||
|
messagebox.showerror("Zugangsdaten laden", f"Laden fehlgeschlagen:\n{exc}")
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
# Log (thread-sicher via Queue)
|
# Log (thread-sicher via Queue)
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"format": 2,
|
||||||
|
"variables": {
|
||||||
|
"v_host": "10.42.44.25",
|
||||||
|
"v_port": 22,
|
||||||
|
"v_user": "eskimue",
|
||||||
|
"v_pass": "",
|
||||||
|
"v_local_file": "",
|
||||||
|
"v_remote_dir": "/tmp/astrometry",
|
||||||
|
"v_input_file": "",
|
||||||
|
"v_out_prefix": "",
|
||||||
|
"v_out_dir": "/tmp/astrometry",
|
||||||
|
"v_new_fits": "",
|
||||||
|
"v_wcs": "",
|
||||||
|
"v_corr": "",
|
||||||
|
"v_rdls": "",
|
||||||
|
"v_axy": "",
|
||||||
|
"v_solved": "",
|
||||||
|
"v_match": "",
|
||||||
|
"v_scale_units": "degwidth",
|
||||||
|
"v_scale_low": "20",
|
||||||
|
"v_scale_high": "30",
|
||||||
|
"v_scale_est": "",
|
||||||
|
"v_scale_err": "",
|
||||||
|
"v_ra": "",
|
||||||
|
"v_dec": "",
|
||||||
|
"v_radius": "1.0",
|
||||||
|
"v_depth": "",
|
||||||
|
"v_cpulimit": "300",
|
||||||
|
"v_plot_scale": "",
|
||||||
|
"v_plot_scale_auto": true,
|
||||||
|
"v_plot_target_long_edge": "900",
|
||||||
|
"v_downsample": "6",
|
||||||
|
"v_tweak_order": "2",
|
||||||
|
"v_odds_solve": "",
|
||||||
|
"v_odds_reject": "",
|
||||||
|
"v_odds_tune": "",
|
||||||
|
"v_objs": "",
|
||||||
|
"v_pixel_error": "",
|
||||||
|
"v_parity": "both",
|
||||||
|
"v_overwrite": true,
|
||||||
|
"v_no_plots": false,
|
||||||
|
"v_no_tweak": false,
|
||||||
|
"v_resort": false,
|
||||||
|
"v_verbose": false,
|
||||||
|
"v_timestamp": true,
|
||||||
|
"v_no_del_temp": false,
|
||||||
|
"v_continue": false,
|
||||||
|
"v_sextractor": false,
|
||||||
|
"v_fits_image": false,
|
||||||
|
"v_no_verify": false,
|
||||||
|
"v_just_augment": false,
|
||||||
|
"v_invert": true,
|
||||||
|
"v_extra": ""
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
"v_pass_enc": "EQERAQYJAA=="
|
||||||
|
},
|
||||||
|
"idx_text": ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"format": 2,
|
||||||
|
"variables": {
|
||||||
|
"v_host": "10.42.44.25",
|
||||||
|
"v_port": 22,
|
||||||
|
"v_user": "eskimue",
|
||||||
|
"v_pass": "",
|
||||||
|
"v_local_file": "C:/Users/ich/Cloud__Museum/Museumsentwicklung/Shop/Faksimile/2026/Heinlein Dieter/IMG_410328-1956_08_30-20000+2000-Te3-26-50.png",
|
||||||
|
"v_remote_dir": "/tmp/astrometry",
|
||||||
|
"v_input_file": "/tmp/astrometry/IMG_410328-1956_08_30-20000+2000-Te3-26-50.png",
|
||||||
|
"v_out_prefix": "",
|
||||||
|
"v_out_dir": "/tmp/astrometry",
|
||||||
|
"v_new_fits": "/tmp/astrometry/IMG_410328-1956_08_30-20000+2000-Te3-26-50.fits",
|
||||||
|
"v_wcs": "",
|
||||||
|
"v_corr": "",
|
||||||
|
"v_rdls": "",
|
||||||
|
"v_axy": "",
|
||||||
|
"v_solved": "",
|
||||||
|
"v_match": "",
|
||||||
|
"v_scale_units": "degwidth",
|
||||||
|
"v_scale_low": "20",
|
||||||
|
"v_scale_high": "30",
|
||||||
|
"v_scale_est": "",
|
||||||
|
"v_scale_err": "",
|
||||||
|
"v_ra": "",
|
||||||
|
"v_dec": "",
|
||||||
|
"v_radius": "1.0",
|
||||||
|
"v_depth": "",
|
||||||
|
"v_cpulimit": "300",
|
||||||
|
"v_plot_scale": "0.15",
|
||||||
|
"v_plot_scale_auto": true,
|
||||||
|
"v_plot_target_long_edge": "900",
|
||||||
|
"v_downsample": "6",
|
||||||
|
"v_tweak_order": "2",
|
||||||
|
"v_odds_solve": "",
|
||||||
|
"v_odds_reject": "",
|
||||||
|
"v_odds_tune": "",
|
||||||
|
"v_objs": "",
|
||||||
|
"v_pixel_error": "",
|
||||||
|
"v_parity": "both",
|
||||||
|
"v_overwrite": true,
|
||||||
|
"v_no_plots": false,
|
||||||
|
"v_no_tweak": false,
|
||||||
|
"v_resort": false,
|
||||||
|
"v_verbose": false,
|
||||||
|
"v_timestamp": true,
|
||||||
|
"v_no_del_temp": false,
|
||||||
|
"v_continue": false,
|
||||||
|
"v_sextractor": false,
|
||||||
|
"v_fits_image": false,
|
||||||
|
"v_no_verify": false,
|
||||||
|
"v_just_augment": false,
|
||||||
|
"v_invert": true,
|
||||||
|
"v_extra": ""
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
"v_pass_enc": "EQERAQYJAA=="
|
||||||
|
},
|
||||||
|
"idx_text": ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"host": "10.42.44.25",
|
||||||
|
"port": 22,
|
||||||
|
"user": "eskimue",
|
||||||
|
"password": "Preside"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user