From 9b5fcd0100957a4d2d364958aa50458d924ab179 Mon Sep 17 00:00:00 2001 From: "laurent.deleers@gmail.com" Date: Wed, 21 May 2025 19:13:46 +0000 Subject: [PATCH] adapt mecator update interface adapt time base before converting to grib refactor some stuff remove debug comments --- README.md | 18 ++++------ downloader.py | 99 +++++++++++++++++++++++++++++++++++++++++---------- iface.py | 56 ++++++++++++++--------------- 3 files changed, 113 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 993ffc9..aedb877 100644 --- a/README.md +++ b/README.md @@ -4,24 +4,20 @@ ## Objective -Retrieve grib files containing water surface current for the regions between holland and france south west, including east uk coasts. -Files must be compatible for common opensource navigation tools like (qtvlm, opencpn and xygrib grib visualization tool) +Provide grib files containing water surface current compatible with the most common navigation tools (opencpn, qtvlm, ...) ## Data Sources -### copernicus -probably the most accurate source for european waters. -- https://data.marine.copernicus.eu/product/IBI_ANALYSISFORECAST_PHY_005_001/services -### openskiron -provide interesting grib for several EU regions. -- https://openskiron.org/en/openwrf +The data are provided by the IBI-MFC. Thanks to them for their works. +The data covers the Iberia-Biscay-Ireland region with a resolution of 0.028° and 10 days of forecasts +More informations available on : https://data.marine.copernicus.eu/product/IBI_ANALYSISFORECAST_PHY_005_001/description ## What the tool does -- [ ] Retrieve data from these Sources -- [ ] Clean data, preserve only surface current, and crop grid to the requested zone. -- [ ] Transform and format these data to a standardized grib file +- [ ] Provide a user-friendly interface to select the zone. +- [ ] Transform data's to an exploitable grid2 file. + ## Prerequisites and Installation - Runs under linux or windows (with wsl) diff --git a/downloader.py b/downloader.py index a7cbc26..236b172 100644 --- a/downloader.py +++ b/downloader.py @@ -8,39 +8,100 @@ class CopernicusDownloader: 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") + self.output_file = os.path.join(os.getcwd(), "downloads", "download.nc") + self.prepared_file = os.path.join(os.getcwd(), "downloads", "download_prepared.nc") + self.output_grib = os.path.join(os.getcwd(), "downloads", "download.grib2") + + self.lat_min = 0.0 + self.lat_max = 0.0 + self.lon_min = 0.0 + self.lon_max = 0.0 + self.days = 1 + + def retrieve_grib2(self, lat_min, lat_max, lon_min, lon_max, days): + self.lat_min = lat_min + self.lat_max = lat_max + self.lon_min = lon_min + self.lon_max = lon_max + self.days = days + # print(f"Démarage du téléchargement avec {lat_min} - {lat_max} - {lon_min} - {lon_max} - {days} ") + + self.download(self.output_file) + self.apply_setreftime(self.output_file,self.prepared_file) + self.convert_to_grib2(self.prepared_file,self.output_grib) + return self.output_grib - def download(self, lat_min, lat_max, lon_min, lon_max, days): + def download(self,outfile): today = datetime.utcnow() start_date = today.strftime("%Y-%m-%dT00:00:00") - end_date = (today + timedelta(days=days)).strftime("%Y-%m-%dT23:00:00") + end_date = (today + timedelta(days=self.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, + output_filename=outfile, + variables=["uo", "vo"], # removed "thetao" (temperature) and "zos" (elevation) + minimum_longitude=self.lon_min, + maximum_longitude=self.lon_max, + minimum_latitude=self.lat_min, + maximum_latitude=self.lat_max, start_datetime=start_date, end_datetime=end_date, username=self.username, password=self.password ) + print(f"✅ File downloaded to : {outfile}") + - 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}") - + def extract_date_and_time(self,infile): + # Extract the first timecode of the nc file try: - subprocess.run(["cdo", "-f", "grb2", "copy", input_path, output_path], check=True) - return output_path + result = subprocess.run( + ["cdo", "showtimestamp", infile], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + # Récupérer le premier timestamp : ex. 2025-05-21T00:00:00 + first_timestamp = result.stdout.strip().split()[0] + date, time = first_timestamp.split('T') + return date, time + except subprocess.CalledProcessError as e: - raise RuntimeError(f"Erreur lors de la conversion avec CDO : {e}") + print(f"[CDO Error] {e.stderr}") + except Exception as e: + print(f"[Error] {e}") + + return None, None + + def apply_setreftime(self,infile,outfile): + # Apply the command cdo -setreftime,, to define a reference time + date, time = self.extract_date_and_time(infile) + if not date or not time: + print("Cannot retrieve date/time from this file.") + return + + try: + subprocess.run( + ["cdo", f"-setreftime,{date},{time}", infile, outfile], + check=True + ) + print(f"✅ File adapted with correct time ref {date} - {time} : {outfile}") + + except subprocess.CalledProcessError as e: + print(f"[Error when setting reference time with CDO -setreftime] {e.stderr}") + + + def convert_to_grib2(self,infile,outfile): + if not os.path.exists(infile): + raise FileNotFoundError(f"File not found : {infile}") + try: + subprocess.run(["cdo", "-f", "grb2", "copy", infile, outfile], check=True) + print(f"✅ File converted to grib : {outfile}") + return outfile + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Error when converting with CDO : {e}") diff --git a/iface.py b/iface.py index ce0f881..b78729f 100644 --- a/iface.py +++ b/iface.py @@ -84,32 +84,36 @@ class MapSelector: self.start_x = self.start_y = None self.rect = None - coord_frame = ttk.Frame(self.frame_download) - coord_frame.pack(pady=5) + params_frame = ttk.Frame(self.frame_download) #, borderwidth=1, relief="solid") + + params_frame.pack(pady=5) + #params_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(params_frame, text="Lat Min:").grid(row=0, column=0) + ttk.Entry(params_frame, textvariable=self.lat1_var, width=10).grid(row=0, column=1) + params_frame.grid_columnconfigure(2, minsize=20) + ttk.Label(params_frame, text="Lon Min:").grid(row=0, column=3) + ttk.Entry(params_frame, textvariable=self.lon1_var, width=10).grid(row=0, column=4) + params_frame.grid_columnconfigure(5, minsize=20) - 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) + ttk.Label(params_frame, text="Lat Max:").grid(row=1, column=0) + ttk.Entry(params_frame, textvariable=self.lat2_var, width=10).grid(row=1, column=1) + ttk.Label(params_frame, text="Lon Max:").grid(row=1, column=3) + ttk.Entry(params_frame, textvariable=self.lon2_var, width=10).grid(row=1, column=4) - 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) + self.duration_var = tk.StringVar(value="3") + ttk.Label(params_frame, text="Days").grid(row=0, column=6) + ttk.Combobox(params_frame, textvariable=self.duration_var, values=["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"], width=5).grid(row=1, column=6) 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) + ttk.Button(btn_frame, text="Download", command=self.start_download).grid(row=0, column=1, padx=5) + ttk.Button(btn_frame, text="Open Folder", command=self.open_download_folder).grid(row=0, column=2, padx=5) self.status = ttk.Label(self.frame_download, text="") self.status.pack() @@ -146,7 +150,7 @@ class MapSelector: self.redraw_rectangle_from_coords() def setzoom(self, event): - # Déterminer la direction du zoom + # Define the zoom direction if event.num == 5 or event.delta == -120: zoom_factor = 0.9 elif event.num == 4 or event.delta == 120: @@ -228,8 +232,7 @@ class MapSelector: 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}") + # 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) @@ -243,7 +246,6 @@ class MapSelector: 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): @@ -252,8 +254,7 @@ class MapSelector: 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}") + # 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 @@ -271,14 +272,11 @@ class MapSelector: 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) @@ -289,10 +287,8 @@ class MapSelector: 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}") @@ -315,16 +311,16 @@ class MapSelector: 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() + + grib_path = self.downloader.retrieve_grib2(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}")