adapt mecator
update interface adapt time base before converting to grib refactor some stuff remove debug comments
This commit is contained in:
18
README.md
18
README.md
@@ -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)
|
||||||
|
|||||||
@@ -9,38 +9,99 @@ class CopernicusDownloader:
|
|||||||
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}")
|
||||||
|
|||||||
56
iface.py
56
iface.py
@@ -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 l’image (sans offset/zoom)
|
# Calculer la position relative dans l’image (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}")
|
||||||
|
|||||||
Reference in New Issue
Block a user