Compare commits
10 Commits
45b0049600
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d391066e5b | |||
| 918e380f56 | |||
| 9b5fcd0100 | |||
| c05ee25923 | |||
| 11b47b1549 | |||
| 922f2cd134 | |||
| 91824db369 | |||
| f40c303726 | |||
| cd9b4d41c6 | |||
| 579d01da45 |
@@ -1,184 +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
|
||||
|
||||
def build_url(base_url, basename):
|
||||
# today = datetime.datetime.utcnow().strftime("%d%m%y")
|
||||
today = date.today().strftime("%y%m%d")
|
||||
filename = f"{basename}_{today}-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()
|
||||
# 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()
|
||||
|
||||
26
README.md
26
README.md
@@ -4,24 +4,28 @@
|
||||
|
||||
## 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)
|
||||
- Requires python3 with the following dependencies
|
||||
- - python3-tk
|
||||
- - python3-pil
|
||||
- - python3-pil.imagetk
|
||||
- - copernicusmarine
|
||||
|
||||
|
||||
## License
|
||||
|
||||
115
downloader.py
Normal file
115
downloader.py
Normal file
@@ -0,0 +1,115 @@
|
||||
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")
|
||||
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)
|
||||
self.file_remove(self.output_file)
|
||||
return self.output_grib
|
||||
|
||||
def file_remove(self,file):
|
||||
if os.path.exists(file):
|
||||
os.remove(file)
|
||||
print(f"✅ File removed : {file}")
|
||||
else:
|
||||
print(f"❌ File {file} does not exist.")
|
||||
|
||||
def download(self,outfile):
|
||||
today = datetime.utcnow()
|
||||
start_date = today.strftime("%Y-%m-%dT00: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=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 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"-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}")
|
||||
356
iface.py
Normal file
356
iface.py
Normal file
@@ -0,0 +1,356 @@
|
||||
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
|
||||
import webbrowser
|
||||
from math import log, tan, radians, pi, degrees, atan, sinh
|
||||
|
||||
from downloader import CopernicusDownloader
|
||||
|
||||
CONFIG_FILE = "config.ini"
|
||||
|
||||
class MapSelector:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("Surface Current 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.frame_about = ttk.Frame(self.notebook)
|
||||
|
||||
self.notebook.add(self.frame_download, text="Download Manager")
|
||||
self.notebook.add(self.frame_config, text="Configuration")
|
||||
self.notebook.add(self.frame_about, text="About")
|
||||
|
||||
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
|
||||
|
||||
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(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(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="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"], width=5).grid(row=1, column=6)
|
||||
|
||||
btn_frame = ttk.Frame(self.frame_download)
|
||||
btn_frame.pack(pady=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()
|
||||
|
||||
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):
|
||||
# Define the zoom direction
|
||||
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 open_link(self, event=None):
|
||||
webbrowser.open_new("https://data.marine.copernicus.eu/register")
|
||||
|
||||
def setup_config_tab(self):
|
||||
frame = self.frame_config
|
||||
|
||||
ttk.Label(frame, text="Use the credential of your account ").pack(pady=5)
|
||||
account_link = tk.Label(frame, text="register on https://data.marine.copernicus.eu/register",fg="blue",cursor="hand2")
|
||||
account_link.pack(pady=5)
|
||||
account_link.bind("<Button-1>", self.open_link)
|
||||
ttk.Label(frame, text="Copernicus Marine 'username'").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="Copernicus Marine 'password'").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 = self.config.getfloat('map', 'map_lat_start')
|
||||
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')
|
||||
|
||||
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}")
|
||||
|
||||
# Longitude (linéaire)
|
||||
x_norm = (lon - lon_min) / (lon_max - lon_min)
|
||||
x = x_norm * self.scale * self.original_img.width + self.offset_x
|
||||
|
||||
# Latitude (projection Mercator)
|
||||
mercator_min = log(tan(pi / 4 + radians(lat_min) / 2)) # Mercator
|
||||
mercator_max = log(tan(pi / 4 + radians(lat_max) / 2)) # Mercator
|
||||
mercator_lat = log(tan(pi / 4 + radians(lat) / 2)) # Mercator
|
||||
|
||||
y_norm = (mercator_max - mercator_lat) / (mercator_max - mercator_min)
|
||||
y = y_norm * self.scale * self.original_img.height + self.offset_y
|
||||
|
||||
return x, y
|
||||
|
||||
def latlon_from_xy(self, x, y):
|
||||
# Coordonnées géographiques de la carte
|
||||
lat_min = self.config.getfloat('map', 'map_lat_start')
|
||||
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}")
|
||||
|
||||
# Calculer la position relative dans l’image (sans offset/zoom)
|
||||
img_x = (x - self.offset_x) / self.scale
|
||||
img_y = (y - self.offset_y) / self.scale
|
||||
|
||||
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
|
||||
|
||||
# Longitude : linéaire
|
||||
lon = lon_min + rel_x * (lon_max - lon_min)
|
||||
|
||||
# Latitude : projection inverse de Mercator
|
||||
merc_min = log(tan(pi / 4 + radians(lat_min) / 2)) # Mercator
|
||||
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
|
||||
return lat, lon
|
||||
else:
|
||||
return None, None # En dehors de l'image
|
||||
|
||||
def on_click(self, event):
|
||||
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):
|
||||
lat1, lon1 = self.latlon_from_xy(self.start_x, self.start_y)
|
||||
lat2, lon2 = self.latlon_from_xy(event.x, event.y)
|
||||
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:
|
||||
|
||||
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}")
|
||||
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
BIN
map-N56-W12-N44-E10.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 MiB |
BIN
map_N44_N56_W12_E12.png
Normal file
BIN
map_N44_N56_W12_E12.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 MiB |
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
pillow
|
||||
copernicusmarine
|
||||
Reference in New Issue
Block a user