77- Plotting wind input data and wind roses
88- Visualizing model output (2D and 1D transects)
99
10- This is the main application module that coordinates the GUI and visualizers .
10+ This is the main application module that coordinates the GUI and tab modules .
1111"""
1212
1313import aeolis
1414from tkinter import *
1515from tkinter import ttk , filedialog , messagebox
1616import os
1717import numpy as np
18- import traceback
1918import netCDF4
20- import matplotlib .pyplot as plt
2119from matplotlib .backends .backend_tkagg import FigureCanvasTkAgg
2220from matplotlib .figure import Figure
2321from aeolis .constants import DEFAULT_CONFIG
2422
2523# Import utilities from gui package
2624from aeolis .gui .utils import (
27- # Constants
28- HILLSHADE_AZIMUTH , HILLSHADE_ALTITUDE , HILLSHADE_AMBIENT ,
29- TIME_UNIT_THRESHOLDS , TIME_UNIT_DIVISORS ,
30- OCEAN_DEPTH_THRESHOLD , OCEAN_DISTANCE_THRESHOLD , SUBSAMPLE_RATE_DIVISOR ,
31- NC_COORD_VARS , VARIABLE_LABELS , VARIABLE_TITLES ,
32- # Utility functions
33- resolve_file_path , make_relative_path , determine_time_unit ,
34- extract_time_slice , apply_hillshade
25+ VARIABLE_LABELS , VARIABLE_TITLES ,
26+ resolve_file_path , make_relative_path
3527)
3628
37- # Import visualizers
38- from aeolis .gui .visualizers .domain import DomainVisualizer
39- from aeolis .gui .visualizers .wind import WindVisualizer
40- from aeolis .gui .visualizers .output_2d import Output2DVisualizer
41- from aeolis .gui .visualizers .output_1d import Output1DVisualizer
29+ # Import GUI tabs
30+ from aeolis .gui .gui_tabs .domain import DomainVisualizer
31+ from aeolis .gui .gui_tabs .wind import WindVisualizer
32+ from aeolis .gui .gui_tabs .output_2d import Output2DVisualizer
33+ from aeolis .gui .gui_tabs .output_1d import Output1DVisualizer
34+ from aeolis .gui .gui_tabs .model_runner import ModelRunner
4235
43- from windrose import WindroseAxes
4436
4537# Initialize with default configuration
4638configfile = "No file selected"
@@ -105,6 +97,7 @@ def create_widgets(self):
10597 self .create_input_file_tab (tab_control )
10698 self .create_domain_tab (tab_control )
10799 self .create_wind_input_tab (tab_control )
100+ self .create_run_model_tab (tab_control )
108101 self .create_plot_output_2d_tab (tab_control )
109102 self .create_plot_output_1d_tab (tab_control )
110103 # Pack the tab control to expand and fill the available space
@@ -118,6 +111,8 @@ def create_widgets(self):
118111
119112 def on_tab_changed (self , event ):
120113 """Handle tab change event to auto-plot domain/wind when tab is selected"""
114+ global configfile
115+
121116 # Get the currently selected tab index
122117 selected_tab = self .tab_control .index (self .tab_control .select ())
123118
@@ -158,6 +153,12 @@ def on_tab_changed(self, event):
158153 except Exception as e :
159154 # Silently fail if plotting doesn't work (e.g., file doesn't exist)
160155 pass
156+
157+ # Run Model tab is at index 3 (0: Input file, 1: Domain, 2: Wind, 3: Run Model, 4: Output 2D, 5: Output 1D)
158+ elif selected_tab == 3 :
159+ # Update config file label
160+ if hasattr (self , 'model_runner_visualizer' ):
161+ self .model_runner_visualizer .update_config_display (configfile )
161162
162163 def create_label_entry (self , tab , text , value , row ):
163164 # Create a label and entry widget for a given tab
@@ -408,7 +409,7 @@ def load_new_config(self):
408409 wind_file = self .wind_file_entry .get ()
409410 if wind_file and wind_file .strip ():
410411 self .load_and_plot_wind ()
411- except :
412+ except Exception :
412413 pass # Silently fail if tabs not yet initialized
413414
414415 messagebox .showinfo ("Success" , f"Configuration loaded from:\n { file_path } " )
@@ -476,8 +477,8 @@ def toggle_y_limits(self):
476477 self .ymax_entry_1d .config (state = 'normal' )
477478
478479 # Update plot if data is loaded
479- if hasattr (self , 'nc_data_cache_1d ' ) and self .nc_data_cache_1d is not None :
480- self .update_1d_plot ()
480+ if hasattr (self , 'output_1d_visualizer ' ) and self . output_1d_visualizer .nc_data_cache_1d is not None :
481+ self .output_1d_visualizer . update_plot ()
481482
482483 def load_and_plot_wind (self ):
483484 """
@@ -631,7 +632,7 @@ def create_plot_output_2d_tab(self, tab_control):
631632
632633 # Browse button for NC file
633634 nc_browse_btn = ttk .Button (file_frame , text = "Browse..." ,
634- command = lambda : self .browse_nc_file () )
635+ command = self .browse_nc_file )
635636 nc_browse_btn .grid (row = 0 , column = 2 , sticky = W , pady = 2 )
636637
637638 # Variable selection dropdown
@@ -787,7 +788,7 @@ def create_plot_output_1d_tab(self, tab_control):
787788
788789 # Browse button for NC file
789790 nc_browse_btn_1d = ttk .Button (file_frame_1d , text = "Browse..." ,
790- command = lambda : self .browse_nc_file_1d () )
791+ command = self .browse_nc_file_1d )
791792 nc_browse_btn_1d .grid (row = 0 , column = 2 , sticky = W , pady = 2 )
792793
793794 # Variable selection dropdown
@@ -909,6 +910,16 @@ def create_plot_output_1d_tab(self, tab_control):
909910 self .time_slider_1d .pack (side = LEFT , fill = X , expand = 1 , padx = 5 )
910911 self .time_slider_1d .set (0 )
911912
913+ # Hold On button
914+ self .hold_on_btn_1d = ttk .Button (slider_frame_1d , text = "Hold On" ,
915+ command = self .toggle_hold_on_1d )
916+ self .hold_on_btn_1d .pack (side = LEFT , padx = 5 )
917+
918+ # Clear Held Plots button
919+ self .clear_held_btn_1d = ttk .Button (slider_frame_1d , text = "Clear Held" ,
920+ command = self .clear_held_plots_1d )
921+ self .clear_held_btn_1d .pack (side = LEFT , padx = 5 )
922+
912923 # Initialize 1D output visualizer (after all UI components are created)
913924 self .output_1d_visualizer = Output1DVisualizer (
914925 self .output_1d_ax , self .output_1d_overview_ax ,
@@ -918,7 +929,8 @@ def create_plot_output_1d_tab(self, tab_control):
918929 self .variable_var_1d , self .transect_direction_var ,
919930 self .nc_file_entry_1d , self .variable_dropdown_1d ,
920931 self .output_1d_overview_canvas ,
921- self .get_config_dir , self .get_variable_label , self .get_variable_title
932+ self .get_config_dir , self .get_variable_label , self .get_variable_title ,
933+ self .auto_ylimits_var , self .ymin_entry_1d , self .ymax_entry_1d
922934 )
923935
924936 # Update slider commands to use visualizer
@@ -993,6 +1005,21 @@ def update_1d_plot(self):
9931005 """
9941006 if hasattr (self , 'output_1d_visualizer' ):
9951007 self .output_1d_visualizer .update_plot ()
1008+
1009+ def toggle_hold_on_1d (self ):
1010+ """
1011+ Toggle hold on for the 1D transect plot.
1012+ This allows overlaying multiple time steps on the same plot.
1013+ """
1014+ if hasattr (self , 'output_1d_visualizer' ):
1015+ self .output_1d_visualizer .toggle_hold_on ()
1016+
1017+ def clear_held_plots_1d (self ):
1018+ """
1019+ Clear all held plots from the 1D transect visualization.
1020+ """
1021+ if hasattr (self , 'output_1d_visualizer' ):
1022+ self .output_1d_visualizer .clear_held_plots ()
9961023
9971024 def get_variable_label (self , var_name ):
9981025 """
@@ -1338,6 +1365,85 @@ def enable_overlay_vegetation(self):
13381365 current_time = int (self .time_slider .get ())
13391366 self .update_time_step (current_time )
13401367
1368+ def create_run_model_tab (self , tab_control ):
1369+ """Create the 'Run Model' tab for executing AeoLiS simulations"""
1370+ tab_run = ttk .Frame (tab_control )
1371+ tab_control .add (tab_run , text = 'Run Model' )
1372+
1373+ # Configure grid weights
1374+ tab_run .columnconfigure (0 , weight = 1 )
1375+ tab_run .rowconfigure (1 , weight = 1 )
1376+
1377+ # Create control frame
1378+ control_frame = ttk .LabelFrame (tab_run , text = "Model Control" , padding = 10 )
1379+ control_frame .grid (row = 0 , column = 0 , padx = 10 , pady = 10 , sticky = (N , W , E ))
1380+
1381+ # Config file display
1382+ config_label = ttk .Label (control_frame , text = "Config file:" )
1383+ config_label .grid (row = 0 , column = 0 , sticky = W , pady = 5 )
1384+
1385+ run_config_label = ttk .Label (control_frame , text = "No file selected" ,
1386+ foreground = "gray" )
1387+ run_config_label .grid (row = 0 , column = 1 , sticky = W , pady = 5 , padx = (10 , 0 ))
1388+
1389+ # Start/Stop buttons
1390+ button_frame = ttk .Frame (control_frame )
1391+ button_frame .grid (row = 1 , column = 0 , columnspan = 2 , pady = 10 )
1392+
1393+ start_model_btn = ttk .Button (button_frame , text = "Start Model" , width = 15 )
1394+ start_model_btn .pack (side = LEFT , padx = 5 )
1395+
1396+ stop_model_btn = ttk .Button (button_frame , text = "Stop Model" ,
1397+ width = 15 , state = DISABLED )
1398+ stop_model_btn .pack (side = LEFT , padx = 5 )
1399+
1400+ # Progress bar
1401+ model_progress = ttk .Progressbar (control_frame , mode = 'indeterminate' , length = 400 )
1402+ model_progress .grid (row = 2 , column = 0 , columnspan = 2 , pady = 5 , sticky = (W , E ))
1403+
1404+ # Status label
1405+ model_status_label = ttk .Label (control_frame , text = "Ready" , foreground = "blue" )
1406+ model_status_label .grid (row = 3 , column = 0 , columnspan = 2 , sticky = W , pady = 5 )
1407+
1408+ # Create output frame for logging
1409+ output_frame = ttk .LabelFrame (tab_run , text = "Model Output / Logging" , padding = 10 )
1410+ output_frame .grid (row = 1 , column = 0 , padx = 10 , pady = (0 , 10 ), sticky = (N , S , E , W ))
1411+ output_frame .rowconfigure (0 , weight = 1 )
1412+ output_frame .columnconfigure (0 , weight = 1 )
1413+
1414+ # Create Text widget with scrollbar for terminal output
1415+ output_scroll = ttk .Scrollbar (output_frame )
1416+ output_scroll .grid (row = 0 , column = 1 , sticky = (N , S ))
1417+
1418+ model_output_text = Text (output_frame , wrap = WORD ,
1419+ yscrollcommand = output_scroll .set ,
1420+ height = 20 , width = 80 ,
1421+ bg = 'black' , fg = 'lime' ,
1422+ font = ('Courier' , 9 ))
1423+ model_output_text .grid (row = 0 , column = 0 , sticky = (N , S , E , W ))
1424+ output_scroll .config (command = model_output_text .yview )
1425+
1426+ # Add clear button
1427+ clear_btn = ttk .Button (output_frame , text = "Clear Output" ,
1428+ command = lambda : model_output_text .delete (1.0 , END ))
1429+ clear_btn .grid (row = 1 , column = 0 , columnspan = 2 , pady = (5 , 0 ))
1430+
1431+ # Initialize model runner visualizer
1432+ self .model_runner_visualizer = ModelRunner (
1433+ start_model_btn , stop_model_btn , model_progress ,
1434+ model_status_label , model_output_text , run_config_label ,
1435+ self .root , self .get_current_config_file
1436+ )
1437+
1438+ # Connect button commands
1439+ start_model_btn .config (command = self .model_runner_visualizer .start_model )
1440+ stop_model_btn .config (command = self .model_runner_visualizer .stop_model )
1441+
1442+ def get_current_config_file (self ):
1443+ """Get the current config file path"""
1444+ global configfile
1445+ return configfile
1446+
13411447 def save (self ):
13421448 # Save the current entries to the configuration dictionary
13431449 for field , entry in self .entries .items ():
0 commit comments