Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion REFACTORING_SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ The refactoring focused on code quality without changing functionality. Here are
1. **Phase 4 (Suggested)**: Split into multiple modules
- `gui/main.py` - Main entry point
- `gui/config_manager.py` - Configuration I/O
- `gui/visualizers.py` - Plotting functions
- `gui/gui_tabs/` - Tab modules for different visualizations
- `gui/utils.py` - Utility functions

2. **Phase 5 (Suggested)**: Add unit tests
Expand Down
152 changes: 129 additions & 23 deletions aeolis/gui/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,32 @@
- Plotting wind input data and wind roses
- Visualizing model output (2D and 1D transects)

This is the main application module that coordinates the GUI and visualizers.
This is the main application module that coordinates the GUI and tab modules.
"""

import aeolis
from tkinter import *
from tkinter import ttk, filedialog, messagebox
import os
import numpy as np
import traceback
import netCDF4
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from aeolis.constants import DEFAULT_CONFIG

# Import utilities from gui package
from aeolis.gui.utils import (
# Constants
HILLSHADE_AZIMUTH, HILLSHADE_ALTITUDE, HILLSHADE_AMBIENT,
TIME_UNIT_THRESHOLDS, TIME_UNIT_DIVISORS,
OCEAN_DEPTH_THRESHOLD, OCEAN_DISTANCE_THRESHOLD, SUBSAMPLE_RATE_DIVISOR,
NC_COORD_VARS, VARIABLE_LABELS, VARIABLE_TITLES,
# Utility functions
resolve_file_path, make_relative_path, determine_time_unit,
extract_time_slice, apply_hillshade
VARIABLE_LABELS, VARIABLE_TITLES,
resolve_file_path, make_relative_path
)

# Import visualizers
from aeolis.gui.visualizers.domain import DomainVisualizer
from aeolis.gui.visualizers.wind import WindVisualizer
from aeolis.gui.visualizers.output_2d import Output2DVisualizer
from aeolis.gui.visualizers.output_1d import Output1DVisualizer
# Import GUI tabs
from aeolis.gui.gui_tabs.domain import DomainVisualizer
from aeolis.gui.gui_tabs.wind import WindVisualizer
from aeolis.gui.gui_tabs.output_2d import Output2DVisualizer
from aeolis.gui.gui_tabs.output_1d import Output1DVisualizer
from aeolis.gui.gui_tabs.model_runner import ModelRunner

from windrose import WindroseAxes

# Initialize with default configuration
configfile = "No file selected"
Expand Down Expand Up @@ -105,6 +97,7 @@ def create_widgets(self):
self.create_input_file_tab(tab_control)
self.create_domain_tab(tab_control)
self.create_wind_input_tab(tab_control)
self.create_run_model_tab(tab_control)
self.create_plot_output_2d_tab(tab_control)
self.create_plot_output_1d_tab(tab_control)
# Pack the tab control to expand and fill the available space
Expand All @@ -118,6 +111,8 @@ def create_widgets(self):

def on_tab_changed(self, event):
"""Handle tab change event to auto-plot domain/wind when tab is selected"""
global configfile

# Get the currently selected tab index
selected_tab = self.tab_control.index(self.tab_control.select())

Expand Down Expand Up @@ -158,6 +153,12 @@ def on_tab_changed(self, event):
except Exception as e:
# Silently fail if plotting doesn't work (e.g., file doesn't exist)
pass

# Run Model tab is at index 3 (0: Input file, 1: Domain, 2: Wind, 3: Run Model, 4: Output 2D, 5: Output 1D)
elif selected_tab == 3:
# Update config file label
if hasattr(self, 'model_runner_visualizer'):
self.model_runner_visualizer.update_config_display(configfile)

def create_label_entry(self, tab, text, value, row):
# Create a label and entry widget for a given tab
Expand Down Expand Up @@ -408,7 +409,7 @@ def load_new_config(self):
wind_file = self.wind_file_entry.get()
if wind_file and wind_file.strip():
self.load_and_plot_wind()
except:
except Exception:
pass # Silently fail if tabs not yet initialized

messagebox.showinfo("Success", f"Configuration loaded from:\n{file_path}")
Expand Down Expand Up @@ -476,8 +477,8 @@ def toggle_y_limits(self):
self.ymax_entry_1d.config(state='normal')

# Update plot if data is loaded
if hasattr(self, 'nc_data_cache_1d') and self.nc_data_cache_1d is not None:
self.update_1d_plot()
if hasattr(self, 'output_1d_visualizer') and self.output_1d_visualizer.nc_data_cache_1d is not None:
self.output_1d_visualizer.update_plot()

def load_and_plot_wind(self):
"""
Expand Down Expand Up @@ -631,7 +632,7 @@ def create_plot_output_2d_tab(self, tab_control):

