Skip to content

Commit f55eeca

Browse files
SierdCopilotCopilot
authored
Gui v0.2 added (#264) (#266)
* add wind plotting functionality * Refactor GUI: Complete modular architecture with all GUI tabs extracted, export functionality, and utilities (#263) * Initial plan * Phase 1: Add constants, utility functions, and improve documentation * Phase 2: Extract helper methods and reduce code duplication * Phase 3: Add variable label/title constants and improve docstrings * Final: Add comprehensive refactoring documentation and summary * Add export functionality: PNG and MP4 animations for all visualizations * Phase 4: Begin code organization - extract utils module and create gui package structure * Add comprehensive additional improvements proposal document * bugfixes related to import and animattion functionality * updated structure for further refactoring * Refactor: Extract DomainVisualizer and rename gui_app_backup.py to application.py * bugfix * bugfix on loading domain * Refactor: Extract WindVisualizer to modular architecture * Refactor: Extract Output2DVisualizer for 2D NetCDF visualization * Refactor: Extract Output1DVisualizer - Complete modular architecture achieved! * bugfixes loading files * removed netcdf check * bugfixes after refractoring * bugfixes with domain overview * Speeding up complex drawing * hold on functionality added * Tab to run code added. * Update aeolis/gui/application.py * Update aeolis/gui/application.py * Update aeolis/gui/visualizers/domain.py * Update aeolis/gui/visualizers/domain.py * Update aeolis/gui/main.py * Update aeolis/gui/visualizers/output_2d.py * Apply suggestions from code review * Rename visualizers folder to gui_tabs and update all imports * bigfixes related to refactoring * reducing code lenght by omitting some redundancies * Apply suggestions from code review * Apply suggestions from code review * Apply suggestions from code review --------- --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Sierd <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 165183d commit f55eeca

File tree

9 files changed

+1889
-25
lines changed

9 files changed

+1889
-25
lines changed

REFACTORING_SUMMARY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ The refactoring focused on code quality without changing functionality. Here are
211211
1. **Phase 4 (Suggested)**: Split into multiple modules
212212
- `gui/main.py` - Main entry point
213213
- `gui/config_manager.py` - Configuration I/O
214-
- `gui/visualizers.py` - Plotting functions
214+
- `gui/gui_tabs/` - Tab modules for different visualizations
215215
- `gui/utils.py` - Utility functions
216216

217217
2. **Phase 5 (Suggested)**: Add unit tests

aeolis/gui/application.py

Lines changed: 129 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,40 +7,32 @@
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

1313
import aeolis
1414
from tkinter import *
1515
from tkinter import ttk, filedialog, messagebox
1616
import os
1717
import numpy as np
18-
import traceback
1918
import netCDF4
20-
import matplotlib.pyplot as plt
2119
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
2220
from matplotlib.figure import Figure
2321
from aeolis.constants import DEFAULT_CONFIG
2422

2523
# Import utilities from gui package
2624
from 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
4638
configfile = "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():

aeolis/gui/gui_tabs/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""
2+
GUI Tabs package for AeoLiS GUI.
3+
4+
This package contains specialized tab modules for different types of data:
5+
- domain: Domain setup visualization (bed, vegetation, etc.)
6+
- wind: Wind input visualization (time series, wind roses)
7+
- output_2d: 2D output visualization
8+
- output_1d: 1D transect visualization
9+
"""
10+
11+
from aeolis.gui.gui_tabs.domain import DomainVisualizer
12+
from aeolis.gui.gui_tabs.wind import WindVisualizer
13+
from aeolis.gui.gui_tabs.output_2d import Output2DVisualizer
14+
from aeolis.gui.gui_tabs.output_1d import Output1DVisualizer
15+
16+
__all__ = ['DomainVisualizer', 'WindVisualizer', 'Output2DVisualizer', 'Output1DVisualizer']

0 commit comments

Comments
 (0)