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
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)

View File

@@ -9,38 +9,99 @@ class CopernicusDownloader:
self.username = username
self.password = password
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()
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:
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:
subprocess.run(["cdo", "-f", "grb2", "copy", input_path, output_path], check=True)
return output_path
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:
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.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 limage (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}")