# Browse button for NC file
nc_browse_btn = ttk.Button(file_frame, text="Browse...",
command=lambda: self.browse_nc_file())
command=self.browse_nc_file)
nc_browse_btn.grid(row=0, column=2, sticky=W, pady=2)

# Variable selection dropdown
Expand Down Expand Up @@ -787,7 +788,7 @@ def create_plot_output_1d_tab(self, tab_control):

# Browse button for NC file
nc_browse_btn_1d = ttk.Button(file_frame_1d, text="Browse...",
command=lambda: self.browse_nc_file_1d())
command=self.browse_nc_file_1d)
nc_browse_btn_1d.grid(row=0, column=2, sticky=W, pady=2)

# Variable selection dropdown
Expand Down Expand Up @@ -909,6 +910,16 @@ def create_plot_output_1d_tab(self, tab_control):
self.time_slider_1d.pack(side=LEFT, fill=X, expand=1, padx=5)
self.time_slider_1d.set(0)

# Hold On button
self.hold_on_btn_1d = ttk.Button(slider_frame_1d, text="Hold On",
command=self.toggle_hold_on_1d)
self.hold_on_btn_1d.pack(side=LEFT, padx=5)

# Clear Held Plots button
self.clear_held_btn_1d = ttk.Button(slider_frame_1d, text="Clear Held",
command=self.clear_held_plots_1d)
self.clear_held_btn_1d.pack(side=LEFT, padx=5)

# Initialize 1D output visualizer (after all UI components are created)
self.output_1d_visualizer = Output1DVisualizer(
self.output_1d_ax, self.output_1d_overview_ax,
Expand All @@ -918,7 +929,8 @@ def create_plot_output_1d_tab(self, tab_control):
self.variable_var_1d, self.transect_direction_var,
self.nc_file_entry_1d, self.variable_dropdown_1d,
self.output_1d_overview_canvas,
self.get_config_dir, self.get_variable_label, self.get_variable_title
self.get_config_dir, self.get_variable_label, self.get_variable_title,
self.auto_ylimits_var, self.ymin_entry_1d, self.ymax_entry_1d
)

# Update slider commands to use visualizer
Expand Down Expand Up @@ -993,6 +1005,21 @@ def update_1d_plot(self):
"""
if hasattr(self, 'output_1d_visualizer'):
self.output_1d_visualizer.update_plot()

def toggle_hold_on_1d(self):
"""
Toggle hold on for the 1D transect plot.
This allows overlaying multiple time steps on the same plot.
"""
if hasattr(self, 'output_1d_visualizer'):
self.output_1d_visualizer.toggle_hold_on()

def clear_held_plots_1d(self):
"""
Clear all held plots from the 1D transect visualization.
"""
if hasattr(self, 'output_1d_visualizer'):
self.output_1d_visualizer.clear_held_plots()

def get_variable_label(self, var_name):
"""
Expand Down Expand Up @@ -1338,6 +1365,85 @@ def enable_overlay_vegetation(self):
current_time = int(self.time_slider.get())
self.update_time_step(current_time)

def create_run_model_tab(self, tab_control):
"""Create the 'Run Model' tab for executing AeoLiS simulations"""
tab_run = ttk.Frame(tab_control)
tab_control.add(tab_run, text='Run Model')

