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.
Files changed (82) hide show
  1. solar_radio_image_viewer/__init__.py +12 -0
  2. solar_radio_image_viewer/assets/add_tab_default.png +0 -0
  3. solar_radio_image_viewer/assets/add_tab_default_light.png +0 -0
  4. solar_radio_image_viewer/assets/add_tab_hover.png +0 -0
  5. solar_radio_image_viewer/assets/add_tab_hover_light.png +0 -0
  6. solar_radio_image_viewer/assets/browse.png +0 -0
  7. solar_radio_image_viewer/assets/browse_light.png +0 -0
  8. solar_radio_image_viewer/assets/close_tab_default.png +0 -0
  9. solar_radio_image_viewer/assets/close_tab_default_light.png +0 -0
  10. solar_radio_image_viewer/assets/close_tab_hover.png +0 -0
  11. solar_radio_image_viewer/assets/close_tab_hover_light.png +0 -0
  12. solar_radio_image_viewer/assets/ellipse_selection.png +0 -0
  13. solar_radio_image_viewer/assets/ellipse_selection_light.png +0 -0
  14. solar_radio_image_viewer/assets/icons8-ellipse-90.png +0 -0
  15. solar_radio_image_viewer/assets/icons8-ellipse-90_light.png +0 -0
  16. solar_radio_image_viewer/assets/icons8-info-90.png +0 -0
  17. solar_radio_image_viewer/assets/icons8-info-90_light.png +0 -0
  18. solar_radio_image_viewer/assets/profile.png +0 -0
  19. solar_radio_image_viewer/assets/profile_light.png +0 -0
  20. solar_radio_image_viewer/assets/rectangle_selection.png +0 -0
  21. solar_radio_image_viewer/assets/rectangle_selection_light.png +0 -0
  22. solar_radio_image_viewer/assets/reset.png +0 -0
  23. solar_radio_image_viewer/assets/reset_light.png +0 -0
  24. solar_radio_image_viewer/assets/ruler.png +0 -0
  25. solar_radio_image_viewer/assets/ruler_light.png +0 -0
  26. solar_radio_image_viewer/assets/search.png +0 -0
  27. solar_radio_image_viewer/assets/search_light.png +0 -0
  28. solar_radio_image_viewer/assets/settings.png +0 -0
  29. solar_radio_image_viewer/assets/settings_light.png +0 -0
  30. solar_radio_image_viewer/assets/splash.fits +0 -0
  31. solar_radio_image_viewer/assets/zoom_60arcmin.png +0 -0
  32. solar_radio_image_viewer/assets/zoom_60arcmin_light.png +0 -0
  33. solar_radio_image_viewer/assets/zoom_in.png +0 -0
  34. solar_radio_image_viewer/assets/zoom_in_light.png +0 -0
  35. solar_radio_image_viewer/assets/zoom_out.png +0 -0
  36. solar_radio_image_viewer/assets/zoom_out_light.png +0 -0
  37. solar_radio_image_viewer/create_video.py +1345 -0
  38. solar_radio_image_viewer/dialogs.py +2665 -0
  39. solar_radio_image_viewer/from_simpl/__init__.py +184 -0
  40. solar_radio_image_viewer/from_simpl/caltable_visualizer.py +1001 -0
  41. solar_radio_image_viewer/from_simpl/dynamic_spectra_dialog.py +332 -0
  42. solar_radio_image_viewer/from_simpl/make_dynamic_spectra.py +351 -0
  43. solar_radio_image_viewer/from_simpl/pipeline_logger_gui.py +1232 -0
  44. solar_radio_image_viewer/from_simpl/simpl_theme.py +352 -0
  45. solar_radio_image_viewer/from_simpl/utils.py +984 -0
  46. solar_radio_image_viewer/from_simpl/view_dynamic_spectra_GUI.py +1975 -0
  47. solar_radio_image_viewer/helioprojective.py +1916 -0
  48. solar_radio_image_viewer/helioprojective_viewer.py +817 -0
  49. solar_radio_image_viewer/helioviewer_browser.py +1514 -0
  50. solar_radio_image_viewer/main.py +148 -0
  51. solar_radio_image_viewer/move_phasecenter.py +1269 -0
  52. solar_radio_image_viewer/napari_viewer.py +368 -0
  53. solar_radio_image_viewer/noaa_events/__init__.py +32 -0
  54. solar_radio_image_viewer/noaa_events/noaa_events.py +430 -0
  55. solar_radio_image_viewer/noaa_events/noaa_events_gui.py +1922 -0
  56. solar_radio_image_viewer/norms.py +293 -0
  57. solar_radio_image_viewer/radio_data_downloader/__init__.py +25 -0
  58. solar_radio_image_viewer/radio_data_downloader/radio_data_downloader.py +756 -0
  59. solar_radio_image_viewer/radio_data_downloader/radio_data_downloader_gui.py +528 -0
  60. solar_radio_image_viewer/searchable_combobox.py +220 -0
  61. solar_radio_image_viewer/solar_context/__init__.py +41 -0
  62. solar_radio_image_viewer/solar_context/active_regions.py +371 -0
  63. solar_radio_image_viewer/solar_context/cme_alerts.py +234 -0
  64. solar_radio_image_viewer/solar_context/context_images.py +297 -0
  65. solar_radio_image_viewer/solar_context/realtime_data.py +528 -0
  66. solar_radio_image_viewer/solar_data_downloader/__init__.py +35 -0
  67. solar_radio_image_viewer/solar_data_downloader/solar_data_downloader.py +1667 -0
  68. solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_cli.py +901 -0
  69. solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_gui.py +1210 -0
  70. solar_radio_image_viewer/styles.py +643 -0
  71. solar_radio_image_viewer/utils/__init__.py +32 -0
  72. solar_radio_image_viewer/utils/rate_limiter.py +255 -0
  73. solar_radio_image_viewer/utils.py +952 -0
  74. solar_radio_image_viewer/video_dialog.py +2629 -0
  75. solar_radio_image_viewer/video_utils.py +656 -0
  76. solar_radio_image_viewer/viewer.py +11174 -0
  77. solarviewer-1.0.2.dist-info/METADATA +343 -0
  78. solarviewer-1.0.2.dist-info/RECORD +82 -0
  79. solarviewer-1.0.2.dist-info/WHEEL +5 -0
  80. solarviewer-1.0.2.dist-info/entry_points.txt +8 -0
  81. solarviewer-1.0.2.dist-info/licenses/LICENSE +21 -0
  82. 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()