adapt mecator

update interface
adapt time base before converting to grib
refactor some stuff
remove debug comments
This commit is contained in:
2025-05-21 19:13:46 +00:00
parent c05ee25923
commit 9b5fcd0100
3 changed files with 113 additions and 60 deletions

View File

@@ -4,24 +4,20 @@
## Objective ## Objective
Retrieve grib files containing water surface current for the regions between holland and france south west, including east uk coasts. Provide grib files containing water surface current compatible with the most common navigation tools (opencpn, qtvlm, ...)
Files must be compatible for common opensource navigation tools like (qtvlm, opencpn and xygrib grib visualization tool)
## Data Sources ## Data Sources
### copernicus The data are provided by the IBI-MFC. Thanks to them for their works.
probably the most accurate source for european waters. The data covers the Iberia-Biscay-Ireland region with a resolution of 0.028° and 10 days of forecasts
- https://data.marine.copernicus.eu/product/IBI_ANALYSISFORECAST_PHY_005_001/services More informations available on : https://data.marine.copernicus.eu/product/IBI_ANALYSISFORECAST_PHY_005_001/description
### openskiron
provide interesting grib for several EU regions.
- https://openskiron.org/en/openwrf
## What the tool does ## What the tool does
- [ ] Retrieve data from these Sources - [ ] Provide a user-friendly interface to select the zone.
- [ ] Clean data, preserve only surface current, and crop grid to the requested zone. - [ ] Transform data's to an exploitable grid2 file.
- [ ] Transform and format these data to a standardized grib file
## Prerequisites and Installation ## Prerequisites and Installation
- Runs under linux or windows (with wsl) - Runs under linux or windows (with wsl)

View File

@@ -8,39 +8,100 @@ class CopernicusDownloader:
self.dataset_id = "cmems_mod_ibi_phy_anfc_0.027deg-2D_PT1H-m" self.dataset_id = "cmems_mod_ibi_phy_anfc_0.027deg-2D_PT1H-m"
self.username = username self.username = username
self.password = password 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")
def download(self, lat_min, lat_max, lon_min, lon_max, days): 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,outfile):
today = datetime.utcnow() today = datetime.utcnow()
start_date = today.strftime("%Y-%m-%dT00:00:00") 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") save_dir = os.path.join(os.getcwd(), "downloads")
os.makedirs(save_dir, exist_ok=True) os.makedirs(save_dir, exist_ok=True)
subset( subset(
dataset_id=self.dataset_id, dataset_id=self.dataset_id,
output_filename=self.output_file, output_filename=outfile,
variables=["zos", "uo", "vo", "thetao"], variables=["uo", "vo"], # removed "thetao" (temperature) and "zos" (elevation)
minimum_longitude=lon_min, minimum_longitude=self.lon_min,
maximum_longitude=lon_max, maximum_longitude=self.lon_max,
minimum_latitude=lat_min, minimum_latitude=self.lat_min,
maximum_latitude=lat_max, maximum_latitude=self.lat_max,
start_datetime=start_date, start_datetime=start_date,
end_datetime=end_date, end_datetime=end_date,
username=self.username, username=self.username,
password=self.password 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): def extract_date_and_time(self,infile):
raise FileNotFoundError(f"Fichier introuvable : {input_path}") # Extract the first timecode of the nc file
try:
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:
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,<date>,<heure> <input> <output> 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: try:
subprocess.run(["cdo", "-f", "grb2", "copy", input_path, output_path], check=True) subprocess.run(
return output_path ["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: except subprocess.CalledProcessError as e:
raise RuntimeError(f"Erreur lors de la conversion avec CDO : {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}")

View File

@@ -84,32 +84,36 @@ class MapSelector:
self.start_x = self.start_y = None self.start_x = self.start_y = None
self.rect = None self.rect = None
coord_frame = ttk.Frame(self.frame_download) params_frame = ttk.Frame(self.frame_download) #, borderwidth=1, relief="solid")
coord_frame.pack(pady=5)
params_frame.pack(pady=5)
#params_frame.pack(pady=5)
self.lat1_var = tk.StringVar() self.lat1_var = tk.StringVar()
self.lon1_var = tk.StringVar() self.lon1_var = tk.StringVar()
self.lat2_var = tk.StringVar() self.lat2_var = tk.StringVar()
self.lon2_var = tk.StringVar() self.lon2_var = tk.StringVar()
ttk.Label(coord_frame, text="Lat Min:").grid(row=0, column=0) ttk.Label(params_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.Entry(params_frame, textvariable=self.lat1_var, width=10).grid(row=0, column=1)
ttk.Label(coord_frame, text="Lon Min:").grid(row=0, column=2) params_frame.grid_columnconfigure(2, minsize=20)
ttk.Entry(coord_frame, textvariable=self.lon1_var, width=10).grid(row=0, column=3) 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.Label(params_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.Entry(params_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.Label(params_frame, text="Lon Max:").grid(row=1, column=3)
ttk.Entry(coord_frame, textvariable=self.lon2_var, width=10).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") self.duration_var = tk.StringVar(value="3")
ttk.Label(self.frame_download, text="Durée (jours):").pack() ttk.Label(params_frame, text="Days").grid(row=0, column=6)
ttk.Combobox(self.frame_download, textvariable=self.duration_var, values=["1", "2", "3", "4", "5"], width=5).pack(pady=5) 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 = ttk.Frame(self.frame_download)
btn_frame.pack(pady=5) 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="Download", 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="Open Folder", command=self.open_download_folder).grid(row=0, column=2, padx=5)
self.status = ttk.Label(self.frame_download, text="") self.status = ttk.Label(self.frame_download, text="")
self.status.pack() self.status.pack()
@@ -146,7 +150,7 @@ class MapSelector:
self.redraw_rectangle_from_coords() self.redraw_rectangle_from_coords()
def setzoom(self, event): def setzoom(self, event):
# Déterminer la direction du zoom # Define the zoom direction
if event.num == 5 or event.delta == -120: if event.num == 5 or event.delta == -120:
zoom_factor = 0.9 zoom_factor = 0.9
elif event.num == 4 or event.delta == 120: elif event.num == 4 or event.delta == 120:
@@ -228,8 +232,7 @@ class MapSelector:
canvas_w = self.canvas.winfo_width() canvas_w = self.canvas.winfo_width()
canvas_h = self.canvas.winfo_height() 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) # Longitude (linéaire)
x_norm = (lon - lon_min) / (lon_max - lon_min) 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_norm = (mercator_max - mercator_lat) / (mercator_max - mercator_min)
y = y_norm * self.scale * self.original_img.height + self.offset_y y = y_norm * self.scale * self.original_img.height + self.offset_y
print(f"xy_from_latlon : {x} - {y}")
return x, y return x, y
def latlon_from_xy(self, x, y): def latlon_from_xy(self, x, y):
@@ -252,8 +254,7 @@ class MapSelector:
lat_max = self.config.getfloat('map', 'map_lat_end') lat_max = self.config.getfloat('map', 'map_lat_end')
lon_min = self.config.getfloat('map', 'map_lon_start') lon_min = self.config.getfloat('map', 'map_lon_start')
lon_max = self.config.getfloat('map', 'map_lon_end') 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 limage (sans offset/zoom) # Calculer la position relative dans limage (sans offset/zoom)
img_x = (x - self.offset_x) / self.scale 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_max = log(tan(pi / 4 + radians(lat_max) / 2)) # Mercator
merc_y = merc_max - rel_y * (merc_max - merc_min) merc_y = merc_max - rel_y * (merc_max - merc_min)
lat = degrees(atan(sinh(merc_y))) # Mercator Inverse lat = degrees(atan(sinh(merc_y))) # Mercator Inverse
print(f"latlon_from_xy : {lat} - {lon}")
return lat, lon return lat, lon
else: else:
return None, None # En dehors de l'image return None, None # En dehors de l'image
def on_click(self, event): def on_click(self, event):
print(f"dbg on_click : {event.x} - {event.y}")
self.start_x, self.start_y = event.x, event.y self.start_x, self.start_y = event.x, event.y
if self.rect: if self.rect:
self.canvas.delete(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) self.canvas.coords(self.rect, self.start_x, self.start_y, event.x, event.y)
def on_release(self, event): 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) lat1, lon1 = self.latlon_from_xy(self.start_x, self.start_y)
lat2, lon2 = self.latlon_from_xy(event.x, event.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.lat1_var.set(f"{min(lat1, lat2):.4f}")
self.lat2_var.set(f"{max(lat1, lat2):.4f}") self.lat2_var.set(f"{max(lat1, lat2):.4f}")
self.lon1_var.set(f"{min(lon1, lon2):.4f}") self.lon1_var.set(f"{min(lon1, lon2):.4f}")
@@ -315,16 +311,16 @@ class MapSelector:
except ValueError: except ValueError:
messagebox.showerror("Erreur", "Veuillez entrer des coordonnées et durée valides.") messagebox.showerror("Erreur", "Veuillez entrer des coordonnées et durée valides.")
return return
self.progress.start(10) self.progress.start(10)
self.status.config(text="Téléchargement en cours...") 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() 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): def download(self, lat_min, lat_max, lon_min, lon_max, days):
try: 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.retrieve_grib2(lat_min, lat_max, lon_min, lon_max, days)
grib_path = self.downloader.convert_to_grib2() #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)}") self.status.config(text=f"Téléchargement et conversion terminés : {os.path.basename(grib_path)}")
except Exception as e: except Exception as e:
self.status.config(text=f"Erreur : {e}") self.status.config(text=f"Erreur : {e}")