# Configure grid weights
tab_run.columnconfigure(0, weight=1)
tab_run.rowconfigure(1, weight=1)

# Create control frame
control_frame = ttk.LabelFrame(tab_run, text="Model Control", padding=10)
control_frame.grid(row=0, column=0, padx=10, pady=10, sticky=(N, W, E))

# Config file display
config_label = ttk.Label(control_frame, text="Config file:")
config_label.grid(row=0, column=0, sticky=W, pady=5)

run_config_label = ttk.Label(control_frame, text="No file selected",
foreground="gray")
run_config_label.grid(row=0, column=1, sticky=W, pady=5, padx=(10, 0))

# Start/Stop buttons
button_frame = ttk.Frame(control_frame)
button_frame.grid(row=1, column=0, columnspan=2, pady=10)

start_model_btn = ttk.Button(button_frame, text="Start Model", width=15)
start_model_btn.pack(side=LEFT, padx=5)

stop_model_btn = ttk.Button(button_frame, text="Stop Model",
width=15, state=DISABLED)
stop_model_btn.pack(side=LEFT, padx=5)

# Progress bar
model_progress = ttk.Progressbar(control_frame, mode='indeterminate', length=400)
model_progress.grid(row=2, column=0, columnspan=2, pady=5, sticky=(W, E))

# Status label
model_status_label = ttk.Label(control_frame, text="Ready", foreground="blue")
model_status_label.grid(row=3, column=0, columnspan=2, sticky=W, pady=5)

# Create output frame for logging
output_frame = ttk.LabelFrame(tab_run, text="Model Output / Logging", padding=10)
output_frame.grid(row=1, column=0, padx=10, pady=(0, 10), sticky=(N, S, E, W))
output_frame.rowconfigure(0, weight=1)
output_frame.columnconfigure(0, weight=1)

# Create Text widget with scrollbar for terminal output
output_scroll = ttk.Scrollbar(output_frame)
output_scroll.grid(row=0, column=1, sticky=(N, S))

model_output_text = Text(output_frame, wrap=WORD,
yscrollcommand=output_scroll.set,
height=20, width=80,
bg='black', fg='lime',
font=('Courier', 9))
model_output_text.grid(row=0, column=0, sticky=(N, S, E, W))
output_scroll.config(command=model_output_text.yview)

# Add clear button
clear_btn = ttk.Button(output_frame, text="Clear Output",
command=lambda: model_output_text.delete(1.0, END))
clear_btn.grid(row=1, column=0, columnspan=2, pady=(5, 0))

# Initialize model runner visualizer
self.model_runner_visualizer = ModelRunner(
start_model_btn, stop_model_btn, model_progress,
model_status_label, model_output_text, run_config_label,
self.root, self.get_current_config_file
)

# Connect button commands
start_model_btn.config(command=self.model_runner_visualizer.start_model)
stop_model_btn.config(command=self.model_runner_visualizer.stop_model)

def get_current_config_file(self):
"""Get the current config file path"""
global configfile
return configfile

def save(self):
# Save the current entries to the configuration dictionary
for field, entry in self.entries.items():
Expand Down
16 changes: 16 additions & 0 deletions aeolis/gui/gui_tabs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
GUI Tabs package for AeoLiS GUI.

This package contains specialized tab modules for different types of data:
- domain: Domain setup visualization (bed, vegetation, etc.)
- wind: Wind input visualization (time series, wind roses)
- output_2d: 2D output visualization
- output_1d: 1D transect visualization
"""

from aeolis.gui.gui_tabs.domain import DomainVisualizer
from aeolis.gui.gui_tabs.wind import WindVisualizer
from aeolis.gui.gui_tabs.output_2d import Output2DVisualizer
from aeolis.gui.gui_tabs.output_1d import Output1DVisualizer

__all__ = ['DomainVisualizer', 'WindVisualizer', 'Output2DVisualizer', 'Output1DVisualizer']
Loading
Loading