diff --git a/assets/icons/app_icon.png b/assets/icons/app_icon.png new file mode 100644 index 0000000..09c832f Binary files /dev/null and b/assets/icons/app_icon.png differ diff --git a/config/app_styles.qss b/assets/styles/app_stlyes.qss similarity index 90% rename from config/app_styles.qss rename to assets/styles/app_stlyes.qss index f13ac94..c50ebd0 100644 --- a/config/app_styles.qss +++ b/assets/styles/app_stlyes.qss @@ -64,3 +64,11 @@ QWidget#waveformChartContainer { border-radius: 8px; padding: 16px; } + +QLabel#chartTitleLabel { + font-size: 14px; + font-weight: bold; + color: #CBD5E0; + padding-bottom: 2px; + padding-left: 5px; +} diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..4867f05 --- /dev/null +++ b/config/__init__.py @@ -0,0 +1,2 @@ +from .areas_config import AREAS_OF_INTEREST +from .stations_config import ALL_STATIONS_CONFIG, SENSOR_TYPE_CHANNELS, STATION_LOOKUP diff --git a/config/__pycache__/__init__.cpython-313.pyc b/config/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..0495235 Binary files /dev/null and b/config/__pycache__/__init__.cpython-313.pyc differ diff --git a/config/__pycache__/areas_config.cpython-313.pyc b/config/__pycache__/areas_config.cpython-313.pyc new file mode 100644 index 0000000..1a9a5e9 Binary files /dev/null and b/config/__pycache__/areas_config.cpython-313.pyc differ diff --git a/config/__pycache__/stations_config.cpython-313.pyc b/config/__pycache__/stations_config.cpython-313.pyc new file mode 100644 index 0000000..2074527 Binary files /dev/null and b/config/__pycache__/stations_config.cpython-313.pyc differ diff --git a/config/areas_config.py b/config/areas_config.py new file mode 100644 index 0000000..87d9a2b --- /dev/null +++ b/config/areas_config.py @@ -0,0 +1,173 @@ +# DICTIONARY mapping area names to a list of station codes belonging to that area. +# +# Each station config in the list is a dictionary: +# { +# code: StationCode +# preferred_channel: channelCode +# preferred_location: locationCode +# } +AREAS_OF_INTEREST = { + "Northland & Auckland": [ + + ], + "Waikato": [ + + ], + "Hikaurangi Subduction Zone": [ + { + "code": "RIZ", + "preferred_channel": "HHE", + "preferred_location": "10" + }, + { + "code": "GLKZ", + "preferred_channel": "HHE", + "preferred_location": "10" + + }, + { + "code": "MXZ", + "preferred_channel": "HHE", + "preferred_location": "10" + }, + { + "code": "WMGZ", + "preferred_channel": "EHE", + "preferred_location": "10" + }, + { + "code": "PUZ", + "preferred_channel": "HHE", + "preferred_location": "10" + }, + { + "code": "CNGZ", + "preferred_channel": "EHE", + "preferred_location": "10" + }, + { + "code": "RIGZ", + "preferred_channel": "EHE", + "preferred_location": "10" + }, + { + "code": "PRGZ", + "preferred_channel": "EHE", + "preferred_location": "10" + }, + { + "code": "KNZ", + "preferred_channel": "HHE", + "preferred_location": "10" + }, + { + "code": "WHHZ", + "preferred_channel": "EHE", + "preferred_location": "10" + }, + { + "code": "ARHZ", + "preferred_channel": "EHE", + "preferred_location": "10" + }, + { + "code": "CKHZ", + "preferred_channel": "EHE", + "preferred_location": "10" + }, + { + "code": "KAHZ", + "preferred_channel": "EHE", + "preferred_location": "10" + }, + { + "code": "PXZ", + "preferred_channel": "HHE", + "preferred_location": "10" + }, + { + "code": "PRHZ", + "preferred_channel": "EHE", + "preferred_location": "10" + }, + { + "code": "BFZ", + "preferred_channel": "HHE", + "preferred_location": "10" + }, + { + "code": "CPWZ", + "preferred_channel": "EHE", + "preferred_location": "10" + }, + { + "code": "TMWZ", + "preferred_channel": "EHE", + "preferred_location": "10" + }, + { + "code": "TRWZ", + "preferred_channel": "EHE", + "preferred_location": "10" + }, + { + "code": "WRRZ", + "preferred_channel": "HHE", + "preferred_location": "10" + }, + { + "code": "CMWZ", + "preferred_channel": "EHE", + "preferred_location": "10" + }, + { + "code": "BSWZ", + "preferred_channel": "HHE", + "preferred_location": "10" + }, + { + "code": "CTZ", + "preferred_channel": "HHE", + "preferred_location": "10" + }, + ], + "Rotorua & East Cape": [ + + ], + "White Island": [ + + ], + "Taupo & Greater Area": [ + + ], + "Hawkes Bay": [ + + ], + "Mount Taranaki": [ + + ], + "Palmerston North & Whanganui": [ + + ], + "Mount Ruapehu": [ + + ], + "Wellington (Ashurst & South)": [ + + ], + "Christchurch & Kaikoura incl. Banks Peninsula": [ + + ], + "Southland (Otago, Invicargil Milford Sound etc)": [ + + ], + "Stewart Island, Snares Island & Antartica": [ + + ], + "Southern Alps / Alpine Fault": [ + + ], + "Queenstown & Wanaka": [ + + ], +} diff --git a/config/stations_config.py b/config/stations_config.py new file mode 100644 index 0000000..0aaf5ba --- /dev/null +++ b/config/stations_config.py @@ -0,0 +1,251 @@ +""" + THIS IS A DICTIONARY that maps a sensot type to a list of available channels for that type +""" +SENSOR_TYPE_CHANNELS = { + "Broadband": ["BHZ", "BHN", "BHE", "HHZ", "HHN", "HHE", "LHZ", "LHN", "LHE"], + "Short Period": ["EHZ", "EHN", "EHE", "SHZ", "SHN", "SHE"], + "Strong Motion": ["HNZ", "HNN", "HNE"], + "All Channels": [] +} + +#List of all known stations with their details +# Each Station is a DICTIONARY +ALL_STATIONS_CONFIG = [ + { + "code": "RIZ", + "name": "Bells Beach, Raoul Is.", + "location_codes": { + "HH": "10", + "LH": "10", + "default": "10" + }, + "types": ["Broadband"], + "area_tags": ["Raoul Island", "Bells Beach" "Hikaurangi Trench"] + }, + { + "code": "GLKZ", + "name": "Green Lake, Raoul Is.", + "location_codes": { + "HH": "10", + "LH": "10", + "default": "10" + }, + "types": ["Broadband"], + "area_tags": ["Raoul Island", "Green Lake", "Hikaurangi Trench"] + }, + { + "code": "MXZ", + "name": "Te Araroa", + "location_codes": { + "HH": "10", + "LH": "10", + "default": "10" + }, + "types": ["Broadband"], + "area_tags": ["East Coast", "Te Araroa", "North Island", "Hikaurangi Trench"] + }, + { + "code": "WMGZ", + "name": "Ruatoria", + "location_codes": { + "EH": "10", + "default": "10" + }, + "types": ["Short Period"], + "area_tags": ["East Coast", "Ruatoria", "North Island", "Hikaurangi Trench"] + }, + { + "code": "PUZ", + "name": "Tokomaru Bay", + "location_codes": { + "HH": "10", + "LH": "10", + "default": "10" + }, + "types": ["Broadband"], + "area_tags": ["East Coast", "Tokomaru Bay", "North Island", "Hikaurangi Trench"] + }, + { + "code": "CNGZ", + "name": "Tologa Bay", + "location_codes": { + "EH": "10", + "default": "10" + }, + "types": ["Shot Period"], + "area_tags": ["East Coast", "Tologa Bay", "Hikaurangi Trench", "North Island"] + }, + { + "code": "RIGZ", + "name": "Gisborne", + "location_codes": { + "EH": "10", + "default": "10" + }, + "types": ["Short Period"], + "area_tags": ["Gisborne", "East Coast", "Hikaurangi Trench", "North Island"] + }, +{ + "code": "PRGZ", + "name": "Tikiwhata", + "location_codes": { + "EH": "10", + "default": "10" + }, + "types": ["Short Period"], + "area_tags": ["East Coast", "Tikiwhata", "Hikaurangi Trench", "North Island"] + }, + { + "code": "KNZ", + "name": "Wairoa", + "location_codes": { + "HH": "10", + "LH": "10", + "default": "10" + }, + "types": ["Broadband"], + "area_tags": ["Wairoa", "East Coast", "Hikaurangi Trench", "North Island"] + }, + { + "code": "WHHZ", + "name": "Waihua", + "location_codes": { + "EH": "10", + "default": "10" + }, + "types": ["Short Period"], + "area_tags": ["Waihua", "East Coast", "Hikaurangi Trench", "North Island"] + }, + { + "code": "ARHZ", + "name": "Aropaonui", + "location_codes": { + "EH": "10", + "default": "10" + }, + "types": ["Short Period"], + "area_tags": ["East Coast", "Aropaonui", "Hikaurangi Trench", "North Island"] + }, + { + "code": "CKHZ", + "name": "Cape Kidnappers", + "location_codes": { + "EH": "10", + "default": "10" + }, + "types": ["Short Period"], + "area_tags": ["Cape Kidnappers", "East Coast", "Hikaurangi Trench", "North Island"] + }, + { + "code": "KAHZ", + "name": "Tukituki", + "location_codes": { + "EH": "10", + "default": "10" + }, + "types": ["Short Period"], + "area_tags": ["Tukituki", "East Coast", "Hikaurangi Trench", "North Island"] + }, + { + "code": "PXZ", + "name": "Pawanui", + "location_codes": { + "HH": "10", + "LH": "10", + "default": "10" + }, + "types": ["Broadband"], + "area_tags": ["Pawanui", "East Coast", "Hikaurangi Trench", "North Island"] + }, + { + "code": "PRHZ", + "name": "Porangahau", + "location_codes": { + "EH": "10", + "default": "10" + }, + "types": ["Short Period"], + "area_tags": ["Porangahau", "East Coast", "Hikaurangi Trench", "North Island"] + }, + { + "code": "BFZ", + "name": "Birch Farm", + "location_codes": { + "HH": "10", + "LH": "10", + "default": "10" + }, + "types": ["Broadband"], + "area_tags": ["Birch Farm", "Hikaurangi Trench", "North Island"] + }, +{ + "code": "CPWZ", + "name": "Castlepoint", + "location_codes": { + "EH": "10", + "default": "10" + }, + "types": ["Short Period"], + "area_tags": ["Castlepoint", "Hikaurangi Trench", "North Island"] + }, +{ + "code": "TMWZ", + "name": "Traveller", + "location_codes": { + "EH": "10", + "default": "10" + }, + "types": ["Short Period"], + "area_tags": ["Traveller", "Hikaurangi Trench", "North Island"] + }, +{ + "code": "WRRZ", + "name": "White Rock", + "location_codes": { + "HH": "10", + "LH": "10", + "default": "10" + }, + "types": ["Broadband"], + "area_tags": ["White Rock", "Hikaurangi Trench", "North Island"] + }, +{ + "code": "CMWZ", + "name": "Cape Campbell", + "location_codes": { + "EH": "10", + "default": "10" + }, + "types": ["Short Period"], + "area_tags": ["Cape Campbell", "North Island" "Hikaurangi Trench"] + }, +{ + "code": "BSWZ", + "name": "Black Birch Station", + "location_codes": { + "HH": "10", + "LH": "10", + "default": "10" + }, + "types": ["Broadband"], + "area_tags": ["Black Birch Station", "South Island" "Hikaurangi Trench"] + }, + { + "code": "CTZ", + "name": "Chatham Islands", + "location_codes": { + "HH": "10", + "LH": "10", + "default": "10" + }, + "types": ["Broadband"], + "area_tags": ["Chatham Islands", "Hikaurangi Trench"] + }, +] + +# Post processing for convenience +ALL_UNIQUE_CHANNELS = sorted(list(set(sum(SENSOR_TYPE_CHANNELS.values(), [])))) +SENSOR_TYPE_CHANNELS["All Channels"] = ALL_UNIQUE_CHANNELS + +# Pre built dictionary for quick lookup by station code +STATION_LOOKUP = {station['code']: station for station in ALL_STATIONS_CONFIG} diff --git a/src/__pycache__/main.cpython-313.pyc b/src/__pycache__/main.cpython-313.pyc index 67e3084..f48c6bf 100644 Binary files a/src/__pycache__/main.cpython-313.pyc and b/src/__pycache__/main.cpython-313.pyc differ diff --git a/src/data/persistence.py b/src/data/persistence.py new file mode 100644 index 0000000..a7d5905 --- /dev/null +++ b/src/data/persistence.py @@ -0,0 +1,37 @@ +import json +import os + +APP_SETTINGS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__))), 'config', 'app_settings.json') +DEFAULT_SETTINGS = { + 'theme': 'dark_blue.xml', + 'default_duration_minutes': 30, + 'last_area_selected': ' --- Select Area ---', + 'last_sensor_type': 'Broadband', + 'last_staion_selected': 'WEL', + 'last_station_selected': 'HHZ', + 'last_location_selected': '10', + # Add More Settings here +} + +def load_app_settings(): + """Loads application setings from app_settings.json""" + settings = DEFAULT_SETTINGS.copy() + if os.path.exists(APP_SETTINGS_FILE): + try: + with open(APP_SETTINGS_FILE, 'r') as f: + user_settings = json.load(f) + settings.update(user_settings) + except json.JSONDecodeError as e: + print(f"Warning: Could not decode app_settings.json. Using default settings. Error: {e}") + except Exception as e: + print(f"Warning: Error loading app_settings.json. Using default settings. Error: {e}") + return settings + +def save_app_settings(settings): + """Saves application settings to app_settings.json.""" + try: + os.makedirs(os.path.dirname(APP_SETTINGS_FILE), exist_ok=True) + with open(APP_SETTINGS_FILE, 'w') as f: + json.dump(seettings, f, indent=4) + except Exception as e: + print(f"ERROR: could not save app_settings.json. Error: {e}") diff --git a/src/main.py b/src/main.py index bf63736..3c48d46 100644 --- a/src/main.py +++ b/src/main.py @@ -4,6 +4,24 @@ from PyQt6.QtWidgets import QApplication from src.ui.main_window import MainWindow from qt_material import apply_stylesheet +# --- Debug Imports --- +print("\n--- sys.path DEBUG ---") +print("Current Working Directory:", os.getcwd()) +print("Script Directory:", os.path.dirname(os.path.abspath(__file__))) +print("Project Root (calculated):", os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')) +print("Python sys.path:") +for p in sys.path: + print(f" - {p}") +print("--- END sys.path DEBUG ---\n") + +#test direct import +try: + import config.areas_config + print(f"DEBUG: AREAS_OF_INTEREST keys: {config.areas_config.AREAS_OF_INTEREST.keys()}") +except Exception as e: + print(f"DEBUG: Failed to directly import config.areas_config: {e}") +# --- END DEBUGGING INPORTS --- + def main(): app = QApplication(sys.argv) diff --git a/src/ui/__pycache__/main_window.cpython-313.pyc b/src/ui/__pycache__/main_window.cpython-313.pyc index ed2497e..5cd7f59 100644 Binary files a/src/ui/__pycache__/main_window.cpython-313.pyc and b/src/ui/__pycache__/main_window.cpython-313.pyc differ diff --git a/src/ui/__pycache__/waveform_chart.cpython-313.pyc b/src/ui/__pycache__/waveform_chart.cpython-313.pyc index 544994a..22ad3c5 100644 Binary files a/src/ui/__pycache__/waveform_chart.cpython-313.pyc and b/src/ui/__pycache__/waveform_chart.cpython-313.pyc differ diff --git a/src/ui/main_window.py b/src/ui/main_window.py index 99f39d6..f2681ee 100644 --- a/src/ui/main_window.py +++ b/src/ui/main_window.py @@ -1,138 +1,159 @@ -from PyQt6.QtWidgets import QMainWindow, QLabel, QVBoxLayout, QWidget, QPushButton, QComboBox, QHBoxLayout -from PyQt6.QtCore import Qt, QTimer -from src.ui.waveform_chart import WaveformChart -from src.data.geonet_api import fetch_waveform_data +from PyQt6.QtWidgets import ( QMainWindow, QLabel, QVBoxLayout, QWidget, QStackedWidget, QMenuBar, QMenu, QStatusBar, QApplication ) +from PyQt6.QtCore import Qt, QTimer, Qsize +from src.ui.views.single_waveform_view import SingleWaveformView +from src.ui.views.area_waveforms_view import AreaWavefromsView +from src.ui.dialogs.settings_dialog import SettingsDialog + +import config import sys +# Theme Modes +THEME_MODE_DARK = "dark_blue.xml" +THEME_MODE_LIGHT = "light_blue.xml" +THEME_MODE_SYSTEM = "system" # Not directly supported but Qt-Material + class MainWindow(QMainWindow): def __init__(self): super().__init__() - self.setWindowTitle("NZ EarthQuake Viewer (POC)") - self.setGeometry(100, 100, 1200, 800) # x, y, width, height - self.setMinimumSize(800, 600) - + self.setWindowTitle("NZ Earthquake Viewer V0.2 (POC)") + self.setGeometry(100, 100, 1200, 800) + self.seMinimumSize(800, 600) + + # Load Pesistent Settings + self.app_settings = load_app_settings() + self._apply_theme(self.app_settings.get('theme', THEME_MODE_DARK)) + + self.resizeEvent = self._on_resize_event + + # --- Central Widget & Layout --- central_widget = QWidget() self.setCentralWidget(central_widget) + self.main_layout = QVBoxLayout(central_widget) + self.main_layout.setContentsMargins(10, 10, 10, 10) + self.main_layout.setSpacing(8) - main_layout = QVBoxLayout(central_widget) - main_layout.setContentsMargins(20, 20, 20, 20) - main_layout.setSpacing(15) - - controls_layout = QHBoxLayout() - + # App Title Label self.app_title_label = QLabel("NZ Seismic Viewer", self) self.app_title_label.setObjectName("headerLabel") - main_layout.addWidget(self.app_title_label) + self.main_layout.addWidget(self.app_title_label) - #Station & channel selection - station_label = QLabel("Station:", self) - station_label.setProperty("controlLabel", True) - self.station_selector = QComboBox(self) - self.station_selector.addItems(["WEL", "MAZ", "LTZ", "WRRZ"]) + # Manage different views + self.stacked_widget = QStackedWidget(self) + self.main_layout.addWidget(self.stackedWidget) - channel_label = QLabel("Channel:", self) - channel_label.setProperty("controlLabel", True) - self.channel_selector = QComboBox(self) - self.channel_selector.addItems([ - "BHZ", "BHN", "BHE", # Common Broadband - "HHZ", "HHN", "HHE", # High-Gain Broadband - "LHZ", "LHN", "LHE", # Low-Gain Broadband, Long Period + # Instantiate views + self.area_waveforms_view = AreaWaveformsView(self) + self.single_waveform_view = SingleWaveformView(self) - "EHZ", "EHN", "EHE", # Short-Period (Popular) + self.stacked_widget.addWidget(self.area_waveforms_view) # Index 0 + self.stacked_widget.addWidget(self.single_waveform_view) # Index 1 - "HNZ", "HNN", "HNE", # High-Gain, Strong Motion + # Menu Bar + self._create_menu_bar() - # OTHER COMMON Channels that could be encountered in the future - # ONLY UNCOMMENT THE LINE WHEN NEEDED! - # "BMZ", "BNN", "BNE", # Broadband Variants - # "ENZ", "ENN", "ENE", # Short-Period Variants - # "SHZ", "SHN", "SHE", # other Short-Period variants (Less Common in NZ) - ]) - self.channel_selector.setCurrentText("HHE") # Default - - #Duration Selector - duration_label = QLabel("Duration (min):", self) - duration_label.setProperty("controlLabel", True) - self.duration_selector = QComboBox(self) - self.duration_selector.addItems(["5", "15", "20", "30", "60", "90", "120"]) # 60 =1hr | 90 = 1.5hrs | 120 = 2hrs - self.duration_selector.setCurrentText("30") # Default to 30 mins - - #Location Code Selector (GeoNet requires this to not be '--') - location_label = QLabel("Location:", self) - location_label.setProperty("controlLabel", True) - self.location_selector = QComboBox(self) - self.location_selector.addItems(["--", "00", "01", "02", "03", "10", "20", "22", "30"]) # DOUBLE CHECK AS GET INTO THE MULTIPLE GRAPHS - self.location_selector.setCurrentText("10") # most responsive I've found on swarm - BC - - # Fetch Button - self.fetch_button = QPushButton("Fetch waveform", self) - self.fetch_button.clicked.connect(self.fetch_and_plot_waveform) - - # LAYOUT - controls_layout.addWidget(station_label) - controls_layout.addWidget(self.station_selector) - controls_layout.addSpacing(15) - controls_layout.addWidget(channel_label) - controls_layout.addWidget(self.channel_selector) - controls_layout.addSpacing(15) - controls_layout.addWidget(location_label) - controls_layout.addWidget(self.location_selector) - controls_layout.addSpacing(15) - controls_layout.addWidget(duration_label) - controls_layout.addWidget(self.duration_selector) - controls_layout.addStretch(1) - controls_layout.addWidget(self.fetch_button) - - main_layout.addLayout(controls_layout) - - self.status_label = QLabel("Ready.", self) + # Status Bar + self.status_bar = QStatusBar(self) + self.setStatusBar(self.status_bar) + self.status_label = QLabel("Ready.") self.status_label.setObjectName("statusLabel") + self.status_abr.addWidget(self.status_label, 1) - main_layout.addWidget(self.status_label) - - self.waveform_chart = WaveformChart(self) - self.waveform_chart.setObjectName("waveformChartContainer") - - main_layout.addWidget(self.waveform_chart) + # Intiial View and Data Load + self.show_area_waveforms_view() + # Polling Timer self.polling_timer = QTimer(self) - self.polling_timer.setInterval(15 * 1000) # 15 seconds - self.polling_timer.timeout.connect(self.fetch_and_plot_waveform) + self.polling_timer.setInterval(15 * 1000) + self.polling_timer.timeout.connect(self._refresh_current_view) self.polling_timer.start() - self.fetch_and_plot_waveform() + # Menu bar creation + def _create_menu_bar(self): + menu_bar = self.menuBar() - def set_status(self, message, is_error=False): - self.status_label.setText(message) + # View Menu + view_menu = menu_bar.addMenu("&View") + area_view_action = view_menu.addAction("Area Waveforms") + single_view_action = view_menu.addAction("Single Waveform") - if is_error: - self.status_label.setStyleSheet("color: #EF4444; font-weight: bold;") - else: - self.status_label.setStyleSheet("color: #6B7280; font-weight: normal;") + area_view_action.triggered.connect(self.show_area_waveforms_view) + single_view_action.triggered.connect(self.show_single_waveform_view) - def fetch_and_plot_waveform(self): - station = self.station_selector.currentText() - channel = self.channel_selector.currentText() - location_code = self.location_selector.currentText() - self.set_status(f"Fetching data for {station}-{channel}...", is_error=False) - self.fetch_button.setEnabled(False) + # Options Menu + options_menu = menu_bar.addMenu("&Options") + settings_action = options_menu.addAction("Settings") + settings_action.triggered.connect(self.show_settings_dialog) - try: - traces = fetch_waveform_data(station, channel, location=location_code, duration_seconds=1800) + # Help Menu + help_menu = menu_bar.addMenu("&Help") + about_action = help_menu.addAction("About") + about_action.triggered.connect(self.show_about_dialog) - if traces: - selected_trace = next((t for t in traces if t['channel'] == channel), None) - if selected_trace and selected_trace['points']: - self.waveform_chart.plot_waveform(selected_trace['points'], f"Live Waveform: {station}-{channel}") - self.set_status(f"Data for {station}-{channel} updated. Last sample at {selected_trace['points'][-1]['x'].strftime('%H:%M:%S')}", is_error=False) - else: - self.set_status(f"No waveform points found for {station}-{channel} in this window", is_error=True) - self.waveform_chart.plot_waveform([], f"No data: {station}-{channel}") + # view Management Methods + def show_area_waveforms_view(self): + self.stacked_widget.setCurrentWidget(self.area_waveforms_view) + self.area_waveforms_view.load_selected_area_waveforms() + self.area_waveforms_view.start_polling() # may need self._refresh_current_view() if start_polling() not work + self.set_status("Showing Area Waveforms.") + self.current_view_mode = "area" + + def show_single_waveform_view(self): + self.stacked_widget.setCurrentWidget(self.single_waveform_view) + self.single_waveform_view.start_polling() + self.set_status("Showing Single Waveform. Select Parameters and click 'fetch' button") + self.current_view_mode = "single" + + def show_settigns_dialog(self): + settings_dialog = sSettignsDialog(self.app_settings, self) + settings_dialog.settings_changed.connect(self._handle_settings_changed) + settings_dialog.exec() + + def _handle_settings_changed(self, new_settings): + self.app_settigns.update(new_settings) + save_app_settings(self.app_settigns) + + # Apply new theme if it changed + if 'theme' in new_settings: + self._apply_theme(new_settings['theme']) + + def _apply_theme(self, theme_choice): + from qt_material import apply_stylesheet + + current_app = QApplication.instance() + if not current_app: return + + if theme_choic == THEME_MODE_SYSTEM: + # qt-material doesn't have a 'system' theme + # Try to detect the system light / dark mode and set the theme acordingly. + # default to dark mode if system is not detected. + print("System theme detection not fully implemented, defaulting to Dark Theme.") + apply_stylesheet(current_app, theme=THEME_MODE_DARK) + elif theme_choice == THEME_MODE_LIGHT: + apply_stylesheet(current_app, theme=THEME_MODE_LIGHT) + else: # Defaults to dark mode if no specific match + apply_stylesheet(current_app, theme=THEME_MODE_DARK) + print(f"Applied theme: {theme_choice}") + + def _refresh_current_view(self) + """ Refreshes the currently active view's data.""" + if self.current_view_mode == "area": + self.area_waveforms_view.load_area_data() + elif self.current_view_mode == "single": + self.single_waveform_view.load_single_data() + # other views added here following the above format + + def _on_resize_event(self, event): + """Overrides the resize event to trigger re layout in active view""" + if self.current_view_mode == "area": + self.area_waveforms_view.hanndle_resize() + elif self.current_view_mode == "single": + self.single_waveform_view.handle_resize() + QMainWindow.resizeEvent(self, event) + + # Status forwarding for "sub" views + def update_main_status(self, message, is_error=False): + self.status_label.setText(message) + if is_error: + self.status_label.setStlyeSheet("color: #EF4444; font-weight: bold;") else: - self.set_status(f"No Data returned from GeoNet for {station}-{channel}.", is_error=True) - self.waveform_chart.plot_waveform([], f"No data: {station}-{channel}") - except Exception as e: - self.set_status(f"Error fetching waveform: {e}", is_error=True) - print(f"Detailed error in main_window.py: {e}", file=sys.stderr) - finally: - self.fetch_button.setEnabled(True) + self.status_label.setStyleSheet("color: #A0AEC0; font-weight: normal;") diff --git a/config/app_settigns.json b/src/ui/views/__init__.py similarity index 100% rename from config/app_settigns.json rename to src/ui/views/__init__.py diff --git a/src/ui/views/area_waveforms_view.py b/src/ui/views/area_waveforms_view.py new file mode 100644 index 0000000..9d0521c --- /dev/null +++ b/src/ui/views/area_waveforms_view.py @@ -0,0 +1,39 @@ +from PyQt6.QtWidgets import ( QWidget, QLabel, QVBoxLayout, QPushButon, QComboBox, QHBoxLayout, QGridLayout ) +from PyQt6.QtCore import Qt +from src.ui.waveform_chart import WaveformChart +from src.ui.geonet_api import fetch_waveform_data +import config +import sys +import math + +class AreaWaveformsView(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.parent_window = parent + + self.main_layout = QVBoxLayout(self) + self.main_layout.setContentsMargins(0, 0, 0, 0) + self.main_layout.setSpacing(8) + + # Area Selection Controls Row 1 + area_controls_layout = QHBoxLayout() + area_controls_layout.setSpacing(5) + + area_label = QLabel("Area:", self) + area_label.setProperty("controlLabel", True) + area_controls_layout.addWidget(area_label) + + self.area_selector = QComboBox(self) + self.area_selector.addItems(["-- Select Area --"] + list(config.AREAS_OF_INTEREST.keys())) + controls_controls_layout.addWidget(self.area_selector) + + self.load_area_button = QPushButton("Fetch Waveforms", self) + self.load_area_button.setObjectName("loadAreaButton") + self.load_area_button.clicked.connect(self.load_area_data) + + area_controls_layout.addWidget(self.load_area_button) + area_controls_layout.addStretch(1) + self.main_layout.addLayout(area_controls_layout) + + # sensor Type Selector - Row 2 + diff --git a/src/ui/views/dialogs/__init__.py b/src/ui/views/dialogs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/views/dialogs/settigns_dialog.py b/src/ui/views/dialogs/settigns_dialog.py new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/views/single_waveform_view.py b/src/ui/views/single_waveform_view.py new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/waveform_chart.py b/src/ui/waveform_chart.py index 48a2299..7b872dc 100644 --- a/src/ui/waveform_chart.py +++ b/src/ui/waveform_chart.py @@ -6,7 +6,7 @@ import matplotlib.dates as mdates from src.utils.mpl_styles import apply_mpl_styles class WaveformChart(QWidget): - def __init__(self, parent=None): + def __init__(self, parent=None, figsize=(10, 4)): super().__init__(parent) self.layout = QVBoxLayout(self) self.layout.setContentsMargins(0,0,0,0) @@ -16,11 +16,12 @@ class WaveformChart(QWidget): apply_mpl_styles() self.title_label = QLabel("Waveform Chart", self) - self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.title_label.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft) + self.title_label.setObjectName("chartTitleLabel") self.layout.addWidget(self.title_label) # Matplot - self.fig = Figure(figsize=(10,4), dpi=100) + self.fig = Figure(figsize=figsize, dpi=100) self.canvas = FigureCanvas(self.fig) self.layout.addWidget(self.canvas) @@ -36,6 +37,12 @@ class WaveformChart(QWidget): def plot_waveform(self, points, title=""): self.title_label.setText(title) self.ax.clear() + self.ax.set_xlabel("Time") + self.ax.set_ylabel("Amplitude") + self.ax.tick_params(axis='x') + self.ax.tick_params(axis='y') + self.ax.xaxis.set_major_formatter(self.formatter) + self.ax.tick_params(axis='x', rotation=45) if points: x_data = [p['x'] for p in points] diff --git a/src/utils/datetime_helpers.py b/src/utils/datetime_helpers.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/error_handling.py b/src/utils/error_handling.py new file mode 100644 index 0000000..e69de29