3535
3636# Import visualizers
3737from aeolis .gui .visualizers .domain import DomainVisualizer
38+ from aeolis .gui .visualizers .wind import WindVisualizer
3839
3940try :
4041 import netCDF4
@@ -519,208 +520,6 @@ def browse_wind_file(self):
519520 # Auto-load and plot the data
520521 self .load_and_plot_wind ()
521522
522- def load_and_plot_wind (self ):
523- """Load wind file and plot time series and wind rose"""
524- try :
525- # Get the wind file path
526- wind_file = self .wind_file_entry .get ()
527-
528- if not wind_file :
529- messagebox .showwarning ("Warning" , "No wind file specified!" )
530- return
531-
532- # Get the directory of the config file to resolve relative paths
533- config_dir = self .get_config_dir ()
534-
535- # Load the wind file
536- if not os .path .isabs (wind_file ):
537- wind_file_path = os .path .join (config_dir , wind_file )
538- else :
539- wind_file_path = wind_file
540-
541- if not os .path .exists (wind_file_path ):
542- messagebox .showerror ("Error" , f"Wind file not found: { wind_file_path } " )
543- return
544-
545- # Check if we already loaded this file (avoid reloading)
546- if hasattr (self , 'wind_data_cache' ) and self .wind_data_cache .get ('file_path' ) == wind_file_path :
547- # Data already loaded, just return (don't reload)
548- return
549-
550- # Load wind data (time, speed, direction)
551- wind_data = np .loadtxt (wind_file_path )
552-
553- # Check data format
554- if wind_data .ndim != 2 or wind_data .shape [1 ] < 3 :
555- messagebox .showerror ("Error" , "Wind file must have at least 3 columns: time, speed, direction" )
556- return
557-
558- time = wind_data [:, 0 ]
559- speed = wind_data [:, 1 ]
560- direction = wind_data [:, 2 ]
561-
562- # Get wind convention from config
563- wind_convention = self .dic .get ('wind_convention' , 'nautical' )
564-
565- # Cache the wind data along with file path and convention
566- self .wind_data_cache = {
567- 'file_path' : wind_file_path ,
568- 'time' : time ,
569- 'speed' : speed ,
570- 'direction' : direction ,
571- 'convention' : wind_convention
572- }
573-
574- # Determine appropriate time unit based on simulation time (tstart and tstop)
575- tstart = 0
576- tstop = 0
577- use_sim_limits = False
578-
579- try :
580- tstart_entry = self .entries .get ('tstart' )
581- tstop_entry = self .entries .get ('tstop' )
582-
583- if tstart_entry and tstop_entry :
584- tstart = float (tstart_entry .get () or 0 )
585- tstop = float (tstop_entry .get () or 0 )
586- if tstop > tstart :
587- sim_duration = tstop - tstart # in seconds
588- use_sim_limits = True
589- else :
590- # If entries don't exist yet, use wind file time range
591- sim_duration = time [- 1 ] - time [0 ] if len (time ) > 0 else 0
592- else :
593- # If entries don't exist yet, use wind file time range
594- sim_duration = time [- 1 ] - time [0 ] if len (time ) > 0 else 0
595- except (ValueError , AttributeError , TypeError ):
596- # Fallback to wind file time range
597- sim_duration = time [- 1 ] - time [0 ] if len (time ) > 0 else 0
598-
599- # Choose appropriate time unit and convert using utility function
600- time_unit , time_divisor = determine_time_unit (sim_duration )
601- time_converted = time / time_divisor
602-
603- # Plot wind speed time series
604- self .wind_speed_ax .clear ()
605-
606- # Plot data line FIRST
607- self .wind_speed_ax .plot (time_converted , speed , 'b-' , linewidth = 1.5 , zorder = 2 , label = 'Wind Speed' )
608- self .wind_speed_ax .set_xlabel (f'Time ({ time_unit } )' )
609- self .wind_speed_ax .set_ylabel ('Wind Speed (m/s)' )
610- self .wind_speed_ax .set_title ('Wind Speed Time Series' )
611- self .wind_speed_ax .grid (True , alpha = 0.3 , zorder = 1 )
612-
613- # Calculate axis limits with 10% padding and add shading on top
614- if use_sim_limits :
615- tstart_converted = tstart / time_divisor
616- tstop_converted = tstop / time_divisor
617- axis_range = tstop_converted - tstart_converted
618- padding = 0.1 * axis_range
619- xlim_min = tstart_converted - padding
620- xlim_max = tstop_converted + padding
621-
622- self .wind_speed_ax .set_xlim ([xlim_min , xlim_max ])
623-
624- # Plot shading AFTER data line (on top) with higher transparency
625- self .wind_speed_ax .axvspan (xlim_min , tstart_converted , alpha = 0.15 , color = 'gray' , zorder = 3 )
626- self .wind_speed_ax .axvspan (tstop_converted , xlim_max , alpha = 0.15 , color = 'gray' , zorder = 3 )
627-
628- # Add legend entry for shaded region
629- import matplotlib .patches as mpatches
630- shaded_patch = mpatches .Patch (color = 'gray' , alpha = 0.15 , label = 'Outside simulation time' )
631- self .wind_speed_ax .legend (handles = [shaded_patch ], loc = 'upper right' , fontsize = 8 )
632-
633- # Plot wind direction time series
634- self .wind_dir_ax .clear ()
635-
636- # Plot data line FIRST
637- self .wind_dir_ax .plot (time_converted , direction , 'r-' , linewidth = 1.5 , zorder = 2 , label = 'Wind Direction' )
638- self .wind_dir_ax .set_xlabel (f'Time ({ time_unit } )' )
639- self .wind_dir_ax .set_ylabel ('Wind Direction (degrees)' )
640- self .wind_dir_ax .set_title (f'Wind Direction Time Series ({ wind_convention } convention)' )
641- self .wind_dir_ax .set_ylim ([0 , 360 ])
642- self .wind_dir_ax .grid (True , alpha = 0.3 , zorder = 1 )
643-
644- # Add shading on top
645- if use_sim_limits :
646- self .wind_dir_ax .set_xlim ([xlim_min , xlim_max ])
647-
648- # Plot shading AFTER data line (on top) with higher transparency
649- self .wind_dir_ax .axvspan (xlim_min , tstart_converted , alpha = 0.15 , color = 'gray' , zorder = 3 )
650- self .wind_dir_ax .axvspan (tstop_converted , xlim_max , alpha = 0.15 , color = 'gray' , zorder = 3 )
651-
652- # Add legend entry for shaded region
653- import matplotlib .patches as mpatches
654- shaded_patch = mpatches .Patch (color = 'gray' , alpha = 0.15 , label = 'Outside simulation time' )
655- self .wind_dir_ax .legend (handles = [shaded_patch ], loc = 'upper right' , fontsize = 8 )
656-
657- # Redraw time series canvas
658- self .wind_ts_canvas .draw ()
659-
660- # Plot wind rose
661- self .plot_windrose (speed , direction , wind_convention )
662-
663- except Exception as e :
664- error_msg = f"Failed to load and plot wind data: { str (e )} \n \n { traceback .format_exc ()} "
665- messagebox .showerror ("Error" , error_msg )
666- print (error_msg )
667-
668- def force_reload_wind (self ):
669- """Force reload of wind data by clearing cache"""
670- # Clear the cache to force reload
671- if hasattr (self , 'wind_data_cache' ):
672- delattr (self , 'wind_data_cache' )
673- # Now load and plot
674- self .load_and_plot_wind ()
675-
676- def plot_windrose (self , speed , direction , convention = 'nautical' ):
677- """Plot wind rose diagram
678-
679- Parameters
680- ----------
681- speed : array
682- Wind speed values
683- direction : array
684- Wind direction values in degrees (as stored in wind file)
685- convention : str
686- 'nautical' (0° = North, clockwise, already in meteorological convention)
687- 'cartesian' (0° = East, will be converted to meteorological using 270 - direction)
688- """
689- try :
690- # Clear the windrose figure
691- self .windrose_fig .clear ()
692-
693- # Convert direction based on convention to meteorological standard (0° = North, clockwise)
694- if convention == 'cartesian' :
695- # Cartesian in AeoLiS: 0° = shore normal (East-like direction)
696- # Convert to meteorological: met = 270 - cart (as done in wind.py)
697- direction_met = (270 - direction ) % 360
698- else :
699- # Already in meteorological/nautical convention (0° = North, clockwise)
700- direction_met = direction
701-
702- # Create windrose axes - simple and clean like in the notebook
703- ax = WindroseAxes .from_ax (fig = self .windrose_fig )
704-
705- # Plot wind rose - windrose library handles everything
706- ax .bar (direction_met , speed , normed = True , opening = 0.8 , edgecolor = 'white' )
707- ax .set_legend (title = 'Wind Speed (m/s)' )
708- ax .set_title (f'Wind Rose ({ convention } convention)' , fontsize = 14 , fontweight = 'bold' )
709-
710- # Redraw windrose canvas
711- self .windrose_canvas .draw ()
712-
713- except Exception as e :
714- error_msg = f"Failed to plot wind rose: { str (e )} \n \n { traceback .format_exc ()} "
715- print (error_msg )
716- # Create a simple text message instead
717- self .windrose_fig .clear ()
718- ax = self .windrose_fig .add_subplot (111 )
719- ax .text (0.5 , 0.5 , 'Wind rose plot failed.\n See console for details.' ,
720- ha = 'center' , va = 'center' , transform = ax .transAxes )
721- ax .axis ('off' )
722- self .windrose_canvas .draw ()
723-
724523 def create_wind_input_tab (self , tab_control ):
725524 """Create the 'Wind Input' tab with wind data visualization"""
726525 tab_wind = ttk .Frame (tab_control )
@@ -746,26 +545,6 @@ def create_wind_input_tab(self, tab_control):
746545 command = self .browse_wind_file )
747546 wind_browse_btn .grid (row = 0 , column = 2 , sticky = W , pady = 2 )
748547
749- # Load button (forces reload by clearing cache)
750- wind_load_btn = ttk .Button (file_frame , text = "Load & Plot" ,
751- command = self .force_reload_wind )
752- wind_load_btn .grid (row = 0 , column = 3 , sticky = W , pady = 2 , padx = 5 )
753-
754- # Export buttons for wind plots
755- export_label_wind = ttk .Label (file_frame , text = "Export:" )
756- export_label_wind .grid (row = 1 , column = 0 , sticky = W , pady = 5 )
757-
758- export_button_frame_wind = ttk .Frame (file_frame )
759- export_button_frame_wind .grid (row = 1 , column = 1 , columnspan = 3 , sticky = W , pady = 5 )
760-
761- export_wind_ts_btn = ttk .Button (export_button_frame_wind , text = "Export Time Series PNG" ,
762- command = self .export_wind_timeseries_png )
763- export_wind_ts_btn .pack (side = LEFT , padx = 5 )
764-
765- export_windrose_btn = ttk .Button (export_button_frame_wind , text = "Export Wind Rose PNG" ,
766- command = self .export_windrose_png )
767- export_windrose_btn .pack (side = LEFT , padx = 5 )
768-
769548 # Create frame for time series plots
770549 timeseries_frame = ttk .LabelFrame (tab_wind , text = "Wind Time Series" , padding = 10 )
771550 timeseries_frame .grid (row = 0 , column = 1 , rowspan = 2 , padx = 10 , pady = 10 , sticky = (N , S , E , W ))
@@ -797,6 +576,37 @@ def create_wind_input_tab(self, tab_control):
797576 self .windrose_canvas = FigureCanvasTkAgg (self .windrose_fig , master = windrose_frame )
798577 self .windrose_canvas .draw ()
799578 self .windrose_canvas .get_tk_widget ().pack (side = TOP , fill = BOTH , expand = 1 )
579+
580+ # Initialize wind visualizer
581+ self .wind_visualizer = WindVisualizer (
582+ self .wind_speed_ax , self .wind_dir_ax , self .wind_ts_canvas , self .wind_ts_fig ,
583+ self .windrose_fig , self .windrose_canvas ,
584+ lambda : self .wind_file_entry , # get_wind_file function
585+ lambda : self .entries , # get_entries function
586+ self .get_config_dir , # get_config_dir function
587+ lambda : self .dic # get_dic function
588+ )
589+
590+ # Now add buttons that use the visualizer
591+ # Load button (forces reload by clearing cache)
592+ wind_load_btn = ttk .Button (file_frame , text = "Load & Plot" ,
593+ command = self .wind_visualizer .force_reload )
594+ wind_load_btn .grid (row = 0 , column = 3 , sticky = W , pady = 2 , padx = 5 )
595+
596+ # Export buttons for wind plots
597+ export_label_wind = ttk .Label (file_frame , text = "Export:" )
598+ export_label_wind .grid (row = 1 , column = 0 , sticky = W , pady = 5 )
599+
600+ export_button_frame_wind = ttk .Frame (file_frame )
601+ export_button_frame_wind .grid (row = 1 , column = 1 , columnspan = 3 , sticky = W , pady = 5 )
602+
603+ export_wind_ts_btn = ttk .Button (export_button_frame_wind , text = "Export Time Series PNG" ,
604+ command = self .wind_visualizer .export_timeseries_png )
605+ export_wind_ts_btn .pack (side = LEFT , padx = 5 )
606+
607+ export_windrose_btn = ttk .Button (export_button_frame_wind , text = "Export Wind Rose PNG" ,
608+ command = self .wind_visualizer .export_windrose_png )
609+ export_windrose_btn .pack (side = LEFT , padx = 5 )
800610
801611 def create_timeframe_tab (self , tab_control ):
802612 # Create the 'Timeframe' tab
@@ -2476,58 +2286,6 @@ def enable_overlay_vegetation(self):
24762286 current_time = int (self .time_slider .get ())
24772287 self .update_time_step (current_time )
24782288
2479- def export_wind_timeseries_png (self ):
2480- """
2481- Export the wind time series plot as a PNG image.
2482- Opens a file dialog to choose save location.
2483- """
2484- if not hasattr (self , 'wind_ts_fig' ) or self .wind_ts_fig is None :
2485- messagebox .showwarning ("Warning" , "No wind plot to export. Please load wind data first." )
2486- return
2487-
2488- # Open file dialog for saving
2489- file_path = filedialog .asksaveasfilename (
2490- initialdir = self .get_config_dir (),
2491- title = "Save wind time series as PNG" ,
2492- defaultextension = ".png" ,
2493- filetypes = (("PNG files" , "*.png" ), ("All files" , "*.*" ))
2494- )
2495-
2496- if file_path :
2497- try :
2498- self .wind_ts_fig .savefig (file_path , dpi = 300 , bbox_inches = 'tight' )
2499- messagebox .showinfo ("Success" , f"Wind time series exported to:\n { file_path } " )
2500- except Exception as e :
2501- error_msg = f"Failed to export plot: { str (e )} \n \n { traceback .format_exc ()} "
2502- messagebox .showerror ("Error" , error_msg )
2503- print (error_msg )
2504-
2505- def export_windrose_png (self ):
2506- """
2507- Export the wind rose plot as a PNG image.
2508- Opens a file dialog to choose save location.
2509- """
2510- if not hasattr (self , 'windrose_fig' ) or self .windrose_fig is None :
2511- messagebox .showwarning ("Warning" , "No wind rose plot to export. Please load wind data first." )
2512- return
2513-
2514- # Open file dialog for saving
2515- file_path = filedialog .asksaveasfilename (
2516- initialdir = self .get_config_dir (),
2517- title = "Save wind rose as PNG" ,
2518- defaultextension = ".png" ,
2519- filetypes = (("PNG files" , "*.png" ), ("All files" , "*.*" ))
2520- )
2521-
2522- if file_path :
2523- try :
2524- self .windrose_fig .savefig (file_path , dpi = 300 , bbox_inches = 'tight' )
2525- messagebox .showinfo ("Success" , f"Wind rose exported to:\n { file_path } " )
2526- except Exception as e :
2527- error_msg = f"Failed to export plot: { str (e )} \n \n { traceback .format_exc ()} "
2528- messagebox .showerror ("Error" , error_msg )
2529- print (error_msg )
2530-
25312289 def export_2d_plot_png (self ):
25322290 """
25332291 Export the current 2D plot as a PNG image.
0 commit comments