Initial commit — QGIS S-57 Converter
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user