Files

325 lines
14 KiB
Python

"""
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("<FocusOut>", 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()