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