diff --git a/DownloadCurrents.py b/DownloadCurrents.py new file mode 100644 index 0000000..fb2aafd --- /dev/null +++ b/DownloadCurrents.py @@ -0,0 +1,185 @@ +# NOTES +# +# - installer miniconda +# - ajouter conda au "PATH" +# > conda init +# > conda create -n feelgrib python=3.13 -y +# > conda activate feelgrib +# > conda install -c conda-forge pygrib requests +# > conda install conda-forge::wgrib2 +# > conda install -c conda-forge cdo + + +# WSL wsl --install +# dans terminal wsl +# apt-get install python3-grib +# apt-get install cdo + + +# telecharger https://www.ftp.cpc.ncep.noaa.gov/wd51we/wgrib2/wgrib2.tgz.v3.1.3 + + + +import os +import requests +import bz2 +import subprocess +import pygrib +import re +from datetime import date,timedelta + +def build_url(base_url, basename,previousday=0): + # today = datetime.datetime.utcnow().strftime("%d%m%y") + today = date.today() + target = (today - timedelta(previousday)).strftime("%y%m%d") + filename = f"{basename}_{target}-12.grb.bz2" + grib_filename = filename.replace(".bz2", "") + return base_url + filename, filename, grib_filename + +def download_file(url, local_filename): + print(f"Téléchargement de : {url}") + response = requests.get(url, stream=True) + if response.status_code != 200: + raise Exception(f"Erreur de téléchargement : HTTP {response.status_code}") + with open(local_filename, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + print(f"Fichier téléchargé : {local_filename}") + +def decompress_bz2(input_file, output_file): + print(f"Décompression de : {input_file}") + with bz2.open(input_file, 'rb') as f_in: + with open(output_file, 'wb') as f_out: + f_out.write(f_in.read()) + print(f"Fichier décompressé : {output_file}") + +def merge_grib_spatially_cdo(file1, file2, output_file, grid_file="target_grid.txt"): + """ + Fusionne deux fichiers GRIB1 spatialement avec CDO. + file1 : chemin du premier fichier GRIB (grille cible) + file2 : second fichier GRIB (sera interpolé sur la même grille) + output_file : nom du fichier GRIB de sortie fusionné + grid_file : nom temporaire pour stocker la définition de grille + """ + try: + # 1. Extraire la grille du fichier 1 + print(f"[CDO] Extraction de la grille de référence depuis {file1}") + subprocess.run(["cdo", "griddes", file1], check=True, stdout=open(grid_file, "w")) + + # Calcul des coordonnées + output = subprocess.check_output(['cdo', 'griddes', file1], text=True) + a_xfirst = float(re.search(r'xfirst\s*=\s*([-\d.]+)', output).group(1)) + a_xinc = float(re.search(r'xinc\s*=\s*([-\d.]+)', output).group(1)) + a_xsize = int(re.search(r'xsize\s*=\s*(\d+)', output).group(1)) + a_yfirst = float(re.search(r'yfirst\s*=\s*([-\d.]+)', output).group(1)) + a_yinc = float(re.search(r'yinc\s*=\s*([-\d.]+)', output).group(1)) + a_ysize = int(re.search(r'ysize\s*=\s*(\d+)', output).group(1)) + + output = subprocess.check_output(['cdo', 'griddes', file2], text=True) + b_xfirst = float(re.search(r'xfirst\s*=\s*([-\d.]+)', output).group(1)) + b_xinc = float(re.search(r'xinc\s*=\s*([-\d.]+)', output).group(1)) + b_xsize = int(re.search(r'xsize\s*=\s*(\d+)', output).group(1)) + b_yfirst = float(re.search(r'yfirst\s*=\s*([-\d.]+)', output).group(1)) + b_yinc = float(re.search(r'yinc\s*=\s*([-\d.]+)', output).group(1)) + b_ysize = int(re.search(r'ysize\s*=\s*(\d+)', output).group(1)) + + xfirst = a_xfirst if a_xfirst < b_xfirst else b_xfirst + xend = a_xfirst+(a_xinc*a_xsize) if a_xfirst+(a_xinc*a_xsize) > b_xfirst+(b_xinc*b_xsize) else b_xfirst+(b_xinc*b_xsize) + xsize = round((xend - xfirst)/a_xinc) + print(f"[CDO] Calcul first a:{a_xfirst} b:{b_xfirst} ==> {xfirst}") + print(f"[CDO] Calcul end a:{a_xfirst+(a_xinc*a_xsize)} b:{b_xfirst+(b_xinc*b_xsize)} ==> {xend} ==> size : {xsize}") + + yfirst = a_yfirst if a_yfirst < b_yfirst else b_yfirst + yend = a_yfirst+(a_yinc*a_ysize) if a_yfirst+(a_yinc*a_ysize) > b_yfirst+(b_yinc*b_ysize) else b_yfirst+(b_yinc*b_ysize) + ysize = round((yend - yfirst)/a_yinc) + print(f"[CDO] Calcul first a:{a_yfirst} b:{b_yfirst} ==> {yfirst}") + print(f"[CDO] Calcul end a:{a_yfirst+(a_yinc*a_ysize)} b:{b_yfirst+(b_yinc*b_ysize)} ==> {yend} ==> size : {ysize}") + + with open(grid_file, "w") as f: + f.write(f"gridtype = lonlat\n") + f.write(f"gridsize = {xsize*ysize}\n") + f.write(f"xsize = {xsize}\n") + f.write(f"ysize = {ysize}\n") + f.write(f"xname = lon\n") + f.write(f"xlongname = \"longitude\"\n") + f.write(f"xunits = \"degrees_east\"\n") + f.write(f"yname = lat\n") + f.write(f"ylongname = \"latitude\"\n") + f.write(f"yunits = \"degrees_north\"\n") + f.write(f"xfirst = {xfirst}\n") + f.write(f"xinc = {a_xinc}\n") + f.write(f"yfirst = {yfirst}\n") + f.write(f"yinc = {a_yinc}\n") + f.write(f"scanningMode = 64\n") + + # 2. Remappage bilinéaire des deux fichiers vers la même grille + file1_remap = file1.replace(".grb", "_remap.grb") + file2_remap = file2.replace(".grb", "_remap.grb") + + print(f"[CDO] Remappage de {file1} → {file1_remap}") + subprocess.run(["cdo", "remapbil," + grid_file, file1, file1_remap], check=True) + + print(f"[CDO] Remappage de {file2} → {file2_remap}") + subprocess.run(["cdo", "remapbil," + grid_file, file2, file2_remap], check=True) + + # 3. Fusion des deux fichiers remappés + print(f"[CDO] Fusion des deux fichiers remappés → {output_file}") + subprocess.run(["cdo", "mergegrid", file1_remap, file2_remap, output_file], check=True) + + print(f"✅ Fichier fusionné créé : {output_file}") + + # Nettoyage temporaire + os.remove(grid_file) + os.remove(file1_remap) + os.remove(file2_remap) + + except subprocess.CalledProcessError as e: + print(f"❌ Erreur lors de l'exécution de CDO : {e}") + except Exception as ex: + print(f"❌ Erreur inattendue : {ex}") + +def extract_surface_current(grib_file, output_file): + grbs = pygrib.open(grib_file) + selected = [] + print(f"Extraction des informations de courrant de surface depuis {grib_file}") + + for grb in grbs: + if "component of current" in grb.parameterName: + # param_id = getattr(grb, 'parameterNumber', 'N/A') + #level = getattr(grb, 'level', 'N/A') + #units = getattr(grb, 'units', 'N/A') + #print(f"ParamId: {param_id}, Niveau: {level}, Unités: {units}") + #print(f"OK Niveau: {grb.level}, Unités: {grb.units}") + selected.append(grb) + + if not selected: + raise Exception("Aucun champ 'unknown' trouvé.") + + with open(output_file, 'wb') as f_out: + for grb in selected: + f_out.write(grb.tostring()) + + print(f"Extraction terminée, fichier sauvegardé : {output_file}") + +def main(): + output_filename = "surface_currents.grb" + merged_filename = "merged.grb" + + url, bz2_filename, grib_a_filename = build_url("https://openskiron.org/gribs_wrf_4km/","Dunkirk_4km_WRF_WAM") + download_file(url, bz2_filename) + decompress_bz2(bz2_filename, grib_a_filename) + + url, bz2_filename, grib_b_filename = build_url("https://openskiron.org/gribs_wrf_4km/","Hastings_4km_WRF_WAM") + download_file(url, bz2_filename) + decompress_bz2(bz2_filename, grib_b_filename) + + extract_surface_current(grib_a_filename,"surfacea.grb") + extract_surface_current(grib_b_filename,"surfaceb.grb") + + + merge_grib_spatially_cdo("surfacea.grb","surfaceb.grb",merged_filename); + + extract_surface_current(merged_filename, output_filename) + +if __name__ == "__main__": + main() diff --git a/downloader.py b/downloader.py new file mode 100644 index 0000000..a7cbc26 --- /dev/null +++ b/downloader.py @@ -0,0 +1,46 @@ +import os +from datetime import datetime, timedelta +from copernicusmarine import subset +import subprocess + +class CopernicusDownloader: + def __init__(self, username="", password=""): + self.dataset_id = "cmems_mod_ibi_phy_anfc_0.027deg-2D_PT1H-m" + self.username = username + self.password = password + self.output_file = os.path.join(os.getcwd(), "downloads", "download.nc") + + def download(self, lat_min, lat_max, lon_min, lon_max, days): + today = datetime.utcnow() + start_date = today.strftime("%Y-%m-%dT00:00:00") + end_date = (today + timedelta(days=days)).strftime("%Y-%m-%dT23:00:00") + + save_dir = os.path.join(os.getcwd(), "downloads") + os.makedirs(save_dir, exist_ok=True) + + subset( + dataset_id=self.dataset_id, + output_filename=self.output_file, + variables=["zos", "uo", "vo", "thetao"], + minimum_longitude=lon_min, + maximum_longitude=lon_max, + minimum_latitude=lat_min, + maximum_latitude=lat_max, + start_datetime=start_date, + end_datetime=end_date, + username=self.username, + password=self.password + ) + + def convert_to_grib2(self, input_path=None, output_path=None): + input_path = input_path or self.output_file + output_path = output_path or input_path.replace(".nc", ".grib2") + + if not os.path.exists(input_path): + raise FileNotFoundError(f"Fichier introuvable : {input_path}") + + try: + subprocess.run(["cdo", "-f", "grb2", "copy", input_path, output_path], check=True) + return output_path + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Erreur lors de la conversion avec CDO : {e}") diff --git a/iface.py b/iface.py new file mode 100644 index 0000000..45731fd --- /dev/null +++ b/iface.py @@ -0,0 +1,330 @@ +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 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, lat_max = self.config.getfloat('map','map_lat_start'), self.config.getfloat('map','map_lat_end') + lon_min, lon_max = self.config.getfloat('map','map_lon_start'), self.config.getfloat('map','map_lon_end') + # print(f"xy_from_latlon dbg coord = {lat_min}-{lat_max}-{lon_min}-{lon_max}") + + 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} ") + + x = (lon - lon_min) / (lon_max - lon_min) * self.scale * self.original_img.width + self.offset_x + y = (lat_max - lat) / (lat_max - lat_min) * 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, lat_max = self.config.getfloat('map','map_lat_start'), self.config.getfloat('map','map_lat_end') + lon_min, lon_max = self.config.getfloat('map','map_lon_start'), 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 sur l'image + img_x = (x - self.offset_x) / self.scale + img_y = (y - self.offset_y) / self.scale + + # Vérifier si le clic est dans l'image affichée + 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 + lat = lat_max - rel_y * (lat_max - lat_min) + lon = lon_min + rel_x * (lon_max - lon_min) + 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() \ No newline at end of file diff --git a/map-N56-W12-N44-E10.png b/map-N56-W12-N44-E10.png new file mode 100644 index 0000000..acc3019 Binary files /dev/null and b/map-N56-W12-N44-E10.png differ