325 lines
14 KiB
Python
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()
|