solarviewer 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- solar_radio_image_viewer/__init__.py +12 -0
- solar_radio_image_viewer/assets/add_tab_default.png +0 -0
- solar_radio_image_viewer/assets/add_tab_default_light.png +0 -0
- solar_radio_image_viewer/assets/add_tab_hover.png +0 -0
- solar_radio_image_viewer/assets/add_tab_hover_light.png +0 -0
- solar_radio_image_viewer/assets/browse.png +0 -0
- solar_radio_image_viewer/assets/browse_light.png +0 -0
- solar_radio_image_viewer/assets/close_tab_default.png +0 -0
- solar_radio_image_viewer/assets/close_tab_default_light.png +0 -0
- solar_radio_image_viewer/assets/close_tab_hover.png +0 -0
- solar_radio_image_viewer/assets/close_tab_hover_light.png +0 -0
- solar_radio_image_viewer/assets/ellipse_selection.png +0 -0
- solar_radio_image_viewer/assets/ellipse_selection_light.png +0 -0
- solar_radio_image_viewer/assets/icons8-ellipse-90.png +0 -0
- solar_radio_image_viewer/assets/icons8-ellipse-90_light.png +0 -0
- solar_radio_image_viewer/assets/icons8-info-90.png +0 -0
- solar_radio_image_viewer/assets/icons8-info-90_light.png +0 -0
- solar_radio_image_viewer/assets/profile.png +0 -0
- solar_radio_image_viewer/assets/profile_light.png +0 -0
- solar_radio_image_viewer/assets/rectangle_selection.png +0 -0
- solar_radio_image_viewer/assets/rectangle_selection_light.png +0 -0
- solar_radio_image_viewer/assets/reset.png +0 -0
- solar_radio_image_viewer/assets/reset_light.png +0 -0
- solar_radio_image_viewer/assets/ruler.png +0 -0
- solar_radio_image_viewer/assets/ruler_light.png +0 -0
- solar_radio_image_viewer/assets/search.png +0 -0
- solar_radio_image_viewer/assets/search_light.png +0 -0
- solar_radio_image_viewer/assets/settings.png +0 -0
- solar_radio_image_viewer/assets/settings_light.png +0 -0
- solar_radio_image_viewer/assets/splash.fits +0 -0
- solar_radio_image_viewer/assets/zoom_60arcmin.png +0 -0
- solar_radio_image_viewer/assets/zoom_60arcmin_light.png +0 -0
- solar_radio_image_viewer/assets/zoom_in.png +0 -0
- solar_radio_image_viewer/assets/zoom_in_light.png +0 -0
- solar_radio_image_viewer/assets/zoom_out.png +0 -0
- solar_radio_image_viewer/assets/zoom_out_light.png +0 -0
- solar_radio_image_viewer/create_video.py +1345 -0
- solar_radio_image_viewer/dialogs.py +2665 -0
- solar_radio_image_viewer/from_simpl/__init__.py +184 -0
- solar_radio_image_viewer/from_simpl/caltable_visualizer.py +1001 -0
- solar_radio_image_viewer/from_simpl/dynamic_spectra_dialog.py +332 -0
- solar_radio_image_viewer/from_simpl/make_dynamic_spectra.py +351 -0
- solar_radio_image_viewer/from_simpl/pipeline_logger_gui.py +1232 -0
- solar_radio_image_viewer/from_simpl/simpl_theme.py +352 -0
- solar_radio_image_viewer/from_simpl/utils.py +984 -0
- solar_radio_image_viewer/from_simpl/view_dynamic_spectra_GUI.py +1975 -0
- solar_radio_image_viewer/helioprojective.py +1916 -0
- solar_radio_image_viewer/helioprojective_viewer.py +817 -0
- solar_radio_image_viewer/helioviewer_browser.py +1514 -0
- solar_radio_image_viewer/main.py +148 -0
- solar_radio_image_viewer/move_phasecenter.py +1269 -0
- solar_radio_image_viewer/napari_viewer.py +368 -0
- solar_radio_image_viewer/noaa_events/__init__.py +32 -0
- solar_radio_image_viewer/noaa_events/noaa_events.py +430 -0
- solar_radio_image_viewer/noaa_events/noaa_events_gui.py +1922 -0
- solar_radio_image_viewer/norms.py +293 -0
- solar_radio_image_viewer/radio_data_downloader/__init__.py +25 -0
- solar_radio_image_viewer/radio_data_downloader/radio_data_downloader.py +756 -0
- solar_radio_image_viewer/radio_data_downloader/radio_data_downloader_gui.py +528 -0
- solar_radio_image_viewer/searchable_combobox.py +220 -0
- solar_radio_image_viewer/solar_context/__init__.py +41 -0
- solar_radio_image_viewer/solar_context/active_regions.py +371 -0
- solar_radio_image_viewer/solar_context/cme_alerts.py +234 -0
- solar_radio_image_viewer/solar_context/context_images.py +297 -0
- solar_radio_image_viewer/solar_context/realtime_data.py +528 -0
- solar_radio_image_viewer/solar_data_downloader/__init__.py +35 -0
- solar_radio_image_viewer/solar_data_downloader/solar_data_downloader.py +1667 -0
- solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_cli.py +901 -0
- solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_gui.py +1210 -0
- solar_radio_image_viewer/styles.py +643 -0
- solar_radio_image_viewer/utils/__init__.py +32 -0
- solar_radio_image_viewer/utils/rate_limiter.py +255 -0
- solar_radio_image_viewer/utils.py +952 -0
- solar_radio_image_viewer/video_dialog.py +2629 -0
- solar_radio_image_viewer/video_utils.py +656 -0
- solar_radio_image_viewer/viewer.py +11174 -0
- solarviewer-1.0.2.dist-info/METADATA +343 -0
- solarviewer-1.0.2.dist-info/RECORD +82 -0
- solarviewer-1.0.2.dist-info/WHEEL +5 -0
- solarviewer-1.0.2.dist-info/entry_points.txt +8 -0
- solarviewer-1.0.2.dist-info/licenses/LICENSE +21 -0
- solarviewer-1.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Radio Data Downloader GUI - PyQt5-based interface for downloading radio solar data.
|
|
4
|
+
|
|
5
|
+
This module provides a graphical interface for downloading radio solar data
|
|
6
|
+
from various observatories (starting with Learmonth) and converting to FITS format.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
import os
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from PyQt5.QtWidgets import (
|
|
16
|
+
QApplication,
|
|
17
|
+
QMainWindow,
|
|
18
|
+
QWidget,
|
|
19
|
+
QVBoxLayout,
|
|
20
|
+
QHBoxLayout,
|
|
21
|
+
QLabel,
|
|
22
|
+
QComboBox,
|
|
23
|
+
QPushButton,
|
|
24
|
+
QLineEdit,
|
|
25
|
+
QDateTimeEdit,
|
|
26
|
+
QFileDialog,
|
|
27
|
+
QProgressBar,
|
|
28
|
+
QMessageBox,
|
|
29
|
+
QGroupBox,
|
|
30
|
+
QCheckBox,
|
|
31
|
+
QTextEdit,
|
|
32
|
+
)
|
|
33
|
+
from PyQt5.QtCore import Qt, QDateTime, pyqtSignal, QThread
|
|
34
|
+
except ImportError:
|
|
35
|
+
print("Error: PyQt5 is required. Please install it with:")
|
|
36
|
+
print(" pip install PyQt5")
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
|
|
39
|
+
# Import the radio data downloader module
|
|
40
|
+
try:
|
|
41
|
+
from . import radio_data_downloader as rdd
|
|
42
|
+
except ImportError:
|
|
43
|
+
try:
|
|
44
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
45
|
+
if script_dir not in sys.path:
|
|
46
|
+
sys.path.append(script_dir)
|
|
47
|
+
import radio_data_downloader as rdd
|
|
48
|
+
except ImportError:
|
|
49
|
+
print("Error: Could not import radio_data_downloader module.")
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DownloadWorker(QThread):
|
|
54
|
+
"""Worker thread for handling downloads without blocking the UI."""
|
|
55
|
+
|
|
56
|
+
progress = pyqtSignal(str)
|
|
57
|
+
finished = pyqtSignal(str) # Emits the path to the created FITS file
|
|
58
|
+
error = pyqtSignal(str)
|
|
59
|
+
|
|
60
|
+
def __init__(self, params: dict):
|
|
61
|
+
super().__init__()
|
|
62
|
+
self.params = params
|
|
63
|
+
self._cancelled = False
|
|
64
|
+
|
|
65
|
+
def cancel(self):
|
|
66
|
+
"""Cancel the download."""
|
|
67
|
+
self._cancelled = True
|
|
68
|
+
|
|
69
|
+
def run(self):
|
|
70
|
+
"""Execute the download and conversion."""
|
|
71
|
+
try:
|
|
72
|
+
# Extract site name from the instrument combo text (e.g., "Learmonth (Australia)" -> "Learmonth")
|
|
73
|
+
instrument = self.params.get("instrument", "Learmonth")
|
|
74
|
+
# Map GUI display names to internal site names
|
|
75
|
+
site_map = {
|
|
76
|
+
"Learmonth (Australia)": "Learmonth",
|
|
77
|
+
"San Vito (Italy)": "San Vito",
|
|
78
|
+
"Palehua (Hawaii, USA)": "Palehua",
|
|
79
|
+
"Holloman (New Mexico, USA)": "Holloman",
|
|
80
|
+
# Handle direct names too for backwards compatibility
|
|
81
|
+
"Learmonth": "Learmonth",
|
|
82
|
+
"San Vito": "San Vito",
|
|
83
|
+
"Palehua": "Palehua",
|
|
84
|
+
"Holloman": "Holloman",
|
|
85
|
+
}
|
|
86
|
+
site = site_map.get(instrument, "Learmonth")
|
|
87
|
+
|
|
88
|
+
date = self.params.get("date")
|
|
89
|
+
start_time = self.params.get("start_time")
|
|
90
|
+
end_time = self.params.get("end_time")
|
|
91
|
+
output_dir = self.params.get("output_dir")
|
|
92
|
+
bkg_sub = self.params.get("background_subtract", False)
|
|
93
|
+
do_flag = self.params.get("flag_bad_channels", True)
|
|
94
|
+
flag_cal = self.params.get("flag_cal_time", True)
|
|
95
|
+
|
|
96
|
+
# Use generic RSTN download function for all sites
|
|
97
|
+
result = rdd.download_and_convert_rstn(
|
|
98
|
+
site=site,
|
|
99
|
+
date=date,
|
|
100
|
+
output_dir=output_dir,
|
|
101
|
+
start_time=start_time,
|
|
102
|
+
end_time=end_time,
|
|
103
|
+
bkg_sub=bkg_sub,
|
|
104
|
+
do_flag=do_flag,
|
|
105
|
+
flag_cal_time=flag_cal,
|
|
106
|
+
progress_callback=self.progress.emit,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if result:
|
|
110
|
+
self.finished.emit(result)
|
|
111
|
+
else:
|
|
112
|
+
self.error.emit(f"Download or conversion failed for {site}. Check if data is available for this date.")
|
|
113
|
+
|
|
114
|
+
except Exception as e:
|
|
115
|
+
self.error.emit(str(e))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class RadioDataDownloaderGUI(QMainWindow):
|
|
119
|
+
"""Main window for the Radio Data Downloader GUI."""
|
|
120
|
+
|
|
121
|
+
def __init__(self, parent=None, initial_datetime=None):
|
|
122
|
+
super().__init__(parent)
|
|
123
|
+
self.setWindowTitle("Radio Solar Data Downloader")
|
|
124
|
+
self.setMinimumWidth(600)
|
|
125
|
+
self.setMinimumHeight(800)
|
|
126
|
+
|
|
127
|
+
# Store initial datetime for time selection
|
|
128
|
+
self.initial_datetime = initial_datetime
|
|
129
|
+
|
|
130
|
+
# Main widget and layout
|
|
131
|
+
self.main_widget = QWidget()
|
|
132
|
+
self.setCentralWidget(self.main_widget)
|
|
133
|
+
self.layout = QVBoxLayout(self.main_widget)
|
|
134
|
+
|
|
135
|
+
# Create UI sections
|
|
136
|
+
self.create_instrument_selection()
|
|
137
|
+
self.create_time_selection()
|
|
138
|
+
self.create_output_selection()
|
|
139
|
+
self.create_processing_options()
|
|
140
|
+
self.create_download_section()
|
|
141
|
+
self.create_log_section()
|
|
142
|
+
|
|
143
|
+
# Initialize worker
|
|
144
|
+
self.download_worker = None
|
|
145
|
+
|
|
146
|
+
def create_instrument_selection(self):
|
|
147
|
+
"""Create the instrument selection section."""
|
|
148
|
+
group = QGroupBox("Select Instrument")
|
|
149
|
+
layout = QVBoxLayout()
|
|
150
|
+
|
|
151
|
+
self.instrument_combo = QComboBox()
|
|
152
|
+
self.instrument_combo.addItems([
|
|
153
|
+
"Learmonth (Australia)",
|
|
154
|
+
"San Vito (Italy)",
|
|
155
|
+
"Palehua (Hawaii, USA)",
|
|
156
|
+
"Holloman (New Mexico, USA)",
|
|
157
|
+
])
|
|
158
|
+
self.instrument_combo.currentTextChanged.connect(self.on_instrument_changed)
|
|
159
|
+
|
|
160
|
+
# Info label - dynamically updated based on selection
|
|
161
|
+
self.info_label = QLabel(
|
|
162
|
+
"RSTN Solar Spectrographs: 25-180 MHz dynamic spectra."
|
|
163
|
+
)
|
|
164
|
+
self.info_label.setStyleSheet("color: gray; font-style: italic;")
|
|
165
|
+
self.info_label.setWordWrap(True)
|
|
166
|
+
|
|
167
|
+
layout.addWidget(self.instrument_combo)
|
|
168
|
+
layout.addWidget(self.info_label)
|
|
169
|
+
group.setLayout(layout)
|
|
170
|
+
self.layout.addWidget(group)
|
|
171
|
+
|
|
172
|
+
def on_instrument_changed(self, text):
|
|
173
|
+
"""Update info label when instrument selection changes."""
|
|
174
|
+
site_info = {
|
|
175
|
+
"Learmonth (Australia)": "Learmonth Solar Observatory, Western Australia.\nFrequency: 25-180 MHz | Data from BOM Australia or NOAA NCEI.",
|
|
176
|
+
"San Vito (Italy)": "San Vito dei Normanni, Southern Italy.\nFrequency: 25-180 MHz | Data from NOAA NCEI archive.",
|
|
177
|
+
"Palehua (Hawaii, USA)": "Palehua, Hawaii.\nFrequency: 25-180 MHz | Data from NOAA NCEI archive.",
|
|
178
|
+
"Holloman (New Mexico, USA)": "Holloman AFB, New Mexico.\nFrequency: 25-180 MHz | ⚠️ Limited data: Apr 2000 - Jul 2004 only.",
|
|
179
|
+
}
|
|
180
|
+
self.info_label.setText(site_info.get(text, "RSTN Solar Spectrograph: 25-180 MHz"))
|
|
181
|
+
|
|
182
|
+
def create_time_selection(self):
|
|
183
|
+
"""Create the time range selection section."""
|
|
184
|
+
group = QGroupBox("Time Range")
|
|
185
|
+
layout = QVBoxLayout()
|
|
186
|
+
|
|
187
|
+
# Full day observation toggle
|
|
188
|
+
self.full_day_checkbox = QCheckBox("Full Day Observation (no time filtering)")
|
|
189
|
+
self.full_day_checkbox.setChecked(True) # Default to full day
|
|
190
|
+
self.full_day_checkbox.setToolTip(
|
|
191
|
+
"When checked, downloads the entire day's observation without time filtering"
|
|
192
|
+
)
|
|
193
|
+
self.full_day_checkbox.toggled.connect(self.on_full_day_toggled)
|
|
194
|
+
layout.addWidget(self.full_day_checkbox)
|
|
195
|
+
|
|
196
|
+
# Date-only selector (visible when full day is checked)
|
|
197
|
+
self.date_layout_widget = QWidget()
|
|
198
|
+
date_layout = QHBoxLayout(self.date_layout_widget)
|
|
199
|
+
date_layout.setContentsMargins(0, 0, 0, 0)
|
|
200
|
+
date_layout.addWidget(QLabel("Date:"))
|
|
201
|
+
self.date_only_edit = QDateTimeEdit()
|
|
202
|
+
self.date_only_edit.setCalendarPopup(True)
|
|
203
|
+
self.date_only_edit.setDisplayFormat("yyyy.MM.dd")
|
|
204
|
+
|
|
205
|
+
# Use initial_datetime if provided, otherwise yesterday
|
|
206
|
+
if self.initial_datetime:
|
|
207
|
+
initial_qdt = QDateTime(self.initial_datetime)
|
|
208
|
+
self.date_only_edit.setDateTime(initial_qdt)
|
|
209
|
+
else:
|
|
210
|
+
yesterday = QDateTime.currentDateTime().addDays(-1)
|
|
211
|
+
self.date_only_edit.setDateTime(yesterday)
|
|
212
|
+
date_layout.addWidget(self.date_only_edit)
|
|
213
|
+
layout.addWidget(self.date_layout_widget)
|
|
214
|
+
|
|
215
|
+
# Start time (hidden when full day is checked)
|
|
216
|
+
self.start_layout_widget = QWidget()
|
|
217
|
+
start_layout = QHBoxLayout(self.start_layout_widget)
|
|
218
|
+
start_layout.setContentsMargins(0, 0, 0, 0)
|
|
219
|
+
start_layout.addWidget(QLabel("Start:"))
|
|
220
|
+
self.start_datetime = QDateTimeEdit()
|
|
221
|
+
self.start_datetime.setCalendarPopup(True)
|
|
222
|
+
self.start_datetime.setDisplayFormat("yyyy.MM.dd HH:mm:ss")
|
|
223
|
+
|
|
224
|
+
# Use initial_datetime if provided, otherwise yesterday 00:00:00
|
|
225
|
+
if self.initial_datetime:
|
|
226
|
+
initial_qdt = QDateTime(self.initial_datetime)
|
|
227
|
+
self.start_datetime.setDateTime(initial_qdt)
|
|
228
|
+
else:
|
|
229
|
+
yesterday = QDateTime.currentDateTime().addDays(-1)
|
|
230
|
+
yesterday.setTime(yesterday.time().fromString("00:00:00", "HH:mm:ss"))
|
|
231
|
+
self.start_datetime.setDateTime(yesterday)
|
|
232
|
+
self.start_datetime.dateChanged.connect(self.on_start_date_changed)
|
|
233
|
+
self.start_datetime.dateTimeChanged.connect(self.on_start_time_changed)
|
|
234
|
+
start_layout.addWidget(self.start_datetime)
|
|
235
|
+
layout.addWidget(self.start_layout_widget)
|
|
236
|
+
|
|
237
|
+
# End time (hidden when full day is checked)
|
|
238
|
+
self.end_layout_widget = QWidget()
|
|
239
|
+
end_layout = QHBoxLayout(self.end_layout_widget)
|
|
240
|
+
end_layout.setContentsMargins(0, 0, 0, 0)
|
|
241
|
+
end_layout.addWidget(QLabel("End:"))
|
|
242
|
+
self.end_datetime = QDateTimeEdit()
|
|
243
|
+
self.end_datetime.setCalendarPopup(True)
|
|
244
|
+
self.end_datetime.setDisplayFormat("yyyy.MM.dd HH:mm:ss")
|
|
245
|
+
|
|
246
|
+
# Use initial_datetime + 1 hour if provided, otherwise yesterday 23:59:59
|
|
247
|
+
if self.initial_datetime:
|
|
248
|
+
from datetime import timedelta
|
|
249
|
+
end_dt = self.initial_datetime + timedelta(hours=1)
|
|
250
|
+
self.end_datetime.setDateTime(QDateTime(end_dt))
|
|
251
|
+
else:
|
|
252
|
+
end_time = QDateTime.currentDateTime().addDays(-1)
|
|
253
|
+
end_time.setTime(end_time.time().fromString("23:59:59", "HH:mm:ss"))
|
|
254
|
+
self.end_datetime.setDateTime(end_time)
|
|
255
|
+
end_layout.addWidget(self.end_datetime)
|
|
256
|
+
layout.addWidget(self.end_layout_widget)
|
|
257
|
+
|
|
258
|
+
# Initially hide start/end time pickers (full day is default)
|
|
259
|
+
self.start_layout_widget.hide()
|
|
260
|
+
self.end_layout_widget.hide()
|
|
261
|
+
|
|
262
|
+
# Note about data availability
|
|
263
|
+
'''note_label = QLabel(
|
|
264
|
+
"Note: Data may not be available for all dates. "
|
|
265
|
+
"Learmonth data is typically available within 1-2 days."
|
|
266
|
+
)
|
|
267
|
+
note_label.setStyleSheet("color: #888;")
|
|
268
|
+
note_label.setWordWrap(True)
|
|
269
|
+
layout.addWidget(note_label)'''
|
|
270
|
+
|
|
271
|
+
group.setLayout(layout)
|
|
272
|
+
self.layout.addWidget(group)
|
|
273
|
+
|
|
274
|
+
def on_full_day_toggled(self, checked):
|
|
275
|
+
"""Toggle time picker visibility based on full day checkbox."""
|
|
276
|
+
if checked:
|
|
277
|
+
# Full day mode - show only date picker
|
|
278
|
+
self.date_layout_widget.show()
|
|
279
|
+
self.start_layout_widget.hide()
|
|
280
|
+
self.end_layout_widget.hide()
|
|
281
|
+
else:
|
|
282
|
+
# Time range mode - show start/end time pickers
|
|
283
|
+
self.date_layout_widget.hide()
|
|
284
|
+
self.start_layout_widget.show()
|
|
285
|
+
self.end_layout_widget.show()
|
|
286
|
+
|
|
287
|
+
def on_start_date_changed(self, new_date):
|
|
288
|
+
"""Sync end date when start date is changed."""
|
|
289
|
+
from PyQt5.QtCore import QTime
|
|
290
|
+
# Keep the current end time but change the date
|
|
291
|
+
current_end_time = self.end_datetime.time()
|
|
292
|
+
end_dt = QDateTime(new_date, current_end_time)
|
|
293
|
+
self.end_datetime.setDateTime(end_dt)
|
|
294
|
+
|
|
295
|
+
def on_start_time_changed(self, new_datetime):
|
|
296
|
+
"""Sync end time when start time is changed (keep 1 hour difference)."""
|
|
297
|
+
# Set end time to start time + 1 hour
|
|
298
|
+
end_dt = new_datetime.addSecs(3600)
|
|
299
|
+
self.end_datetime.setDateTime(end_dt)
|
|
300
|
+
|
|
301
|
+
def create_output_selection(self):
|
|
302
|
+
"""Create the output directory selection section."""
|
|
303
|
+
group = QGroupBox("Output Settings")
|
|
304
|
+
layout = QVBoxLayout()
|
|
305
|
+
|
|
306
|
+
dir_layout = QHBoxLayout()
|
|
307
|
+
dir_layout.addWidget(QLabel("Output Directory:"))
|
|
308
|
+
|
|
309
|
+
self.output_dir = QLineEdit()
|
|
310
|
+
self.output_dir.setText(os.path.join(os.getcwd(), "radio_solar_data"))
|
|
311
|
+
dir_layout.addWidget(self.output_dir)
|
|
312
|
+
|
|
313
|
+
browse_button = QPushButton("Browse...")
|
|
314
|
+
browse_button.clicked.connect(self.browse_output_dir)
|
|
315
|
+
dir_layout.addWidget(browse_button)
|
|
316
|
+
|
|
317
|
+
layout.addLayout(dir_layout)
|
|
318
|
+
group.setLayout(layout)
|
|
319
|
+
self.layout.addWidget(group)
|
|
320
|
+
|
|
321
|
+
def create_processing_options(self):
|
|
322
|
+
"""Create the processing options section."""
|
|
323
|
+
group = QGroupBox("Processing Options")
|
|
324
|
+
layout = QVBoxLayout()
|
|
325
|
+
|
|
326
|
+
self.flag_checkbox = QCheckBox("Flag known bad frequency channels")
|
|
327
|
+
self.flag_checkbox.setChecked(True)
|
|
328
|
+
self.flag_checkbox.setToolTip(
|
|
329
|
+
"Remove data from frequency channels known to have interference or issues"
|
|
330
|
+
)
|
|
331
|
+
layout.addWidget(self.flag_checkbox)
|
|
332
|
+
|
|
333
|
+
self.flag_cal_checkbox = QCheckBox("Flag calibration time periods")
|
|
334
|
+
self.flag_cal_checkbox.setChecked(True)
|
|
335
|
+
self.flag_cal_checkbox.setToolTip(
|
|
336
|
+
"Detect and remove calibration periods that show as spikes in the data"
|
|
337
|
+
)
|
|
338
|
+
layout.addWidget(self.flag_cal_checkbox)
|
|
339
|
+
|
|
340
|
+
self.bkg_sub_checkbox = QCheckBox("Background subtraction")
|
|
341
|
+
self.bkg_sub_checkbox.setChecked(False)
|
|
342
|
+
self.bkg_sub_checkbox.setToolTip(
|
|
343
|
+
"Normalize each frequency channel by its median value"
|
|
344
|
+
)
|
|
345
|
+
layout.addWidget(self.bkg_sub_checkbox)
|
|
346
|
+
|
|
347
|
+
group.setLayout(layout)
|
|
348
|
+
self.layout.addWidget(group)
|
|
349
|
+
|
|
350
|
+
def create_download_section(self):
|
|
351
|
+
"""Create the download button and progress section."""
|
|
352
|
+
layout = QHBoxLayout()
|
|
353
|
+
|
|
354
|
+
self.download_button = QPushButton("Download && Convert to FITS")
|
|
355
|
+
self.download_button.clicked.connect(self.start_download)
|
|
356
|
+
self.download_button.setMinimumHeight(40)
|
|
357
|
+
self.download_button.setStyleSheet("font-weight: bold;")
|
|
358
|
+
layout.addWidget(self.download_button)
|
|
359
|
+
|
|
360
|
+
self.cancel_button = QPushButton("Cancel")
|
|
361
|
+
self.cancel_button.clicked.connect(self.cancel_download)
|
|
362
|
+
self.cancel_button.setEnabled(False)
|
|
363
|
+
self.cancel_button.setMinimumHeight(40)
|
|
364
|
+
layout.addWidget(self.cancel_button)
|
|
365
|
+
|
|
366
|
+
self.layout.addLayout(layout)
|
|
367
|
+
|
|
368
|
+
# Progress bar
|
|
369
|
+
self.progress_bar = QProgressBar()
|
|
370
|
+
self.progress_bar.setTextVisible(False)
|
|
371
|
+
self.progress_bar.setRange(0, 0) # Indeterminate
|
|
372
|
+
self.progress_bar.hide()
|
|
373
|
+
self.layout.addWidget(self.progress_bar)
|
|
374
|
+
|
|
375
|
+
def create_log_section(self):
|
|
376
|
+
"""Create the log/status section."""
|
|
377
|
+
group = QGroupBox("Status")
|
|
378
|
+
layout = QVBoxLayout()
|
|
379
|
+
|
|
380
|
+
self.log_text = QTextEdit()
|
|
381
|
+
self.log_text.setReadOnly(True)
|
|
382
|
+
self.log_text.setMaximumHeight(150)
|
|
383
|
+
self.log_text.setPlaceholderText("Download status will appear here...")
|
|
384
|
+
|
|
385
|
+
layout.addWidget(self.log_text)
|
|
386
|
+
group.setLayout(layout)
|
|
387
|
+
self.layout.addWidget(group)
|
|
388
|
+
|
|
389
|
+
def browse_output_dir(self):
|
|
390
|
+
"""Open directory selection dialog."""
|
|
391
|
+
directory = QFileDialog.getExistingDirectory(
|
|
392
|
+
self, "Select Output Directory", self.output_dir.text()
|
|
393
|
+
)
|
|
394
|
+
if directory:
|
|
395
|
+
self.output_dir.setText(directory)
|
|
396
|
+
|
|
397
|
+
def log_message(self, message: str):
|
|
398
|
+
"""Add a message to the log."""
|
|
399
|
+
self.log_text.append(message)
|
|
400
|
+
# Scroll to bottom
|
|
401
|
+
scrollbar = self.log_text.verticalScrollBar()
|
|
402
|
+
scrollbar.setValue(scrollbar.maximum())
|
|
403
|
+
|
|
404
|
+
def start_download(self):
|
|
405
|
+
"""Start the download process."""
|
|
406
|
+
# Validate inputs
|
|
407
|
+
output_dir = self.output_dir.text()
|
|
408
|
+
if not output_dir:
|
|
409
|
+
QMessageBox.warning(self, "Error", "Please specify an output directory.")
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
# Check if full day mode or time range mode
|
|
413
|
+
if self.full_day_checkbox.isChecked():
|
|
414
|
+
# Full day mode - no time filtering
|
|
415
|
+
date = self.date_only_edit.dateTime().toString("yyyy-MM-dd")
|
|
416
|
+
start_time = None
|
|
417
|
+
end_time = None
|
|
418
|
+
log_msg = f"Starting download for {date} (full day observation)..."
|
|
419
|
+
else:
|
|
420
|
+
# Time range mode
|
|
421
|
+
start_dt = self.start_datetime.dateTime()
|
|
422
|
+
end_dt = self.end_datetime.dateTime()
|
|
423
|
+
|
|
424
|
+
# Validate time range
|
|
425
|
+
if start_dt >= end_dt:
|
|
426
|
+
QMessageBox.warning(self, "Error", "Start time must be before end time.")
|
|
427
|
+
return
|
|
428
|
+
|
|
429
|
+
date = start_dt.toString("yyyy-MM-dd")
|
|
430
|
+
start_time = start_dt.toString("HH:mm:ss")
|
|
431
|
+
end_time = end_dt.toString("HH:mm:ss")
|
|
432
|
+
log_msg = f"Starting download for {date}...\nTime range: {start_time} to {end_time}"
|
|
433
|
+
|
|
434
|
+
params = {
|
|
435
|
+
"instrument": self.instrument_combo.currentText(),
|
|
436
|
+
"date": date,
|
|
437
|
+
"start_time": start_time,
|
|
438
|
+
"end_time": end_time,
|
|
439
|
+
"output_dir": output_dir,
|
|
440
|
+
"background_subtract": self.bkg_sub_checkbox.isChecked(),
|
|
441
|
+
"flag_bad_channels": self.flag_checkbox.isChecked(),
|
|
442
|
+
"flag_cal_time": self.flag_cal_checkbox.isChecked(),
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
# Update UI
|
|
446
|
+
self.download_button.setEnabled(False)
|
|
447
|
+
self.cancel_button.setEnabled(True)
|
|
448
|
+
self.progress_bar.show()
|
|
449
|
+
self.log_text.clear()
|
|
450
|
+
self.log_message(log_msg)
|
|
451
|
+
|
|
452
|
+
# Start worker thread
|
|
453
|
+
self.download_worker = DownloadWorker(params)
|
|
454
|
+
self.download_worker.progress.connect(self.on_progress)
|
|
455
|
+
self.download_worker.finished.connect(self.on_finished)
|
|
456
|
+
self.download_worker.error.connect(self.on_error)
|
|
457
|
+
self.download_worker.start()
|
|
458
|
+
|
|
459
|
+
def cancel_download(self):
|
|
460
|
+
"""Cancel the current download."""
|
|
461
|
+
if self.download_worker:
|
|
462
|
+
self.download_worker.cancel()
|
|
463
|
+
self.log_message("Cancelling download...")
|
|
464
|
+
|
|
465
|
+
def on_progress(self, message: str):
|
|
466
|
+
"""Handle progress updates."""
|
|
467
|
+
self.log_message(message)
|
|
468
|
+
|
|
469
|
+
def on_finished(self, fits_file: str):
|
|
470
|
+
"""Handle download completion."""
|
|
471
|
+
self.download_button.setEnabled(True)
|
|
472
|
+
self.cancel_button.setEnabled(False)
|
|
473
|
+
self.progress_bar.hide()
|
|
474
|
+
|
|
475
|
+
self.log_message(f"\n✓ Success! FITS file created:")
|
|
476
|
+
self.log_message(f" {fits_file}")
|
|
477
|
+
self.log_message("\nYou can now open this file with the Dynamic Spectrum Viewer:")
|
|
478
|
+
self.log_message(" Tools → LOFAR Tools → Dynamic Spectrum Viewer")
|
|
479
|
+
|
|
480
|
+
QMessageBox.information(
|
|
481
|
+
self,
|
|
482
|
+
"Download Complete",
|
|
483
|
+
f"FITS file created successfully!\n\n{fits_file}\n\n"
|
|
484
|
+
"You can open this file with the Dynamic Spectrum Viewer."
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
def on_error(self, error_message: str):
|
|
488
|
+
"""Handle download errors."""
|
|
489
|
+
self.download_button.setEnabled(True)
|
|
490
|
+
self.cancel_button.setEnabled(False)
|
|
491
|
+
self.progress_bar.hide()
|
|
492
|
+
|
|
493
|
+
self.log_message(f"\n✗ Error: {error_message}")
|
|
494
|
+
|
|
495
|
+
QMessageBox.critical(
|
|
496
|
+
self,
|
|
497
|
+
"Download Failed",
|
|
498
|
+
f"Failed to download or convert data:\n\n{error_message}"
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def launch_gui(parent=None, initial_datetime=None) -> RadioDataDownloaderGUI:
|
|
503
|
+
"""
|
|
504
|
+
Launch the Radio Data Downloader GUI.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
parent: Optional parent widget for integration with other PyQt applications
|
|
508
|
+
initial_datetime: Optional datetime to initialize the time selectors
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
RadioDataDownloaderGUI: The main window instance
|
|
512
|
+
"""
|
|
513
|
+
if not QApplication.instance():
|
|
514
|
+
app = QApplication(sys.argv)
|
|
515
|
+
else:
|
|
516
|
+
app = QApplication.instance()
|
|
517
|
+
|
|
518
|
+
window = RadioDataDownloaderGUI(parent, initial_datetime=initial_datetime)
|
|
519
|
+
window.show()
|
|
520
|
+
|
|
521
|
+
if parent is None:
|
|
522
|
+
sys.exit(app.exec_())
|
|
523
|
+
|
|
524
|
+
return window
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
if __name__ == "__main__":
|
|
528
|
+
launch_gui()
|