Files
feelGrib/iface.py

350 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
from PIL import Image, ImageTk
import threading
import os
import platform
import subprocess
import configparser
from math import log, tan, radians, pi, degrees, atan, sinh
from downloader import CopernicusDownloader
CONFIG_FILE = "config.ini"
class MapSelector:
def __init__(self, root):
self.root = root
self.root.title("Copernicus Marine Downloader")
self.config = configparser.ConfigParser()
self.load_config()
self.notebook = ttk.Notebook(self.root)
self.notebook.pack(fill=tk.BOTH, expand=True)
self.frame_download = ttk.Frame(self.notebook)
self.frame_config = ttk.Frame(self.notebook)
self.notebook.add(self.frame_download, text="Téléchargement")
self.notebook.add(self.frame_config, text="Configuration")
self.downloader = CopernicusDownloader(
username=self.config.get("AUTH", "username", fallback=""),
password=self.config.get("AUTH", "password", fallback="")
)
self.initialized = False
self.scale = 1.0 # Facteur de zoom initial
self.min_scale = 0.3 # Zoom minimum
self.max_scale = 2.0 # Zoom maximum
self.offset_x = 0 # Décalage horizontal
self.offset_y = 0 # Décalage vertical
self.start_pan_x = 0 # Position initiale du clic pour le déplacement
self.start_pan_y = 0
self.setup_download_tab()
self.setup_config_tab()
def load_config(self):
if not os.path.exists(CONFIG_FILE):
self.config["AUTH"] = {"username": "", "password": ""}
with open(CONFIG_FILE, "w") as f:
self.config.write(f)
else:
self.config.read(CONFIG_FILE)
def save_config(self):
with open(CONFIG_FILE, "w") as f:
self.config.write(f)
def setup_download_tab(self):
# Canvas et image
self.canvas = tk.Canvas(self.frame_download, cursor="cross")
self.canvas.pack(fill=tk.BOTH, expand=True)
filename = self.config['map']['filename'].strip('"')
self.original_img = Image.open(filename)
self.display_img = self.original_img.copy()
self.tk_img = ImageTk.PhotoImage(self.display_img)
self.img_id = self.canvas.create_image(self.offset_x, self.offset_y, anchor="nw", image=self.tk_img)
self.canvas.bind("<Configure>", self.on_canvas_configure)
self.canvas.bind("<Button-1>", self.on_click)
self.canvas.bind("<B1-Motion>", self.on_drag)
self.canvas.bind("<ButtonRelease-1>", self.on_release)
self.canvas.bind("<MouseWheel>", self.setzoom) # Windows
self.canvas.bind("<Button-4>", self.setzoom) # Linux scroll up
self.canvas.bind("<Button-5>", self.setzoom) # Linux scroll down
self.canvas.bind("<ButtonPress-3>", self.start_pan)
self.canvas.bind("<B3-Motion>", self.pan_image)
self.start_x = self.start_y = None
self.rect = None
coord_frame = ttk.Frame(self.frame_download)
coord_frame.pack(pady=5)
self.lat1_var = tk.StringVar()
self.lon1_var = tk.StringVar()
self.lat2_var = tk.StringVar()
self.lon2_var = tk.StringVar()
ttk.Label(coord_frame, text="Lat Min:").grid(row=0, column=0)
ttk.Entry(coord_frame, textvariable=self.lat1_var, width=10).grid(row=0, column=1)
ttk.Label(coord_frame, text="Lon Min:").grid(row=0, column=2)
ttk.Entry(coord_frame, textvariable=self.lon1_var, width=10).grid(row=0, column=3)
ttk.Label(coord_frame, text="Lat Max:").grid(row=1, column=0)
ttk.Entry(coord_frame, textvariable=self.lat2_var, width=10).grid(row=1, column=1)
ttk.Label(coord_frame, text="Lon Max:").grid(row=1, column=2)
ttk.Entry(coord_frame, textvariable=self.lon2_var, width=10).grid(row=1, column=3)
self.duration_var = tk.StringVar(value="1")
ttk.Label(self.frame_download, text="Durée (jours):").pack()
ttk.Combobox(self.frame_download, textvariable=self.duration_var, values=["1", "2", "3", "4", "5"], width=5).pack(pady=5)
btn_frame = ttk.Frame(self.frame_download)
btn_frame.pack(pady=5)
ttk.Button(btn_frame, text="Télécharger", command=self.start_download).grid(row=0, column=1, padx=5)
ttk.Button(btn_frame, text="Ouvrir le dossier", command=self.open_download_folder).grid(row=0, column=2, padx=5)
self.status = ttk.Label(self.frame_download, text="")
self.status.pack()
self.progress = ttk.Progressbar(self.frame_download, orient="horizontal", length=300, mode="indeterminate")
self.progress.pack(pady=5)
self.display_image()
def on_canvas_configure(self, event):
if not self.initialized:
self.canvas_width = event.width
self.canvas_height = event.height
#print(f"configure canvas {self.original_img.width}")
factor = self.canvas_width / self.original_img.width
self.zoom(factor,0,-round(self.canvas_height / 2)) # Affichage initial sans zoom
self.initialized = True
def start_pan(self, event):
self.start_pan_x = event.x
self.start_pan_y = event.y
def pan_image(self, event):
dx = event.x - self.start_pan_x
dy = event.y - self.start_pan_y
self.offset_x += dx
self.offset_y += dy
self.canvas.move(self.img_id, dx, dy)
self.start_pan_x = event.x
self.start_pan_y = event.y
self.redraw_rectangle_from_coords()
def setzoom(self, event):
# Déterminer la direction du zoom
if event.num == 5 or event.delta == -120:
zoom_factor = 0.9
elif event.num == 4 or event.delta == 120:
zoom_factor = 1.1
else:
return
self.zoom(zoom_factor,event.x,event.y)
def zoom(self, zoom_factor, zoom_x=200, zoom_y=200):
# Calculer le nouveau facteur de zoom
new_scale = self.scale * zoom_factor
if self.min_scale <= new_scale <= self.max_scale:
self.scale = new_scale
# Redimensionner l'image
width = int(self.original_img.width * self.scale)
height = int(self.original_img.height * self.scale)
self.display_img = self.original_img.resize((width, height), Image.Resampling.LANCZOS)
self.tk_img = ImageTk.PhotoImage(self.display_img)
# Mettre à jour l'image sur le canvas
self.canvas.itemconfig(self.img_id, image=self.tk_img)
# Ajuster la position de l'image pour centrer le zoom sur le pointeur de la souris
canvas_coords = self.canvas.coords(self.img_id)
mouse_x = self.canvas.canvasx(zoom_x)
mouse_y = self.canvas.canvasy(zoom_y)
self.offset_x = mouse_x - (mouse_x - canvas_coords[0]) * zoom_factor
self.offset_y = mouse_y - (mouse_y - canvas_coords[1]) * zoom_factor
self.canvas.coords(self.img_id, self.offset_x, self.offset_y)
self.redraw_rectangle_from_coords()
def setup_config_tab(self):
frame = self.frame_config
ttk.Label(frame, text="Nom d'utilisateur Copernicus Marine:").pack(pady=5)
self.username_var = tk.StringVar(value=self.config.get("AUTH", "username", fallback=""))
ttk.Entry(frame, textvariable=self.username_var, width=30).pack()
ttk.Label(frame, text="Mot de passe:").pack(pady=5)
self.password_var = tk.StringVar(value=self.config.get("AUTH", "password", fallback=""))
ttk.Entry(frame, textvariable=self.password_var, show="*", width=30).pack()
ttk.Button(frame, text="Sauvegarder", command=self.save_credentials).pack(pady=10)
def save_credentials(self):
self.config["AUTH"]["username"] = self.username_var.get()
self.config["AUTH"]["password"] = self.password_var.get()
self.save_config()
messagebox.showinfo("Info", "Identifiants sauvegardés avec succès.")
# Met à jour le downloader avec les nouveaux identifiants
self.downloader.username = self.username_var.get()
self.downloader.password = self.password_var.get()
def redraw_rectangle_from_coords(self):
try:
lat1 = float(self.lat1_var.get())
lat2 = float(self.lat2_var.get())
lon1 = float(self.lon1_var.get())
lon2 = float(self.lon2_var.get())
# Convert lat/lon to canvas pixel coordinates
x1, y1 = self.xy_from_latlon(lat1, lon1)
x2, y2 = self.xy_from_latlon(lat2, lon2)
# Supprimer l'ancien rectangle s'il existe
if self.rect:
self.canvas.delete(self.rect)
self.rect = self.canvas.create_rectangle(x1, y1, x2, y2, outline="red", width=2)
except ValueError:
# Ignore si les champs ne sont pas valides
pass
def xy_from_latlon(self, lat, lon):
lat_min = self.config.getfloat('map', 'map_lat_start')
lat_max = self.config.getfloat('map', 'map_lat_end')
lon_min = self.config.getfloat('map', 'map_lon_start')
lon_max = self.config.getfloat('map', 'map_lon_end')
canvas_w = self.canvas.winfo_width()
canvas_h = self.canvas.winfo_height()
print(f"canvas = {canvas_w} {canvas_h} scale = {self.scale} offset = {self.offset_x} {self.offset_y}")
# Longitude (linéaire)
x_norm = (lon - lon_min) / (lon_max - lon_min)
x = x_norm * self.scale * self.original_img.width + self.offset_x
# Latitude (projection Mercator)
mercator_min = log(tan(pi / 4 + radians(lat_min) / 2)) # Mercator
mercator_max = log(tan(pi / 4 + radians(lat_max) / 2)) # Mercator
mercator_lat = log(tan(pi / 4 + radians(lat) / 2)) # Mercator
y_norm = (mercator_max - mercator_lat) / (mercator_max - mercator_min)
y = y_norm * self.scale * self.original_img.height + self.offset_y
print(f"xy_from_latlon : {x} - {y}")
return x, y
def latlon_from_xy(self, x, y):
# Coordonnées géographiques de la carte
lat_min = self.config.getfloat('map', 'map_lat_start')
lat_max = self.config.getfloat('map', 'map_lat_end')
lon_min = self.config.getfloat('map', 'map_lon_start')
lon_max = self.config.getfloat('map', 'map_lon_end')
print(f"latlon_from_xy dbg coord = {lat_min}-{lat_max}-{lon_min}-{lon_max}")
# Calculer la position relative dans limage (sans offset/zoom)
img_x = (x - self.offset_x) / self.scale
img_y = (y - self.offset_y) / self.scale
if 0 <= img_x <= self.original_img.width and 0 <= img_y <= self.original_img.height:
rel_x = img_x / self.original_img.width
rel_y = img_y / self.original_img.height
# Longitude : linéaire
lon = lon_min + rel_x * (lon_max - lon_min)
# Latitude : projection inverse de Mercator
merc_min = log(tan(pi / 4 + radians(lat_min) / 2)) # Mercator
merc_max = log(tan(pi / 4 + radians(lat_max) / 2)) # Mercator
merc_y = merc_max - rel_y * (merc_max - merc_min)
lat = degrees(atan(sinh(merc_y))) # Mercator Inverse
print(f"latlon_from_xy : {lat} - {lon}")
return lat, lon
else:
return None, None # En dehors de l'image
def on_click(self, event):
print(f"dbg on_click : {event.x} - {event.y}")
self.start_x, self.start_y = event.x, event.y
if self.rect:
self.canvas.delete(self.rect)
self.rect = self.canvas.create_rectangle(self.start_x, self.start_y, event.x, event.y, outline="red")
def on_drag(self, event):
if self.rect:
self.canvas.coords(self.rect, self.start_x, self.start_y, event.x, event.y)
def on_release(self, event):
print(f"dbg 1 {self.start_x} - {self.start_y} -- {event.x} - {event.y}\n")
lat1, lon1 = self.latlon_from_xy(self.start_x, self.start_y)
lat2, lon2 = self.latlon_from_xy(event.x, event.y)
print("dbg 2\n")
self.lat1_var.set(f"{min(lat1, lat2):.4f}")
self.lat2_var.set(f"{max(lat1, lat2):.4f}")
self.lon1_var.set(f"{min(lon1, lon2):.4f}")
self.lon2_var.set(f"{max(lon1, lon2):.4f}")
def display_image(self):
self.canvas_width = self.canvas.winfo_width() or self.original_img.width
self.canvas_height = self.canvas.winfo_height() or self.original_img.height
resized_img = self.original_img.resize((self.canvas_width, self.canvas_height), Image.Resampling.LANCZOS)
self.tk_img = ImageTk.PhotoImage(resized_img)
self.canvas.itemconfig(self.img_id, image=self.tk_img)
def start_download(self):
try:
lat_min = float(self.lat1_var.get())
lat_max = float(self.lat2_var.get())
lon_min = float(self.lon1_var.get())
lon_max = float(self.lon2_var.get())
days = int(self.duration_var.get())
except ValueError:
messagebox.showerror("Erreur", "Veuillez entrer des coordonnées et durée valides.")
return
self.progress.start(10)
self.status.config(text="Téléchargement en cours...")
threading.Thread(target=self.download, args=(lat_min, lat_max, lon_min, lon_max, days), daemon=True).start()
def download(self, lat_min, lat_max, lon_min, lon_max, days):
try:
self.downloader.download(lat_min, lat_max, lon_min, lon_max, days)
self.status.config(text="Conversion en GRIB2 en cours...")
grib_path = self.downloader.convert_to_grib2()
self.status.config(text=f"Téléchargement et conversion terminés : {os.path.basename(grib_path)}")
except Exception as e:
self.status.config(text=f"Erreur : {e}")
finally:
self.progress.stop()
def open_download_folder(self):
folder_path = os.path.join(os.getcwd(), "downloads")
if not os.path.exists(folder_path):
os.makedirs(folder_path)
if platform.system() == "Windows":
os.startfile(folder_path)
elif platform.system() == "Darwin": # macOS
subprocess.run(["open", folder_path])
else: # Linux
subprocess.run(["xdg-open", folder_path])
if __name__ == "__main__":
root = tk.Tk()
root.geometry("900x600")
app = MapSelector(root)
root.mainloop()