""" QGISS57Converter — GUI Corre el converter en el mismo proceso (sin subprocess ni conda). Completamente portable: solo necesita este .exe. """ import io import os import queue import sys as _sys import threading import tkinter as tk from tkinter import filedialog, messagebox, scrolledtext from pathlib import Path # ── paths ───────────────────────────────────────────────────────────────────── if getattr(_sys, "frozen", False): SCRIPT_DIR = Path(_sys._MEIPASS) else: SCRIPT_DIR = Path(__file__).parent _sys.path.insert(0, str(SCRIPT_DIR)) # Tell pyproj where its PROJ data is (bundled inside the exe) if getattr(_sys, "frozen", False): os.environ["PROJ_DATA"] = str(SCRIPT_DIR / "proj_data") os.environ["PROJ_LIB"] = str(SCRIPT_DIR / "proj_data") CONFIG = SCRIPT_DIR / "cell_config.json" REF_PDF = SCRIPT_DIR / "CAPAS_REFERENCIA.pdf" MANUAL_HTML = SCRIPT_DIR / "MANUAL.html" # Import converter (bundled alongside gui in the exe) from converter import convert as _do_convert # ── theme ───────────────────────────────────────────────────────────────────── BG = "#1e2532" PANEL = "#252d3d" ACCENT = "#1e7fc8" ACCENT_HOV = "#1565a0" TEXT_LIGHT = "#e8eaf0" TEXT_DIM = "#8a94aa" GREEN = "#2ecc71" RED = "#e74c3c" FONT_UI = ("Segoe UI", 10) FONT_MONO = ("Consolas", 9) # ── stdout capture ──────────────────────────────────────────────────────────── class _QueueWriter: """Redirects print() output to a queue for the GUI to consume.""" def __init__(self, q: queue.Queue): self._q = q def write(self, text): if text: self._q.put(text) def flush(self): pass def isatty(self): return False # ── app ─────────────────────────────────────────────────────────────────────── class App(tk.Tk): def __init__(self): super().__init__() self.title("QGISS57Converter") self.geometry("720x540") self.resizable(True, True) self.minsize(600, 440) self.configure(bg=BG) self._converting = False self._log_queue = queue.Queue() self._cancel_flag = threading.Event() self._build_ui() # ── UI ──────────────────────────────────────────────────────────────────── def _build_ui(self): hdr = tk.Frame(self, bg=ACCENT, height=48) hdr.pack(fill="x") tk.Label(hdr, text="QGIS -> S-57 ENC Converter", bg=ACCENT, fg="white", font=("Segoe UI", 13, "bold")).pack(side="left", padx=16, pady=10) tk.Button(hdr, text="? Manual de usuario", bg=ACCENT_HOV, fg="white", relief="flat", font=("Segoe UI", 9), cursor="hand2", bd=0, padx=8, command=self._open_manual).pack(side="right", padx=4, pady=10) tk.Button(hdr, text="? Referencia de capas", bg=ACCENT_HOV, fg="white", relief="flat", font=("Segoe UI", 9), cursor="hand2", bd=0, padx=8, command=self._open_ref).pack(side="right", padx=4, pady=10) body = tk.Frame(self, bg=BG) body.pack(fill="both", expand=True, padx=20, pady=16) body.columnconfigure(0, weight=1) # entrada tk.Label(body, text="Proyecto QGIS (.qgz / .qgs)", bg=BG, fg=TEXT_DIM, font=FONT_UI).grid(row=0, column=0, sticky="w") row_in = tk.Frame(body, bg=BG) row_in.grid(row=1, column=0, sticky="ew", pady=(2, 10)) self._var_in = tk.StringVar() self._ent_in = tk.Entry(row_in, textvariable=self._var_in, bg=PANEL, fg=TEXT_LIGHT, insertbackground=TEXT_LIGHT, relief="flat", font=FONT_UI, bd=6) self._ent_in.pack(side="left", fill="x", expand=True) self._ent_in.bind("", lambda _: self._auto_output()) tk.Button(row_in, text="Examinar...", bg=ACCENT, fg="white", relief="flat", font=FONT_UI, cursor="hand2", bd=0, padx=10, command=self._pick_input).pack(side="left", padx=(6, 0)) # salida tk.Label(body, text="Archivo de salida (.000)", bg=BG, fg=TEXT_DIM, font=FONT_UI).grid(row=2, column=0, sticky="w") row_out = tk.Frame(body, bg=BG) row_out.grid(row=3, column=0, sticky="ew", pady=(2, 10)) self._var_out = tk.StringVar() tk.Entry(row_out, textvariable=self._var_out, bg=PANEL, fg=TEXT_LIGHT, insertbackground=TEXT_LIGHT, relief="flat", font=FONT_UI, bd=6).pack(side="left", fill="x", expand=True) tk.Button(row_out, text="Guardar en...", bg=PANEL, fg=TEXT_LIGHT, relief="flat", font=FONT_UI, cursor="hand2", bd=0, padx=10, command=self._pick_output).pack(side="left", padx=(6, 0)) # directorio extra de CSVs (opcional) tk.Label(body, text="Directorio extra de CSVs (opcional — boyas, luces, etc.)", bg=BG, fg=TEXT_DIM, font=FONT_UI).grid(row=4, column=0, sticky="w") row_csv = tk.Frame(body, bg=BG) row_csv.grid(row=5, column=0, sticky="ew", pady=(2, 4)) self._var_csv = tk.StringVar() self._ent_csv = tk.Entry(row_csv, textvariable=self._var_csv, bg=PANEL, fg=TEXT_LIGHT, insertbackground=TEXT_LIGHT, relief="flat", font=FONT_UI, bd=6) self._ent_csv.pack(side="left", fill="x", expand=True) tk.Button(row_csv, text="Examinar...", bg=PANEL, fg=TEXT_LIGHT, relief="flat", font=FONT_UI, cursor="hand2", bd=0, padx=10, command=self._pick_csv_dir).pack(side="left", padx=(6, 0)) tk.Button(row_csv, text="✕", bg=PANEL, fg=TEXT_DIM, relief="flat", font=("Segoe UI", 9), cursor="hand2", bd=0, padx=6, command=lambda: self._var_csv.set("")).pack(side="left", padx=(2, 0)) tk.Label(body, text="Los archivos .csv en ese directorio se incluyen como capas extra " "(nombre del archivo = clase S-57, ej. BOYLAT.csv → BOYLAT).", bg=BG, fg=TEXT_DIM, font=("Segoe UI", 8), wraplength=640, justify="left").grid(row=6, column=0, sticky="w", pady=(0, 8)) # botones row_btn = tk.Frame(body, bg=BG) row_btn.grid(row=7, column=0, sticky="ew", pady=(4, 12)) self._btn_convert = tk.Button( row_btn, text=">> Convertir", bg=GREEN, fg="white", activebackground="#27ae60", relief="flat", font=("Segoe UI", 11, "bold"), cursor="hand2", bd=0, padx=20, pady=8, command=self._start_conversion) self._btn_convert.pack(side="left") self._btn_open = tk.Button( row_btn, text="Abrir carpeta", bg=PANEL, fg=TEXT_LIGHT, relief="flat", font=FONT_UI, cursor="hand2", bd=0, padx=14, pady=8, state="disabled", command=self._open_output_dir) self._btn_open.pack(side="left", padx=(8, 0)) # estado self._lbl_status = tk.Label(body, text="Listo.", bg=BG, fg=TEXT_DIM, font=("Segoe UI", 9)) self._lbl_status.grid(row=8, column=0, sticky="w") # log tk.Label(body, text="Log de conversion", bg=BG, fg=TEXT_DIM, font=FONT_UI).grid(row=9, column=0, sticky="w", pady=(10, 2)) self._log = scrolledtext.ScrolledText( body, bg="#0d1117", fg="#c9d1d9", insertbackground="white", relief="flat", font=FONT_MONO, bd=4, height=12, wrap="word") self._log.grid(row=10, column=0, sticky="nsew") self._log.configure(state="disabled") self._log.tag_configure("green", foreground=GREEN) self._log.tag_configure("red", foreground=RED) body.rowconfigure(10, weight=1) # ── acciones ────────────────────────────────────────────────────────────── def _pick_input(self): p = filedialog.askopenfilename( title="Seleccionar proyecto QGIS", filetypes=[("QGIS project", "*.qgz *.qgs"), ("Todos", "*.*")]) if p: self._var_in.set(p) self._auto_output() def _auto_output(self): src = Path(self._var_in.get().strip()) if src.exists(): self._var_out.set(str(src.parent / (src.stem.replace(" ", "_") + ".000"))) def _pick_output(self): p = filedialog.asksaveasfilename( title="Guardar carta S-57 como", defaultextension=".000", filetypes=[("S-57 ENC", "*.000"), ("Todos", "*.*")]) if p: self._var_out.set(p) def _open_manual(self): if MANUAL_HTML.exists(): import webbrowser webbrowser.open(MANUAL_HTML.as_uri()) else: messagebox.showinfo("Manual", "No se encontro MANUAL.html.") def _open_ref(self): if REF_PDF.exists(): os.startfile(str(REF_PDF)) else: messagebox.showinfo("Referencia", "No se encontro CAPAS_REFERENCIA.pdf.") def _pick_csv_dir(self): p = filedialog.askdirectory(title="Seleccionar directorio con archivos CSV extra") if p: self._var_csv.set(p) def _open_output_dir(self): out = Path(self._var_out.get().strip()) folder = out.parent if out.parent.exists() else SCRIPT_DIR os.startfile(str(folder)) def _log_write(self, text, tag=None): self._log.configure(state="normal") self._log.insert("end", text, tag or ()) self._log.see("end") self._log.configure(state="disabled") def _set_status(self, msg, color=TEXT_DIM): self._lbl_status.configure(text=msg, fg=color) # ── conversion ──────────────────────────────────────────────────────────── def _start_conversion(self): src = self._var_in.get().strip() out = self._var_out.get().strip() csv_dir = self._var_csv.get().strip() or None if not src: messagebox.showwarning("Falta archivo", "Selecciona un archivo .qgz primero.") return if not Path(src).exists(): messagebox.showerror("No encontrado", f"No existe:\n{src}") return if csv_dir and not Path(csv_dir).is_dir(): messagebox.showerror("Directorio no existe", f"El directorio de CSVs no existe:\n{csv_dir}") return if not out: self._auto_output() out = self._var_out.get().strip() self._log.configure(state="normal") self._log.delete("1.0", "end") self._log.configure(state="disabled") self._btn_convert.configure(state="disabled") self._btn_open.configure(state="disabled") self._set_status("Convirtiendo...", ACCENT) self._converting = True # Start polling the log queue self._poll_log() threading.Thread(target=self._run_conversion, args=(src, out, csv_dir), daemon=True).start() def _poll_log(self): """Drain the log queue and update the text widget.""" try: while True: text = self._log_queue.get_nowait() self._log_write(text) except queue.Empty: pass if self._converting: self.after(40, self._poll_log) def _run_conversion(self, src: str, out: str, csv_dir=None): old_stdout = _sys.stdout old_stderr = _sys.stderr writer = _QueueWriter(self._log_queue) _sys.stdout = writer _sys.stderr = writer rc = 0 try: _do_convert( project_path = src, output_path = out, config_path = str(CONFIG), list_only = False, force = True, verbose = True, extra_csv_dir = csv_dir, ) except SystemExit as e: rc = int(e.code) if e.code is not None else 1 except Exception as e: _sys.stdout.write(f"\n[ERROR] {e}\n") rc = 1 finally: _sys.stdout = old_stdout _sys.stderr = old_stderr self.after(0, self._on_done, rc, out) def _on_done(self, rc: int, out: str): self._converting = False self._btn_convert.configure(state="normal") if rc == 0 and Path(out).exists(): size = Path(out).stat().st_size self._log_write(f"\nListo: {out} ({size:,} bytes)\n", "green") self._set_status(f"Completado: {Path(out).name}", GREEN) self._btn_open.configure(state="normal") else: self._log_write(f"\nLa conversion fallo (codigo {rc})\n", "red") self._set_status("Error en la conversion.", RED) if __name__ == "__main__": app = App() app.mainloop()