Compare commits

...

10 Commits

Author SHA1 Message Date
d391066e5b add map 2025-05-23 07:00:44 +00:00
918e380f56 optimizations 2025-05-22 15:24:59 +00:00
9b5fcd0100 adapt mecator
update interface
adapt time base before converting to grib
refactor some stuff
remove debug comments
2025-05-21 19:13:46 +00:00
c05ee25923 modification en projection mercator pour la latitude 2025-05-20 16:01:20 +00:00
11b47b1549 init 2025-05-20 15:09:33 +00:00
922f2cd134 Delete iface.py 2025-05-20 17:05:13 +02:00
91824db369 Delete downloader.py 2025-05-20 17:05:00 +02:00
f40c303726 Delete DownloadCurrents.py 2025-05-20 17:04:44 +02:00
cd9b4d41c6 Edit README.md 2025-05-20 09:36:09 +02:00
579d01da45 add works in progress 2025-05-20 06:14:36 +00:00
8 changed files with 673 additions and 195 deletions

View File

@@ -1,184 +1,185 @@
# NOTES # NOTES
# #
# - installer miniconda # - installer miniconda
# - ajouter conda au "PATH" # - ajouter conda au "PATH"
# > conda init # > conda init
# > conda create -n feelgrib python=3.13 -y # > conda create -n feelgrib python=3.13 -y
# > conda activate feelgrib # > conda activate feelgrib
# > conda install -c conda-forge pygrib requests # > conda install -c conda-forge pygrib requests
# > conda install conda-forge::wgrib2 # > conda install conda-forge::wgrib2
# > conda install -c conda-forge cdo # > conda install -c conda-forge cdo
# WSL wsl --install # WSL wsl --install
# dans terminal wsl # dans terminal wsl
# apt-get install python3-grib # apt-get install python3-grib
# apt-get install cdo # apt-get install cdo
# telecharger https://www.ftp.cpc.ncep.noaa.gov/wd51we/wgrib2/wgrib2.tgz.v3.1.3 # telecharger https://www.ftp.cpc.ncep.noaa.gov/wd51we/wgrib2/wgrib2.tgz.v3.1.3
import os import os
import requests import requests
import bz2 import bz2
import subprocess import subprocess
import pygrib import pygrib
import re import re
from datetime import date from datetime import date,timedelta
def build_url(base_url, basename): def build_url(base_url, basename,previousday=0):
# today = datetime.datetime.utcnow().strftime("%d%m%y") # today = datetime.datetime.utcnow().strftime("%d%m%y")
today = date.today().strftime("%y%m%d") today = date.today()
filename = f"{basename}_{today}-12.grb.bz2" target = (today - timedelta(previousday)).strftime("%y%m%d")
grib_filename = filename.replace(".bz2", "") filename = f"{basename}_{target}-12.grb.bz2"
return base_url + filename, filename, grib_filename 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}") def download_file(url, local_filename):
response = requests.get(url, stream=True) print(f"Téléchargement de : {url}")
if response.status_code != 200: response = requests.get(url, stream=True)
raise Exception(f"Erreur de téléchargement : HTTP {response.status_code}") if response.status_code != 200:
with open(local_filename, 'wb') as f: raise Exception(f"Erreur de téléchargement : HTTP {response.status_code}")
for chunk in response.iter_content(chunk_size=8192): with open(local_filename, 'wb') as f:
f.write(chunk) for chunk in response.iter_content(chunk_size=8192):
print(f"Fichier téléchargé : {local_filename}") 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}") def decompress_bz2(input_file, output_file):
with bz2.open(input_file, 'rb') as f_in: print(f"Décompression de : {input_file}")
with open(output_file, 'wb') as f_out: with bz2.open(input_file, 'rb') as f_in:
f_out.write(f_in.read()) with open(output_file, 'wb') as f_out:
print(f"Fichier décompressé : {output_file}") 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"):
""" 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) Fusionne deux fichiers GRIB1 spatialement avec CDO.
file2 : second fichier GRIB (sera interpolé sur la même grille) file1 : chemin du premier fichier GRIB (grille cible)
output_file : nom du fichier GRIB de sortie fusionné file2 : second fichier GRIB (sera interpolé sur la même grille)
grid_file : nom temporaire pour stocker la définition de 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 try:
print(f"[CDO] Extraction de la grille de référence depuis {file1}") # 1. Extraire la grille du fichier 1
subprocess.run(["cdo", "griddes", file1], check=True, stdout=open(grid_file, "w")) 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) # Calcul des coordonnées
a_xfirst = float(re.search(r'xfirst\s*=\s*([-\d.]+)', output).group(1)) output = subprocess.check_output(['cdo', 'griddes', file1], text=True)
a_xinc = float(re.search(r'xinc\s*=\s*([-\d.]+)', output).group(1)) a_xfirst = float(re.search(r'xfirst\s*=\s*([-\d.]+)', output).group(1))
a_xsize = int(re.search(r'xsize\s*=\s*(\d+)', output).group(1)) a_xinc = float(re.search(r'xinc\s*=\s*([-\d.]+)', output).group(1))
a_yfirst = float(re.search(r'yfirst\s*=\s*([-\d.]+)', output).group(1)) a_xsize = int(re.search(r'xsize\s*=\s*(\d+)', output).group(1))
a_yinc = float(re.search(r'yinc\s*=\s*([-\d.]+)', output).group(1)) a_yfirst = float(re.search(r'yfirst\s*=\s*([-\d.]+)', output).group(1))
a_ysize = int(re.search(r'ysize\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)) output = subprocess.check_output(['cdo', 'griddes', file2], text=True)
b_xinc = float(re.search(r'xinc\s*=\s*([-\d.]+)', output).group(1)) b_xfirst = float(re.search(r'xfirst\s*=\s*([-\d.]+)', output).group(1))
b_xsize = int(re.search(r'xsize\s*=\s*(\d+)', output).group(1)) b_xinc = float(re.search(r'xinc\s*=\s*([-\d.]+)', output).group(1))
b_yfirst = float(re.search(r'yfirst\s*=\s*([-\d.]+)', output).group(1)) b_xsize = int(re.search(r'xsize\s*=\s*(\d+)', output).group(1))
b_yinc = float(re.search(r'yinc\s*=\s*([-\d.]+)', output).group(1)) b_yfirst = float(re.search(r'yfirst\s*=\s*([-\d.]+)', output).group(1))
b_ysize = int(re.search(r'ysize\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) xfirst = a_xfirst if a_xfirst < b_xfirst else b_xfirst
xsize = round((xend - xfirst)/a_xinc) 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)
print(f"[CDO] Calcul first a:{a_xfirst} b:{b_xfirst} ==> {xfirst}") xsize = round((xend - xfirst)/a_xinc)
print(f"[CDO] Calcul end a:{a_xfirst+(a_xinc*a_xsize)} b:{b_xfirst+(b_xinc*b_xsize)} ==> {xend} ==> size : {xsize}") 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) yfirst = a_yfirst if a_yfirst < b_yfirst else b_yfirst
ysize = round((yend - yfirst)/a_yinc) 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)
print(f"[CDO] Calcul first a:{a_yfirst} b:{b_yfirst} ==> {yfirst}") ysize = round((yend - yfirst)/a_yinc)
print(f"[CDO] Calcul end a:{a_yfirst+(a_yinc*a_ysize)} b:{b_yfirst+(b_yinc*b_ysize)} ==> {yend} ==> size : {ysize}") 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") with open(grid_file, "w") as f:
f.write(f"gridsize = {xsize*ysize}\n") f.write(f"gridtype = lonlat\n")
f.write(f"xsize = {xsize}\n") f.write(f"gridsize = {xsize*ysize}\n")
f.write(f"ysize = {ysize}\n") f.write(f"xsize = {xsize}\n")
f.write(f"xname = lon\n") f.write(f"ysize = {ysize}\n")
f.write(f"xlongname = \"longitude\"\n") f.write(f"xname = lon\n")
f.write(f"xunits = \"degrees_east\"\n") f.write(f"xlongname = \"longitude\"\n")
f.write(f"yname = lat\n") f.write(f"xunits = \"degrees_east\"\n")
f.write(f"ylongname = \"latitude\"\n") f.write(f"yname = lat\n")
f.write(f"yunits = \"degrees_north\"\n") f.write(f"ylongname = \"latitude\"\n")
f.write(f"xfirst = {xfirst}\n") f.write(f"yunits = \"degrees_north\"\n")
f.write(f"xinc = {a_xinc}\n") f.write(f"xfirst = {xfirst}\n")
f.write(f"yfirst = {yfirst}\n") f.write(f"xinc = {a_xinc}\n")
f.write(f"yinc = {a_yinc}\n") f.write(f"yfirst = {yfirst}\n")
f.write(f"scanningMode = 64\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") # 2. Remappage bilinéaire des deux fichiers vers la même grille
file2_remap = file2.replace(".grb", "_remap.grb") 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 {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) 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}") # 3. Fusion des deux fichiers remappés
subprocess.run(["cdo", "mergegrid", file1_remap, file2_remap, output_file], check=True) 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}")
print(f"✅ Fichier fusionné créé : {output_file}")
# Nettoyage temporaire
os.remove(grid_file) # Nettoyage temporaire
os.remove(file1_remap) os.remove(grid_file)
os.remove(file2_remap) 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 subprocess.CalledProcessError as e:
except Exception as ex: print(f"❌ Erreur lors de l'exécution de CDO : {e}")
print(f"❌ Erreur inattendue : {ex}") except Exception as ex:
print(f"❌ Erreur inattendue : {ex}")
def extract_surface_current(grib_file, output_file):
grbs = pygrib.open(grib_file) def extract_surface_current(grib_file, output_file):
selected = [] grbs = pygrib.open(grib_file)
print(f"Extraction des informations de courrant de surface depuis {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: for grb in grbs:
# param_id = getattr(grb, 'parameterNumber', 'N/A') if "component of current" in grb.parameterName:
#level = getattr(grb, 'level', 'N/A') # param_id = getattr(grb, 'parameterNumber', 'N/A')
#units = getattr(grb, 'units', 'N/A') #level = getattr(grb, 'level', 'N/A')
#print(f"ParamId: {param_id}, Niveau: {level}, Unités: {units}") #units = getattr(grb, 'units', 'N/A')
#print(f"OK Niveau: {grb.level}, Unités: {grb.units}") #print(f"ParamId: {param_id}, Niveau: {level}, Unités: {units}")
selected.append(grb) #print(f"OK Niveau: {grb.level}, Unités: {grb.units}")
selected.append(grb)
if not selected:
raise Exception("Aucun champ 'unknown' trouvé.") if not selected:
raise Exception("Aucun champ 'unknown' trouvé.")
with open(output_file, 'wb') as f_out:
for grb in selected: with open(output_file, 'wb') as f_out:
f_out.write(grb.tostring()) for grb in selected:
f_out.write(grb.tostring())
print(f"Extraction terminée, fichier sauvegardé : {output_file}")
print(f"Extraction terminée, fichier sauvegardé : {output_file}")
def main():
output_filename = "surface_currents.grb" def main():
merged_filename = "merged.grb" 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) url, bz2_filename, grib_a_filename = build_url("https://openskiron.org/gribs_wrf_4km/","Dunkirk_4km_WRF_WAM")
#decompress_bz2(bz2_filename, grib_a_filename) 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) url, bz2_filename, grib_b_filename = build_url("https://openskiron.org/gribs_wrf_4km/","Hastings_4km_WRF_WAM")
#decompress_bz2(bz2_filename, grib_b_filename) 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") 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);
merge_grib_spatially_cdo("surfacea.grb","surfaceb.grb",merged_filename);
extract_surface_current(merged_filename, output_filename)
extract_surface_current(merged_filename, output_filename)
if __name__ == "__main__":
main() if __name__ == "__main__":
main()

View File

@@ -4,24 +4,28 @@
## 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
- Runs under linux or windows (with wsl)
- Requires python3 with the following dependencies
- - python3-tk
- - python3-pil
- - python3-pil.imagetk
- - copernicusmarine
## License ## License

115
downloader.py Normal file
View 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
View 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 limage (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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

BIN
map.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 769 KiB

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
View File

@@ -0,0 +1,2 @@
pillow
copernicusmarine