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("", self.on_canvas_configure) self.canvas.bind("", self.on_click) self.canvas.bind("", self.on_drag) self.canvas.bind("", self.on_release) self.canvas.bind("", self.setzoom) # Windows self.canvas.bind("", self.setzoom) # Linux scroll up self.canvas.bind("", self.setzoom) # Linux scroll down self.canvas.bind("", self.start_pan) self.canvas.bind("", 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 l’image (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()