This commit is contained in:
2025-05-20 15:09:33 +00:00
parent 922f2cd134
commit 11b47b1549
4 changed files with 561 additions and 0 deletions

185
DownloadCurrents.py Normal file
View File

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

46
downloader.py Normal file
View File

@@ -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}")

330
iface.py Normal file
View File

@@ -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("<Configure>", self.on_canvas_configure)
self.canvas.bind("<Button-1>", self.on_click)
self.canvas.bind("<B1-Motion>", self.on_drag)
self.canvas.bind("<ButtonRelease-1>", self.on_release)
self.canvas.bind("<MouseWheel>", self.setzoom) # Windows
self.canvas.bind("<Button-4>", self.setzoom) # Linux scroll up
self.canvas.bind("<Button-5>", self.setzoom) # Linux scroll down
self.canvas.bind("<ButtonPress-3>", self.start_pan)
self.canvas.bind("<B3-Motion>", 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()

BIN
map-N56-W12-N44-E10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB