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,1210 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Solar Data Viewer GUI - PyQt5-based interface for downloading solar observatory data.
|
|
4
|
+
|
|
5
|
+
This module provides a graphical interface for downloading data from various solar
|
|
6
|
+
observatories using the solar_data_downloader module. It can be used as a standalone
|
|
7
|
+
application or integrated into other PyQt applications.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
import os
|
|
12
|
+
import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional, Dict, List
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from PyQt5.QtWidgets import (
|
|
18
|
+
QApplication,
|
|
19
|
+
QMainWindow,
|
|
20
|
+
QWidget,
|
|
21
|
+
QVBoxLayout,
|
|
22
|
+
QHBoxLayout,
|
|
23
|
+
QLabel,
|
|
24
|
+
QComboBox,
|
|
25
|
+
QPushButton,
|
|
26
|
+
QLineEdit,
|
|
27
|
+
QDateTimeEdit,
|
|
28
|
+
QFileDialog,
|
|
29
|
+
QProgressBar,
|
|
30
|
+
QMessageBox,
|
|
31
|
+
QGroupBox,
|
|
32
|
+
QRadioButton,
|
|
33
|
+
QButtonGroup,
|
|
34
|
+
QCheckBox,
|
|
35
|
+
QScrollArea,
|
|
36
|
+
)
|
|
37
|
+
from PyQt5.QtCore import Qt, QDateTime, pyqtSignal, QThread
|
|
38
|
+
except ImportError:
|
|
39
|
+
print("Error: PyQt5 is required. Please install it with:")
|
|
40
|
+
print(" pip install PyQt5")
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
|
|
43
|
+
# Try to import the solar_data_downloader module
|
|
44
|
+
try:
|
|
45
|
+
# First try relative import (when used as part of package)
|
|
46
|
+
from . import solar_data_downloader as sdd
|
|
47
|
+
except ImportError:
|
|
48
|
+
try:
|
|
49
|
+
# Then try importing from the same directory (when run as script)
|
|
50
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
51
|
+
if script_dir not in sys.path:
|
|
52
|
+
sys.path.append(script_dir)
|
|
53
|
+
import solar_data_downloader as sdd
|
|
54
|
+
except ImportError:
|
|
55
|
+
print("Error: Could not import solar_data_downloader module.")
|
|
56
|
+
print(
|
|
57
|
+
"Make sure solar_data_downloader.py is in the same directory as this script."
|
|
58
|
+
)
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
|
|
61
|
+
# Check if required packages are installed
|
|
62
|
+
try:
|
|
63
|
+
import sunpy
|
|
64
|
+
import drms
|
|
65
|
+
import astropy
|
|
66
|
+
except ImportError as e:
|
|
67
|
+
print(f"Error: Missing required package: {e.name}")
|
|
68
|
+
print("Please install the required packages with:")
|
|
69
|
+
print(" pip install sunpy drms astropy")
|
|
70
|
+
print("For AIA Level 1.5 calibration, also install:")
|
|
71
|
+
print(" pip install aiapy")
|
|
72
|
+
sys.exit(1)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Instruments that only support Fido (no DRMS option)
|
|
76
|
+
FIDO_ONLY_INSTRUMENTS = ["IRIS", "SOHO", "GOES SUVI", "STEREO", "GONG"]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class DownloadWorker(QThread):
|
|
80
|
+
"""Worker thread for handling downloads using subprocess with real-time progress updates."""
|
|
81
|
+
|
|
82
|
+
progress = pyqtSignal(str) # Signal to update progress text
|
|
83
|
+
finished = pyqtSignal(list) # Signal emitted with list of downloaded files
|
|
84
|
+
error = pyqtSignal(str) # Signal emitted when an error occurs
|
|
85
|
+
|
|
86
|
+
def __init__(self, download_params: dict):
|
|
87
|
+
super().__init__()
|
|
88
|
+
self.params = download_params
|
|
89
|
+
self._cancelled = False
|
|
90
|
+
|
|
91
|
+
def cancel(self):
|
|
92
|
+
"""Cancel the download."""
|
|
93
|
+
self._cancelled = True
|
|
94
|
+
|
|
95
|
+
def run(self):
|
|
96
|
+
"""Execute the download operation in a subprocess with real-time progress streaming."""
|
|
97
|
+
import subprocess
|
|
98
|
+
import json
|
|
99
|
+
import re
|
|
100
|
+
import time
|
|
101
|
+
import select
|
|
102
|
+
import os
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
# Create a temporary Python script to run the download
|
|
106
|
+
script = self._generate_download_script()
|
|
107
|
+
|
|
108
|
+
# Run the download in a subprocess with real-time output
|
|
109
|
+
self.progress.emit("Starting download...")
|
|
110
|
+
|
|
111
|
+
process = subprocess.Popen(
|
|
112
|
+
[sys.executable, "-u", "-c", script], # -u for unbuffered output
|
|
113
|
+
stdout=subprocess.PIPE,
|
|
114
|
+
stderr=subprocess.PIPE,
|
|
115
|
+
bufsize=0, # Unbuffered
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Set non-blocking mode on stderr for progress reading
|
|
119
|
+
import fcntl
|
|
120
|
+
fl = fcntl.fcntl(process.stderr, fcntl.F_GETFL)
|
|
121
|
+
fcntl.fcntl(process.stderr, fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
|
122
|
+
fl = fcntl.fcntl(process.stdout, fcntl.F_GETFL)
|
|
123
|
+
fcntl.fcntl(process.stdout, fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
|
124
|
+
|
|
125
|
+
files = []
|
|
126
|
+
stdout_buffer = ""
|
|
127
|
+
stderr_buffer = ""
|
|
128
|
+
total_files = 0
|
|
129
|
+
last_progress_update = time.time()
|
|
130
|
+
|
|
131
|
+
# Read output in real-time
|
|
132
|
+
while True:
|
|
133
|
+
if self._cancelled:
|
|
134
|
+
process.terminate()
|
|
135
|
+
self.error.emit("Download cancelled by user")
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
# Use select to check for readable data
|
|
139
|
+
readable, _, _ = select.select([process.stdout, process.stderr], [], [], 0.1)
|
|
140
|
+
|
|
141
|
+
# Read from stdout (main status messages)
|
|
142
|
+
if process.stdout in readable:
|
|
143
|
+
try:
|
|
144
|
+
chunk = process.stdout.read(4096)
|
|
145
|
+
if chunk:
|
|
146
|
+
stdout_buffer += chunk.decode('utf-8', errors='replace')
|
|
147
|
+
|
|
148
|
+
# Process complete lines
|
|
149
|
+
while '\n' in stdout_buffer:
|
|
150
|
+
line, stdout_buffer = stdout_buffer.split('\n', 1)
|
|
151
|
+
line = line.strip()
|
|
152
|
+
|
|
153
|
+
if line.startswith("DOWNLOADED_FILES:"):
|
|
154
|
+
try:
|
|
155
|
+
files_json = line.replace("DOWNLOADED_FILES:", "").strip()
|
|
156
|
+
files = json.loads(files_json)
|
|
157
|
+
except json.JSONDecodeError:
|
|
158
|
+
pass
|
|
159
|
+
elif line.startswith("Searching for"):
|
|
160
|
+
self.progress.emit(line)
|
|
161
|
+
elif line.startswith("Found"):
|
|
162
|
+
self.progress.emit(line)
|
|
163
|
+
match = re.search(r'Found (\d+) files', line)
|
|
164
|
+
if match:
|
|
165
|
+
total_files = int(match.group(1))
|
|
166
|
+
elif "Successfully" in line:
|
|
167
|
+
self.progress.emit(line)
|
|
168
|
+
elif "Processing" in line:
|
|
169
|
+
self.progress.emit(line)
|
|
170
|
+
except (BlockingIOError, IOError):
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
# Read from stderr (parfive progress bars)
|
|
174
|
+
if process.stderr in readable:
|
|
175
|
+
try:
|
|
176
|
+
chunk = process.stderr.read(4096)
|
|
177
|
+
if chunk:
|
|
178
|
+
stderr_buffer += chunk.decode('utf-8', errors='replace')
|
|
179
|
+
|
|
180
|
+
# Process carriage return separated updates
|
|
181
|
+
while '\r' in stderr_buffer or '\n' in stderr_buffer:
|
|
182
|
+
# Split on either \r or \n
|
|
183
|
+
sep = '\r' if '\r' in stderr_buffer else '\n'
|
|
184
|
+
line, stderr_buffer = stderr_buffer.split(sep, 1)
|
|
185
|
+
|
|
186
|
+
# Clean ANSI escape codes
|
|
187
|
+
clean_line = re.sub(r'\x1b\[[0-9;]*m', '', line).strip()
|
|
188
|
+
|
|
189
|
+
current_time = time.time()
|
|
190
|
+
|
|
191
|
+
# Parse "Files Downloaded:" progress
|
|
192
|
+
if "Files Downloaded:" in clean_line:
|
|
193
|
+
match = re.search(r'Files Downloaded:\s*(\d+)%.*?(\d+)/(\d+)', clean_line)
|
|
194
|
+
if match:
|
|
195
|
+
pct = match.group(1)
|
|
196
|
+
completed = match.group(2)
|
|
197
|
+
total = match.group(3)
|
|
198
|
+
if current_time - last_progress_update > 0.5:
|
|
199
|
+
self.progress.emit(f"Downloading: {completed}/{total} files ({pct}%)")
|
|
200
|
+
last_progress_update = current_time
|
|
201
|
+
else:
|
|
202
|
+
match = re.search(r'(\d+)%', clean_line)
|
|
203
|
+
if match and current_time - last_progress_update > 0.5:
|
|
204
|
+
pct = match.group(1)
|
|
205
|
+
self.progress.emit(f"Downloading... {pct}%")
|
|
206
|
+
last_progress_update = current_time
|
|
207
|
+
except (BlockingIOError, IOError):
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
# Check if process has finished
|
|
211
|
+
if process.poll() is not None:
|
|
212
|
+
# Read any remaining buffered output
|
|
213
|
+
try:
|
|
214
|
+
remaining_stdout = process.stdout.read()
|
|
215
|
+
if remaining_stdout:
|
|
216
|
+
stdout_buffer += remaining_stdout.decode('utf-8', errors='replace')
|
|
217
|
+
except:
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
# Process remaining stdout
|
|
221
|
+
for line in stdout_buffer.split('\n'):
|
|
222
|
+
line = line.strip()
|
|
223
|
+
if line.startswith("DOWNLOADED_FILES:"):
|
|
224
|
+
try:
|
|
225
|
+
files_json = line.replace("DOWNLOADED_FILES:", "").strip()
|
|
226
|
+
files = json.loads(files_json)
|
|
227
|
+
except json.JSONDecodeError:
|
|
228
|
+
pass
|
|
229
|
+
break
|
|
230
|
+
|
|
231
|
+
if process.returncode != 0:
|
|
232
|
+
self.error.emit("Download failed. Check console for details.")
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
self.progress.emit(f"Download complete! {len(files)} files")
|
|
236
|
+
self.finished.emit(files)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
except Exception as e:
|
|
241
|
+
self.error.emit(str(e))
|
|
242
|
+
|
|
243
|
+
def _generate_download_script(self):
|
|
244
|
+
"""Generate a Python script to run the download."""
|
|
245
|
+
import json
|
|
246
|
+
|
|
247
|
+
params = self.params
|
|
248
|
+
instrument = params.get("instrument")
|
|
249
|
+
|
|
250
|
+
script_lines = [
|
|
251
|
+
"import sys",
|
|
252
|
+
"import json",
|
|
253
|
+
f"sys.path.insert(0, '{os.path.dirname(os.path.dirname(os.path.abspath(__file__)))}' )",
|
|
254
|
+
"from solar_radio_image_viewer.solar_data_downloader import solar_data_downloader as sdd",
|
|
255
|
+
"",
|
|
256
|
+
"try:",
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
if instrument == "AIA":
|
|
260
|
+
skip_cal = params.get("skip_calibration", False)
|
|
261
|
+
apply_psf = params.get("apply_psf", False)
|
|
262
|
+
apply_deg = params.get("apply_degradation", True)
|
|
263
|
+
apply_exp = params.get("apply_exposure_norm", True)
|
|
264
|
+
|
|
265
|
+
if params.get("use_fido", True):
|
|
266
|
+
script_lines.append(f''' files = sdd.download_aia_with_fido(
|
|
267
|
+
wavelength="{params['wavelength']}",
|
|
268
|
+
start_time="{params['start_time']}",
|
|
269
|
+
end_time="{params['end_time']}",
|
|
270
|
+
output_dir="{params['output_dir']}",
|
|
271
|
+
skip_calibration={skip_cal},
|
|
272
|
+
apply_psf={apply_psf},
|
|
273
|
+
apply_degradation={apply_deg},
|
|
274
|
+
apply_exposure_norm={apply_exp},
|
|
275
|
+
)''')
|
|
276
|
+
else:
|
|
277
|
+
email = params.get('email') or 'None'
|
|
278
|
+
script_lines.append(f''' files = sdd.download_aia(
|
|
279
|
+
wavelength="{params['wavelength']}",
|
|
280
|
+
cadence="{params['cadence']}",
|
|
281
|
+
start_time="{params['start_time']}",
|
|
282
|
+
end_time="{params['end_time']}",
|
|
283
|
+
output_dir="{params['output_dir']}",
|
|
284
|
+
email={email!r},
|
|
285
|
+
skip_calibration={skip_cal},
|
|
286
|
+
)''')
|
|
287
|
+
|
|
288
|
+
elif instrument == "HMI":
|
|
289
|
+
skip_cal = params.get("skip_calibration", False)
|
|
290
|
+
if params.get("use_fido", True):
|
|
291
|
+
script_lines.append(f''' files = sdd.download_hmi_with_fido(
|
|
292
|
+
series="{params['series']}",
|
|
293
|
+
start_time="{params['start_time']}",
|
|
294
|
+
end_time="{params['end_time']}",
|
|
295
|
+
output_dir="{params['output_dir']}",
|
|
296
|
+
skip_calibration={skip_cal},
|
|
297
|
+
)''')
|
|
298
|
+
else:
|
|
299
|
+
email = params.get('email') or 'None'
|
|
300
|
+
script_lines.append(f''' files = sdd.download_hmi(
|
|
301
|
+
series="{params['series']}",
|
|
302
|
+
start_time="{params['start_time']}",
|
|
303
|
+
end_time="{params['end_time']}",
|
|
304
|
+
output_dir="{params['output_dir']}",
|
|
305
|
+
email={email!r},
|
|
306
|
+
skip_calibration={skip_cal},
|
|
307
|
+
)''')
|
|
308
|
+
|
|
309
|
+
elif instrument == "IRIS":
|
|
310
|
+
wavelength = params.get('wavelength')
|
|
311
|
+
script_lines.append(f''' files = sdd.download_iris(
|
|
312
|
+
start_time="{params['start_time']}",
|
|
313
|
+
end_time="{params['end_time']}",
|
|
314
|
+
output_dir="{params['output_dir']}",
|
|
315
|
+
obs_type="{params['obs_type']}",
|
|
316
|
+
wavelength={wavelength!r},
|
|
317
|
+
)''')
|
|
318
|
+
|
|
319
|
+
elif instrument == "SOHO":
|
|
320
|
+
wavelength = params.get('wavelength')
|
|
321
|
+
detector = params.get('detector')
|
|
322
|
+
script_lines.append(f''' files = sdd.download_soho(
|
|
323
|
+
instrument="{params['soho_instrument']}",
|
|
324
|
+
start_time="{params['start_time']}",
|
|
325
|
+
end_time="{params['end_time']}",
|
|
326
|
+
output_dir="{params['output_dir']}",
|
|
327
|
+
wavelength={wavelength!r},
|
|
328
|
+
detector={detector!r},
|
|
329
|
+
)''')
|
|
330
|
+
|
|
331
|
+
elif instrument == "GOES SUVI":
|
|
332
|
+
wavelength = params.get('wavelength')
|
|
333
|
+
level = params.get('level', '2')
|
|
334
|
+
script_lines.append(f''' files = sdd.download_goes_suvi(
|
|
335
|
+
start_time="{params['start_time']}",
|
|
336
|
+
end_time="{params['end_time']}",
|
|
337
|
+
output_dir="{params['output_dir']}",
|
|
338
|
+
wavelength={wavelength!r},
|
|
339
|
+
level="{level}",
|
|
340
|
+
)''')
|
|
341
|
+
|
|
342
|
+
elif instrument == "STEREO":
|
|
343
|
+
spacecraft = params.get('spacecraft', 'A')
|
|
344
|
+
stereo_inst = params.get('stereo_instrument', 'EUVI')
|
|
345
|
+
wavelength = params.get('wavelength')
|
|
346
|
+
script_lines.append(f''' files = sdd.download_stereo(
|
|
347
|
+
start_time="{params['start_time']}",
|
|
348
|
+
end_time="{params['end_time']}",
|
|
349
|
+
output_dir="{params['output_dir']}",
|
|
350
|
+
spacecraft="{spacecraft}",
|
|
351
|
+
instrument="{stereo_inst}",
|
|
352
|
+
wavelength={wavelength!r},
|
|
353
|
+
)''')
|
|
354
|
+
|
|
355
|
+
elif instrument == "GONG":
|
|
356
|
+
script_lines.append(f''' files = sdd.download_gong(
|
|
357
|
+
start_time="{params['start_time']}",
|
|
358
|
+
end_time="{params['end_time']}",
|
|
359
|
+
output_dir="{params['output_dir']}",
|
|
360
|
+
)''')
|
|
361
|
+
|
|
362
|
+
# Add output section - flush to ensure real-time output
|
|
363
|
+
script_lines.extend([
|
|
364
|
+
" sys.stdout.flush()",
|
|
365
|
+
" if files:",
|
|
366
|
+
" print('DOWNLOADED_FILES:' + json.dumps(files))",
|
|
367
|
+
" sys.stdout.flush()",
|
|
368
|
+
" else:",
|
|
369
|
+
" print('DOWNLOADED_FILES:[]')",
|
|
370
|
+
" sys.stdout.flush()",
|
|
371
|
+
"except Exception as e:",
|
|
372
|
+
" import traceback",
|
|
373
|
+
" traceback.print_exc()",
|
|
374
|
+
" sys.exit(1)",
|
|
375
|
+
])
|
|
376
|
+
|
|
377
|
+
return "\n".join(script_lines)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
class SolarDataViewerGUI(QMainWindow):
|
|
381
|
+
"""Main window for the Solar Data Viewer GUI application."""
|
|
382
|
+
|
|
383
|
+
def __init__(self, parent=None, initial_datetime=None):
|
|
384
|
+
super().__init__(parent)
|
|
385
|
+
self.setWindowTitle("Solar Data Downloader")
|
|
386
|
+
self.setMinimumWidth(600)
|
|
387
|
+
self.setMinimumHeight(800)
|
|
388
|
+
|
|
389
|
+
# Store initial datetime for time selection
|
|
390
|
+
self.initial_datetime = initial_datetime
|
|
391
|
+
|
|
392
|
+
# Initialize the main widget and layout
|
|
393
|
+
self.main_widget = QWidget()
|
|
394
|
+
self.setCentralWidget(self.main_widget)
|
|
395
|
+
self.layout = QVBoxLayout(self.main_widget)
|
|
396
|
+
|
|
397
|
+
# Create the UI components
|
|
398
|
+
self.create_instrument_selection()
|
|
399
|
+
self.create_parameter_widgets()
|
|
400
|
+
self.create_time_selection()
|
|
401
|
+
self.create_output_selection()
|
|
402
|
+
self.create_calibration_options()
|
|
403
|
+
self.create_download_section()
|
|
404
|
+
|
|
405
|
+
# Initialize the download worker
|
|
406
|
+
self.download_worker = None
|
|
407
|
+
|
|
408
|
+
# Initial update for method visibility
|
|
409
|
+
self.on_instrument_changed(0)
|
|
410
|
+
|
|
411
|
+
def create_instrument_selection(self):
|
|
412
|
+
"""Create the instrument selection section."""
|
|
413
|
+
group = QGroupBox("Select Instrument")
|
|
414
|
+
layout = QVBoxLayout()
|
|
415
|
+
|
|
416
|
+
self.instrument_combo = QComboBox()
|
|
417
|
+
self.instrument_combo.addItems(
|
|
418
|
+
[
|
|
419
|
+
"SDO/AIA (Atmospheric Imaging Assembly)",
|
|
420
|
+
"SDO/HMI (Helioseismic and Magnetic Imager)",
|
|
421
|
+
"IRIS (Interface Region Imaging Spectrograph)",
|
|
422
|
+
"SOHO (Solar and Heliospheric Observatory)",
|
|
423
|
+
"GOES/SUVI (Solar Ultraviolet Imager)",
|
|
424
|
+
"STEREO/SECCHI (Sun Earth Connection)",
|
|
425
|
+
# "GONG (Global Oscillation Network Group)",
|
|
426
|
+
]
|
|
427
|
+
)
|
|
428
|
+
self.instrument_combo.currentIndexChanged.connect(self.on_instrument_changed)
|
|
429
|
+
|
|
430
|
+
layout.addWidget(self.instrument_combo)
|
|
431
|
+
group.setLayout(layout)
|
|
432
|
+
self.layout.addWidget(group)
|
|
433
|
+
|
|
434
|
+
def create_parameter_widgets(self):
|
|
435
|
+
"""Create the parameter selection widgets for each instrument."""
|
|
436
|
+
self.param_group = QGroupBox("Instrument Parameters")
|
|
437
|
+
self.param_layout = QVBoxLayout()
|
|
438
|
+
|
|
439
|
+
# AIA parameters
|
|
440
|
+
self.aia_params = QWidget()
|
|
441
|
+
aia_layout = QVBoxLayout()
|
|
442
|
+
|
|
443
|
+
# Wavelength selection
|
|
444
|
+
wavelength_layout = QHBoxLayout()
|
|
445
|
+
wavelength_layout.addWidget(QLabel("Wavelength:"))
|
|
446
|
+
self.wavelength_combo = QComboBox()
|
|
447
|
+
self.wavelength_combo.addItems(
|
|
448
|
+
[
|
|
449
|
+
"94 Å (Fe XVIII, hot)",
|
|
450
|
+
"131 Å (Fe VIII/XXI)",
|
|
451
|
+
"171 Å (Fe IX, corona)",
|
|
452
|
+
"193 Å (Fe XII/XXIV)",
|
|
453
|
+
"211 Å (Fe XIV)",
|
|
454
|
+
"304 Å (He II)",
|
|
455
|
+
"335 Å (Fe XVI)",
|
|
456
|
+
"1600 Å (C IV, UV)",
|
|
457
|
+
"1700 Å (UV continuum)",
|
|
458
|
+
"4500 Å (Visible)",
|
|
459
|
+
]
|
|
460
|
+
)
|
|
461
|
+
self.wavelength_combo.currentIndexChanged.connect(self.on_aia_wavelength_changed)
|
|
462
|
+
wavelength_layout.addWidget(self.wavelength_combo)
|
|
463
|
+
aia_layout.addLayout(wavelength_layout)
|
|
464
|
+
|
|
465
|
+
# Cadence selection (for DRMS only)
|
|
466
|
+
self.cadence_widget = QWidget()
|
|
467
|
+
cadence_layout = QHBoxLayout()
|
|
468
|
+
cadence_layout.setContentsMargins(0, 0, 0, 0)
|
|
469
|
+
cadence_layout.addWidget(QLabel("Cadence:"))
|
|
470
|
+
self.cadence_combo = QComboBox()
|
|
471
|
+
self.cadence_combo.addItems(["12s", "24s", "1h"])
|
|
472
|
+
cadence_layout.addWidget(self.cadence_combo)
|
|
473
|
+
self.cadence_widget.setLayout(cadence_layout)
|
|
474
|
+
aia_layout.addWidget(self.cadence_widget)
|
|
475
|
+
|
|
476
|
+
self.aia_params.setLayout(aia_layout)
|
|
477
|
+
|
|
478
|
+
# HMI parameters
|
|
479
|
+
self.hmi_params = QWidget()
|
|
480
|
+
hmi_layout = QVBoxLayout()
|
|
481
|
+
|
|
482
|
+
series_layout = QHBoxLayout()
|
|
483
|
+
series_layout.addWidget(QLabel("Series:"))
|
|
484
|
+
self.series_combo = QComboBox()
|
|
485
|
+
self.series_combo.addItems(
|
|
486
|
+
[
|
|
487
|
+
"45s (LOS magnetogram)",
|
|
488
|
+
"720s (LOS magnetogram, 12 min)",
|
|
489
|
+
"B_45s (LOS magnetogram)",
|
|
490
|
+
"B_720s (LOS magnetogram, 12 min)",
|
|
491
|
+
"Ic_45s (Continuum intensity)",
|
|
492
|
+
"Ic_720s (Continuum intensity, 12 min)",
|
|
493
|
+
"V_45s (LOS velocity)",
|
|
494
|
+
"V_720s (LOS velocity, 12 min)",
|
|
495
|
+
]
|
|
496
|
+
)
|
|
497
|
+
series_layout.addWidget(self.series_combo)
|
|
498
|
+
hmi_layout.addLayout(series_layout)
|
|
499
|
+
|
|
500
|
+
self.hmi_params.setLayout(hmi_layout)
|
|
501
|
+
|
|
502
|
+
# IRIS parameters
|
|
503
|
+
self.iris_params = QWidget()
|
|
504
|
+
iris_layout = QVBoxLayout()
|
|
505
|
+
|
|
506
|
+
obs_type_layout = QHBoxLayout()
|
|
507
|
+
obs_type_layout.addWidget(QLabel("Observation Type:"))
|
|
508
|
+
self.obs_type_combo = QComboBox()
|
|
509
|
+
self.obs_type_combo.addItems(["SJI (Slit-Jaw)", "Raster (Spectrograph)"])
|
|
510
|
+
obs_type_layout.addWidget(self.obs_type_combo)
|
|
511
|
+
iris_layout.addLayout(obs_type_layout)
|
|
512
|
+
|
|
513
|
+
iris_wavelength_layout = QHBoxLayout()
|
|
514
|
+
iris_wavelength_layout.addWidget(QLabel("Wavelength:"))
|
|
515
|
+
self.iris_wavelength_combo = QComboBox()
|
|
516
|
+
self.iris_wavelength_combo.addItems(
|
|
517
|
+
[
|
|
518
|
+
"1330 Å (C II)",
|
|
519
|
+
"1400 Å (Si IV)",
|
|
520
|
+
"2796 Å (Mg II k)",
|
|
521
|
+
"2832 Å (Photosphere)",
|
|
522
|
+
]
|
|
523
|
+
)
|
|
524
|
+
iris_wavelength_layout.addWidget(self.iris_wavelength_combo)
|
|
525
|
+
iris_layout.addLayout(iris_wavelength_layout)
|
|
526
|
+
|
|
527
|
+
self.iris_params.setLayout(iris_layout)
|
|
528
|
+
|
|
529
|
+
# SOHO parameters
|
|
530
|
+
self.soho_params = QWidget()
|
|
531
|
+
soho_layout = QVBoxLayout()
|
|
532
|
+
|
|
533
|
+
soho_instrument_layout = QHBoxLayout()
|
|
534
|
+
soho_instrument_layout.addWidget(QLabel("SOHO Instrument:"))
|
|
535
|
+
self.soho_instrument_combo = QComboBox()
|
|
536
|
+
self.soho_instrument_combo.addItems(["EIT", "LASCO", "MDI"])
|
|
537
|
+
self.soho_instrument_combo.currentIndexChanged.connect(
|
|
538
|
+
self.on_soho_instrument_changed
|
|
539
|
+
)
|
|
540
|
+
soho_instrument_layout.addWidget(self.soho_instrument_combo)
|
|
541
|
+
soho_layout.addLayout(soho_instrument_layout)
|
|
542
|
+
|
|
543
|
+
# SOHO EIT wavelength
|
|
544
|
+
self.soho_eit_params = QWidget()
|
|
545
|
+
eit_layout = QHBoxLayout()
|
|
546
|
+
eit_layout.setContentsMargins(0, 0, 0, 0)
|
|
547
|
+
eit_layout.addWidget(QLabel("Wavelength:"))
|
|
548
|
+
self.eit_wavelength_combo = QComboBox()
|
|
549
|
+
self.eit_wavelength_combo.addItems(
|
|
550
|
+
["171 Å (Fe IX/X)", "195 Å (Fe XII)", "284 Å (Fe XV)", "304 Å (He II)"]
|
|
551
|
+
)
|
|
552
|
+
eit_layout.addWidget(self.eit_wavelength_combo)
|
|
553
|
+
self.soho_eit_params.setLayout(eit_layout)
|
|
554
|
+
|
|
555
|
+
# SOHO LASCO detector
|
|
556
|
+
self.soho_lasco_params = QWidget()
|
|
557
|
+
lasco_layout = QHBoxLayout()
|
|
558
|
+
lasco_layout.setContentsMargins(0, 0, 0, 0)
|
|
559
|
+
lasco_layout.addWidget(QLabel("Detector:"))
|
|
560
|
+
self.lasco_detector_combo = QComboBox()
|
|
561
|
+
self.lasco_detector_combo.addItems(["C2 (2-6 Rs)", "C3 (3.7-30 Rs)"])
|
|
562
|
+
lasco_layout.addWidget(self.lasco_detector_combo)
|
|
563
|
+
self.soho_lasco_params.setLayout(lasco_layout)
|
|
564
|
+
|
|
565
|
+
soho_layout.addWidget(self.soho_eit_params)
|
|
566
|
+
soho_layout.addWidget(self.soho_lasco_params)
|
|
567
|
+
self.soho_params.setLayout(soho_layout)
|
|
568
|
+
|
|
569
|
+
# GOES SUVI parameters
|
|
570
|
+
self.suvi_params = QWidget()
|
|
571
|
+
suvi_layout = QVBoxLayout()
|
|
572
|
+
|
|
573
|
+
suvi_wavelength_layout = QHBoxLayout()
|
|
574
|
+
suvi_wavelength_layout.addWidget(QLabel("Wavelength:"))
|
|
575
|
+
self.suvi_wavelength_combo = QComboBox()
|
|
576
|
+
self.suvi_wavelength_combo.addItems(
|
|
577
|
+
[
|
|
578
|
+
"94 Å (Fe XVIII)",
|
|
579
|
+
"131 Å (Fe VIII/XXI)",
|
|
580
|
+
"171 Å (Fe IX)",
|
|
581
|
+
"195 Å (Fe XII)",
|
|
582
|
+
"284 Å (Fe XV)",
|
|
583
|
+
"304 Å (He II)",
|
|
584
|
+
]
|
|
585
|
+
)
|
|
586
|
+
suvi_wavelength_layout.addWidget(self.suvi_wavelength_combo)
|
|
587
|
+
suvi_layout.addLayout(suvi_wavelength_layout)
|
|
588
|
+
|
|
589
|
+
suvi_level_layout = QHBoxLayout()
|
|
590
|
+
suvi_level_layout.addWidget(QLabel("Data Level:"))
|
|
591
|
+
self.suvi_level_combo = QComboBox()
|
|
592
|
+
self.suvi_level_combo.addItems(["Level 2 (calibrated)", "Level 1b (raw)"])
|
|
593
|
+
suvi_level_layout.addWidget(self.suvi_level_combo)
|
|
594
|
+
suvi_layout.addLayout(suvi_level_layout)
|
|
595
|
+
|
|
596
|
+
self.suvi_params.setLayout(suvi_layout)
|
|
597
|
+
|
|
598
|
+
# STEREO parameters
|
|
599
|
+
self.stereo_params = QWidget()
|
|
600
|
+
stereo_layout = QVBoxLayout()
|
|
601
|
+
|
|
602
|
+
stereo_sc_layout = QHBoxLayout()
|
|
603
|
+
stereo_sc_layout.addWidget(QLabel("Spacecraft:"))
|
|
604
|
+
self.stereo_sc_combo = QComboBox()
|
|
605
|
+
self.stereo_sc_combo.addItems(["STEREO-A", "STEREO-B (pre-2014 only)"])
|
|
606
|
+
stereo_sc_layout.addWidget(self.stereo_sc_combo)
|
|
607
|
+
stereo_layout.addLayout(stereo_sc_layout)
|
|
608
|
+
|
|
609
|
+
stereo_inst_layout = QHBoxLayout()
|
|
610
|
+
stereo_inst_layout.addWidget(QLabel("Instrument:"))
|
|
611
|
+
self.stereo_inst_combo = QComboBox()
|
|
612
|
+
self.stereo_inst_combo.addItems(
|
|
613
|
+
["EUVI (EUV Imager)", "COR1 (Inner coronagraph)", "COR2 (Outer coronagraph)"]
|
|
614
|
+
)
|
|
615
|
+
self.stereo_inst_combo.currentIndexChanged.connect(self.on_stereo_instrument_changed)
|
|
616
|
+
stereo_inst_layout.addWidget(self.stereo_inst_combo)
|
|
617
|
+
stereo_layout.addLayout(stereo_inst_layout)
|
|
618
|
+
|
|
619
|
+
# STEREO EUVI wavelength
|
|
620
|
+
self.stereo_euvi_params = QWidget()
|
|
621
|
+
euvi_layout = QHBoxLayout()
|
|
622
|
+
euvi_layout.setContentsMargins(0, 0, 0, 0)
|
|
623
|
+
euvi_layout.addWidget(QLabel("Wavelength:"))
|
|
624
|
+
self.stereo_wavelength_combo = QComboBox()
|
|
625
|
+
self.stereo_wavelength_combo.addItems(
|
|
626
|
+
["171 Å", "195 Å", "284 Å", "304 Å"]
|
|
627
|
+
)
|
|
628
|
+
euvi_layout.addWidget(self.stereo_wavelength_combo)
|
|
629
|
+
self.stereo_euvi_params.setLayout(euvi_layout)
|
|
630
|
+
stereo_layout.addWidget(self.stereo_euvi_params)
|
|
631
|
+
|
|
632
|
+
self.stereo_params.setLayout(stereo_layout)
|
|
633
|
+
|
|
634
|
+
# GONG parameters (minimal - just magnetograms)
|
|
635
|
+
self.gong_params = QWidget()
|
|
636
|
+
gong_layout = QVBoxLayout()
|
|
637
|
+
gong_label = QLabel("GONG provides magnetogram data.\nNo additional parameters needed.")
|
|
638
|
+
gong_label.setStyleSheet("color: gray; font-style: italic;")
|
|
639
|
+
gong_layout.addWidget(gong_label)
|
|
640
|
+
self.gong_params.setLayout(gong_layout)
|
|
641
|
+
|
|
642
|
+
# Add all parameter widgets to the group
|
|
643
|
+
self.param_layout.addWidget(self.aia_params)
|
|
644
|
+
self.param_layout.addWidget(self.hmi_params)
|
|
645
|
+
self.param_layout.addWidget(self.iris_params)
|
|
646
|
+
self.param_layout.addWidget(self.soho_params)
|
|
647
|
+
self.param_layout.addWidget(self.suvi_params)
|
|
648
|
+
self.param_layout.addWidget(self.stereo_params)
|
|
649
|
+
self.param_layout.addWidget(self.gong_params)
|
|
650
|
+
|
|
651
|
+
self.param_group.setLayout(self.param_layout)
|
|
652
|
+
self.layout.addWidget(self.param_group)
|
|
653
|
+
|
|
654
|
+
# Hide all except AIA initially
|
|
655
|
+
self.hmi_params.hide()
|
|
656
|
+
self.iris_params.hide()
|
|
657
|
+
self.soho_params.hide()
|
|
658
|
+
self.soho_eit_params.hide()
|
|
659
|
+
self.soho_lasco_params.hide()
|
|
660
|
+
self.suvi_params.hide()
|
|
661
|
+
self.stereo_params.hide()
|
|
662
|
+
self.gong_params.hide()
|
|
663
|
+
|
|
664
|
+
def create_time_selection(self):
|
|
665
|
+
"""Create the time range selection section."""
|
|
666
|
+
group = QGroupBox("Time Range")
|
|
667
|
+
layout = QVBoxLayout()
|
|
668
|
+
|
|
669
|
+
# Start time
|
|
670
|
+
start_layout = QHBoxLayout()
|
|
671
|
+
start_layout.addWidget(QLabel("Start:"))
|
|
672
|
+
self.start_datetime = QDateTimeEdit()
|
|
673
|
+
self.start_datetime.setCalendarPopup(True)
|
|
674
|
+
self.start_datetime.setDisplayFormat("yyyy.MM.dd HH:mm:ss")
|
|
675
|
+
|
|
676
|
+
# Use initial_datetime if provided, otherwise current time
|
|
677
|
+
if self.initial_datetime:
|
|
678
|
+
from PyQt5.QtCore import QDateTime
|
|
679
|
+
self.start_datetime.setDateTime(QDateTime(self.initial_datetime))
|
|
680
|
+
else:
|
|
681
|
+
self.start_datetime.setDateTime(QDateTime.currentDateTime())
|
|
682
|
+
|
|
683
|
+
# Connect to sync end time when start time changes
|
|
684
|
+
self.start_datetime.dateTimeChanged.connect(self.on_start_datetime_changed)
|
|
685
|
+
start_layout.addWidget(self.start_datetime)
|
|
686
|
+
layout.addLayout(start_layout)
|
|
687
|
+
|
|
688
|
+
# End time
|
|
689
|
+
end_layout = QHBoxLayout()
|
|
690
|
+
end_layout.addWidget(QLabel("End:"))
|
|
691
|
+
self.end_datetime = QDateTimeEdit()
|
|
692
|
+
self.end_datetime.setCalendarPopup(True)
|
|
693
|
+
self.end_datetime.setDisplayFormat("yyyy.MM.dd HH:mm:ss")
|
|
694
|
+
|
|
695
|
+
# Use initial_datetime + 1 hour if provided, otherwise current time + 1 hour
|
|
696
|
+
if self.initial_datetime:
|
|
697
|
+
from datetime import timedelta
|
|
698
|
+
from PyQt5.QtCore import QDateTime
|
|
699
|
+
end_dt = self.initial_datetime + timedelta(hours=1)
|
|
700
|
+
self.end_datetime.setDateTime(QDateTime(end_dt))
|
|
701
|
+
else:
|
|
702
|
+
self.end_datetime.setDateTime(
|
|
703
|
+
QDateTime.currentDateTime().addSecs(3600) # Default to 1 hour later
|
|
704
|
+
)
|
|
705
|
+
end_layout.addWidget(self.end_datetime)
|
|
706
|
+
layout.addLayout(end_layout)
|
|
707
|
+
|
|
708
|
+
# Cadence info label
|
|
709
|
+
self.cadence_label = QLabel()
|
|
710
|
+
self.cadence_label.setStyleSheet("color: #888; font-style: italic;")
|
|
711
|
+
self.cadence_label.setWordWrap(True)
|
|
712
|
+
self.update_cadence_info() # Set initial value
|
|
713
|
+
layout.addWidget(self.cadence_label)
|
|
714
|
+
|
|
715
|
+
group.setLayout(layout)
|
|
716
|
+
self.layout.addWidget(group)
|
|
717
|
+
|
|
718
|
+
def on_start_datetime_changed(self, new_datetime):
|
|
719
|
+
"""Sync end time when start time is changed (keep 1 hour difference)."""
|
|
720
|
+
# Set end time to start time + 1 hour
|
|
721
|
+
end_dt = new_datetime.addSecs(3600)
|
|
722
|
+
self.end_datetime.setDateTime(end_dt)
|
|
723
|
+
|
|
724
|
+
def create_output_selection(self):
|
|
725
|
+
"""Create the output directory selection section."""
|
|
726
|
+
group = QGroupBox("Output Settings")
|
|
727
|
+
layout = QVBoxLayout()
|
|
728
|
+
|
|
729
|
+
# Output directory
|
|
730
|
+
dir_layout = QHBoxLayout()
|
|
731
|
+
dir_layout.addWidget(QLabel("Output Directory:"))
|
|
732
|
+
self.output_dir = QLineEdit()
|
|
733
|
+
self.output_dir.setText(os.path.join(os.getcwd(), "solar_data"))
|
|
734
|
+
dir_layout.addWidget(self.output_dir)
|
|
735
|
+
|
|
736
|
+
browse_button = QPushButton("Browse...")
|
|
737
|
+
browse_button.clicked.connect(self.browse_output_dir)
|
|
738
|
+
dir_layout.addWidget(browse_button)
|
|
739
|
+
layout.addLayout(dir_layout)
|
|
740
|
+
|
|
741
|
+
# Download method section
|
|
742
|
+
self.method_widget = QWidget()
|
|
743
|
+
method_layout = QHBoxLayout()
|
|
744
|
+
method_layout.setContentsMargins(0, 0, 0, 0)
|
|
745
|
+
method_layout.addWidget(QLabel("Download Method:"))
|
|
746
|
+
self.method_group = QButtonGroup()
|
|
747
|
+
|
|
748
|
+
self.fido_radio = QRadioButton("Fido (recommended)")
|
|
749
|
+
self.fido_radio.setChecked(True) # Fido is now default
|
|
750
|
+
self.method_group.addButton(self.fido_radio, 1)
|
|
751
|
+
method_layout.addWidget(self.fido_radio)
|
|
752
|
+
|
|
753
|
+
self.drms_radio = QRadioButton("DRMS")
|
|
754
|
+
self.method_group.addButton(self.drms_radio, 0)
|
|
755
|
+
method_layout.addWidget(self.drms_radio)
|
|
756
|
+
|
|
757
|
+
self.method_group.buttonClicked.connect(self.on_method_changed)
|
|
758
|
+
self.method_widget.setLayout(method_layout)
|
|
759
|
+
layout.addWidget(self.method_widget)
|
|
760
|
+
|
|
761
|
+
# Email for DRMS (hidden by default since Fido is default)
|
|
762
|
+
self.email_widget = QWidget()
|
|
763
|
+
email_layout = QHBoxLayout()
|
|
764
|
+
email_layout.setContentsMargins(0, 0, 0, 0)
|
|
765
|
+
email_layout.addWidget(QLabel("Email (for DRMS):"))
|
|
766
|
+
self.email_input = QLineEdit()
|
|
767
|
+
self.email_input.setPlaceholderText("Required for DRMS downloads")
|
|
768
|
+
email_layout.addWidget(self.email_input)
|
|
769
|
+
self.email_widget.setLayout(email_layout)
|
|
770
|
+
self.email_widget.hide() # Hidden by default
|
|
771
|
+
layout.addWidget(self.email_widget)
|
|
772
|
+
|
|
773
|
+
group.setLayout(layout)
|
|
774
|
+
self.layout.addWidget(group)
|
|
775
|
+
|
|
776
|
+
def create_calibration_options(self):
|
|
777
|
+
"""Create the calibration options section."""
|
|
778
|
+
group = QGroupBox("Calibration Options")
|
|
779
|
+
layout = QVBoxLayout()
|
|
780
|
+
|
|
781
|
+
# Main calibration checkbox
|
|
782
|
+
self.calibrate_checkbox = QCheckBox("Apply Level 1.5 Calibration")
|
|
783
|
+
self.calibrate_checkbox.setChecked(True)
|
|
784
|
+
self.calibrate_checkbox.setToolTip(
|
|
785
|
+
"Apply standard calibration (recommended for scientific use)\n"
|
|
786
|
+
"Includes: pointing correction, rotation, scaling, centering"
|
|
787
|
+
)
|
|
788
|
+
self.calibrate_checkbox.stateChanged.connect(self.on_calibration_changed)
|
|
789
|
+
layout.addWidget(self.calibrate_checkbox)
|
|
790
|
+
|
|
791
|
+
# Advanced options container
|
|
792
|
+
self.calib_options_widget = QWidget()
|
|
793
|
+
calib_layout = QVBoxLayout()
|
|
794
|
+
calib_layout.setContentsMargins(20, 0, 0, 0) # Indent
|
|
795
|
+
|
|
796
|
+
# AIA-specific options
|
|
797
|
+
self.aia_calib_label = QLabel("AIA Advanced Options:")
|
|
798
|
+
self.aia_calib_label.setStyleSheet("font-weight: bold;")
|
|
799
|
+
calib_layout.addWidget(self.aia_calib_label)
|
|
800
|
+
|
|
801
|
+
self.psf_checkbox = QCheckBox("PSF Deconvolution")
|
|
802
|
+
self.psf_checkbox.setChecked(False) # Off by default (slow)
|
|
803
|
+
self.psf_checkbox.setToolTip(
|
|
804
|
+
"Apply Point Spread Function deconvolution to sharpen images.\n"
|
|
805
|
+
"⚠️ WARNING: This is VERY SLOW (~30-60 seconds per image)\n"
|
|
806
|
+
"Uses Richardson-Lucy algorithm with 25 iterations.\n"
|
|
807
|
+
"Downloads PSF data from JSOC on first use."
|
|
808
|
+
)
|
|
809
|
+
calib_layout.addWidget(self.psf_checkbox)
|
|
810
|
+
|
|
811
|
+
# Warning label for PSF
|
|
812
|
+
self.psf_warning = QLabel("⚠️ PSF deconvolution might take a while")
|
|
813
|
+
self.psf_warning.setStyleSheet("color: orange; font-size: 11px;")
|
|
814
|
+
self.psf_warning.hide()
|
|
815
|
+
self.psf_checkbox.stateChanged.connect(
|
|
816
|
+
lambda state: self.psf_warning.setVisible(state == 2)
|
|
817
|
+
)
|
|
818
|
+
calib_layout.addWidget(self.psf_warning)
|
|
819
|
+
|
|
820
|
+
self.degradation_checkbox = QCheckBox("Degradation Correction")
|
|
821
|
+
self.degradation_checkbox.setChecked(True)
|
|
822
|
+
self.degradation_checkbox.setToolTip(
|
|
823
|
+
"Apply time-dependent degradation correction.\n"
|
|
824
|
+
"Compensates for instrument sensitivity changes over time."
|
|
825
|
+
)
|
|
826
|
+
calib_layout.addWidget(self.degradation_checkbox)
|
|
827
|
+
|
|
828
|
+
self.exposure_norm_checkbox = QCheckBox("Exposure Time Normalization")
|
|
829
|
+
self.exposure_norm_checkbox.setChecked(True)
|
|
830
|
+
self.exposure_norm_checkbox.setToolTip(
|
|
831
|
+
"Normalize data by exposure time.\n"
|
|
832
|
+
"Converts DN to DN/s for consistent comparison."
|
|
833
|
+
)
|
|
834
|
+
calib_layout.addWidget(self.exposure_norm_checkbox)
|
|
835
|
+
|
|
836
|
+
self.calib_options_widget.setLayout(calib_layout)
|
|
837
|
+
layout.addWidget(self.calib_options_widget)
|
|
838
|
+
|
|
839
|
+
group.setLayout(layout)
|
|
840
|
+
self.layout.addWidget(group)
|
|
841
|
+
|
|
842
|
+
def on_calibration_changed(self, state):
|
|
843
|
+
"""Handle calibration checkbox state changes."""
|
|
844
|
+
self.calib_options_widget.setVisible(state == 2)
|
|
845
|
+
|
|
846
|
+
def create_download_section(self):
|
|
847
|
+
"""Create the download button and progress section."""
|
|
848
|
+
# Progress bar
|
|
849
|
+
self.progress_bar = QProgressBar()
|
|
850
|
+
self.progress_bar.setTextVisible(True)
|
|
851
|
+
self.progress_bar.hide()
|
|
852
|
+
self.layout.addWidget(self.progress_bar)
|
|
853
|
+
|
|
854
|
+
# Status label
|
|
855
|
+
self.status_label = QLabel()
|
|
856
|
+
self.status_label.setWordWrap(True)
|
|
857
|
+
self.layout.addWidget(self.status_label)
|
|
858
|
+
|
|
859
|
+
# Download button
|
|
860
|
+
self.download_button = QPushButton("Download")
|
|
861
|
+
self.download_button.clicked.connect(self.start_download)
|
|
862
|
+
self.layout.addWidget(self.download_button)
|
|
863
|
+
|
|
864
|
+
def browse_output_dir(self):
|
|
865
|
+
"""Open a directory selection dialog."""
|
|
866
|
+
dir_path = QFileDialog.getExistingDirectory(
|
|
867
|
+
self,
|
|
868
|
+
"Select Output Directory",
|
|
869
|
+
self.output_dir.text(),
|
|
870
|
+
QFileDialog.ShowDirsOnly,
|
|
871
|
+
)
|
|
872
|
+
if dir_path:
|
|
873
|
+
self.output_dir.setText(dir_path)
|
|
874
|
+
|
|
875
|
+
def get_instrument_name(self, index):
|
|
876
|
+
"""Get short instrument name from index."""
|
|
877
|
+
names = ["AIA", "HMI", "IRIS", "SOHO", "GOES SUVI", "STEREO", "GONG"]
|
|
878
|
+
return names[index] if index < len(names) else "Unknown"
|
|
879
|
+
|
|
880
|
+
def on_instrument_changed(self, index):
|
|
881
|
+
"""Handle instrument selection changes."""
|
|
882
|
+
# Hide all parameter widgets
|
|
883
|
+
self.aia_params.hide()
|
|
884
|
+
self.hmi_params.hide()
|
|
885
|
+
self.iris_params.hide()
|
|
886
|
+
self.soho_params.hide()
|
|
887
|
+
self.suvi_params.hide()
|
|
888
|
+
self.stereo_params.hide()
|
|
889
|
+
self.gong_params.hide()
|
|
890
|
+
|
|
891
|
+
# Show the selected instrument's parameters
|
|
892
|
+
if index == 0: # AIA
|
|
893
|
+
self.aia_params.show()
|
|
894
|
+
elif index == 1: # HMI
|
|
895
|
+
self.hmi_params.show()
|
|
896
|
+
elif index == 2: # IRIS
|
|
897
|
+
self.iris_params.show()
|
|
898
|
+
elif index == 3: # SOHO
|
|
899
|
+
self.soho_params.show()
|
|
900
|
+
self.on_soho_instrument_changed(self.soho_instrument_combo.currentIndex())
|
|
901
|
+
elif index == 4: # GOES SUVI
|
|
902
|
+
self.suvi_params.show()
|
|
903
|
+
elif index == 5: # STEREO
|
|
904
|
+
self.stereo_params.show()
|
|
905
|
+
self.on_stereo_instrument_changed(self.stereo_inst_combo.currentIndex())
|
|
906
|
+
elif index == 6: # GONG
|
|
907
|
+
self.gong_params.show()
|
|
908
|
+
|
|
909
|
+
# Handle Fido-only instruments
|
|
910
|
+
instrument_name = self.get_instrument_name(index)
|
|
911
|
+
if instrument_name in FIDO_ONLY_INSTRUMENTS:
|
|
912
|
+
# Force Fido and hide method selection
|
|
913
|
+
self.fido_radio.setChecked(True)
|
|
914
|
+
self.method_widget.hide()
|
|
915
|
+
self.email_widget.hide()
|
|
916
|
+
self.cadence_widget.hide()
|
|
917
|
+
else:
|
|
918
|
+
# Show method selection for AIA/HMI
|
|
919
|
+
self.method_widget.show()
|
|
920
|
+
self.on_method_changed()
|
|
921
|
+
|
|
922
|
+
# Show AIA-specific calibration options only for AIA
|
|
923
|
+
if index == 0: # AIA
|
|
924
|
+
self.aia_calib_label.show()
|
|
925
|
+
self.psf_checkbox.show()
|
|
926
|
+
self.psf_warning.setVisible(self.psf_checkbox.isChecked())
|
|
927
|
+
self.degradation_checkbox.show()
|
|
928
|
+
self.exposure_norm_checkbox.show()
|
|
929
|
+
else:
|
|
930
|
+
self.aia_calib_label.hide()
|
|
931
|
+
self.psf_checkbox.hide()
|
|
932
|
+
self.psf_warning.hide()
|
|
933
|
+
self.degradation_checkbox.hide()
|
|
934
|
+
self.exposure_norm_checkbox.hide()
|
|
935
|
+
|
|
936
|
+
# Update cadence info for the selected instrument
|
|
937
|
+
self.update_cadence_info()
|
|
938
|
+
|
|
939
|
+
def on_method_changed(self, button=None):
|
|
940
|
+
"""Handle download method changes."""
|
|
941
|
+
use_fido = self.method_group.checkedId() == 1
|
|
942
|
+
|
|
943
|
+
if use_fido:
|
|
944
|
+
self.email_widget.hide()
|
|
945
|
+
self.cadence_widget.hide() # Fido doesn't use cadence
|
|
946
|
+
else:
|
|
947
|
+
self.email_widget.show()
|
|
948
|
+
# Show cadence only for AIA with DRMS
|
|
949
|
+
if self.instrument_combo.currentIndex() == 0:
|
|
950
|
+
self.cadence_widget.show()
|
|
951
|
+
self.on_aia_wavelength_changed() # Update cadence based on wavelength
|
|
952
|
+
|
|
953
|
+
def on_aia_wavelength_changed(self, index=None):
|
|
954
|
+
"""Auto-select cadence based on AIA wavelength."""
|
|
955
|
+
wavelength_text = self.wavelength_combo.currentText()
|
|
956
|
+
|
|
957
|
+
if "1600" in wavelength_text or "1700" in wavelength_text:
|
|
958
|
+
self.cadence_combo.setCurrentIndex(1) # 24s
|
|
959
|
+
elif "4500" in wavelength_text:
|
|
960
|
+
self.cadence_combo.setCurrentIndex(2) # 1h
|
|
961
|
+
else:
|
|
962
|
+
self.cadence_combo.setCurrentIndex(0) # 12s
|
|
963
|
+
|
|
964
|
+
self.update_cadence_info()
|
|
965
|
+
|
|
966
|
+
def on_soho_instrument_changed(self, index):
|
|
967
|
+
"""Handle SOHO instrument selection changes."""
|
|
968
|
+
self.soho_eit_params.hide()
|
|
969
|
+
self.soho_lasco_params.hide()
|
|
970
|
+
|
|
971
|
+
if index == 0: # EIT
|
|
972
|
+
self.soho_eit_params.show()
|
|
973
|
+
elif index == 1: # LASCO
|
|
974
|
+
self.soho_lasco_params.show()
|
|
975
|
+
|
|
976
|
+
self.update_cadence_info()
|
|
977
|
+
|
|
978
|
+
def on_stereo_instrument_changed(self, index):
|
|
979
|
+
"""Handle STEREO instrument selection changes."""
|
|
980
|
+
if index == 0: # EUVI
|
|
981
|
+
self.stereo_euvi_params.show()
|
|
982
|
+
else: # COR1/COR2
|
|
983
|
+
self.stereo_euvi_params.hide()
|
|
984
|
+
self.update_cadence_info()
|
|
985
|
+
|
|
986
|
+
def update_cadence_info(self):
|
|
987
|
+
"""Update the cadence info label based on selected instrument and parameters."""
|
|
988
|
+
index = self.instrument_combo.currentIndex()
|
|
989
|
+
|
|
990
|
+
# Cadence info for each instrument
|
|
991
|
+
cadence_info = {
|
|
992
|
+
0: { # AIA
|
|
993
|
+
"default": "12s (EUV), 24s (UV), 1h (4500Å)",
|
|
994
|
+
"wavelengths": {
|
|
995
|
+
"94": "12s", "131": "12s", "171": "12s", "193": "12s",
|
|
996
|
+
"211": "12s", "304": "12s", "335": "12s",
|
|
997
|
+
"1600": "24s", "1700": "24s", "4500": "1 hour"
|
|
998
|
+
}
|
|
999
|
+
},
|
|
1000
|
+
1: { # HMI
|
|
1001
|
+
"default": "45s (magnetogram), 45s (continuum), 45s (Doppler)"
|
|
1002
|
+
},
|
|
1003
|
+
2: { # IRIS
|
|
1004
|
+
"default": "Variable (depends on observing program, typically minutes)"
|
|
1005
|
+
},
|
|
1006
|
+
3: { # SOHO
|
|
1007
|
+
"EIT": "~12 min (synoptic), 1-6 min (campaign)",
|
|
1008
|
+
"LASCO": "~12-30 min (C2/C3)",
|
|
1009
|
+
"MDI": "1 min (discontinued 2011)"
|
|
1010
|
+
},
|
|
1011
|
+
4: { # GOES SUVI
|
|
1012
|
+
"default": "4 min (Level 2 composites)"
|
|
1013
|
+
},
|
|
1014
|
+
5: { # STEREO
|
|
1015
|
+
"EUVI": "2.5-10 min (wavelength dependent)",
|
|
1016
|
+
"COR1": "5 min",
|
|
1017
|
+
"COR2": "15-30 min"
|
|
1018
|
+
},
|
|
1019
|
+
6: { # GONG
|
|
1020
|
+
"default": "1 min (magnetograms)"
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if index == 0: # AIA - show wavelength-specific cadence
|
|
1025
|
+
wl = self.wavelength_combo.currentText().split()[0]
|
|
1026
|
+
wl_cadences = cadence_info[0]["wavelengths"]
|
|
1027
|
+
cadence = wl_cadences.get(wl, "12s")
|
|
1028
|
+
self.cadence_label.setText(f"ℹ️ Typical cadence: {cadence}")
|
|
1029
|
+
elif index == 3: # SOHO - depends on sub-instrument
|
|
1030
|
+
soho_inst = self.soho_instrument_combo.currentText()
|
|
1031
|
+
cadence = cadence_info[3].get(soho_inst, "Variable")
|
|
1032
|
+
self.cadence_label.setText(f"ℹ️ Typical cadence: {cadence}")
|
|
1033
|
+
elif index == 5: # STEREO - depends on sub-instrument
|
|
1034
|
+
stereo_inst = self.stereo_inst_combo.currentText()
|
|
1035
|
+
if "EUVI" in stereo_inst:
|
|
1036
|
+
cadence = cadence_info[5]["EUVI"]
|
|
1037
|
+
elif "COR1" in stereo_inst:
|
|
1038
|
+
cadence = cadence_info[5]["COR1"]
|
|
1039
|
+
else:
|
|
1040
|
+
cadence = cadence_info[5]["COR2"]
|
|
1041
|
+
self.cadence_label.setText(f"ℹ️ Typical cadence: {cadence}")
|
|
1042
|
+
else:
|
|
1043
|
+
cadence = cadence_info.get(index, {}).get("default", "Variable")
|
|
1044
|
+
self.cadence_label.setText(f"ℹ️ Typical cadence: {cadence}")
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
def get_download_parameters(self) -> dict:
|
|
1048
|
+
"""Gather all parameters needed for the download."""
|
|
1049
|
+
instrument_index = self.instrument_combo.currentIndex()
|
|
1050
|
+
start_time = self.start_datetime.dateTime().toString("yyyy.MM.dd HH:mm:ss")
|
|
1051
|
+
end_time = self.end_datetime.dateTime().toString("yyyy.MM.dd HH:mm:ss")
|
|
1052
|
+
output_dir = self.output_dir.text()
|
|
1053
|
+
use_fido = self.method_group.checkedId() == 1
|
|
1054
|
+
email = self.email_input.text() if not use_fido else None
|
|
1055
|
+
|
|
1056
|
+
params = {
|
|
1057
|
+
"start_time": start_time,
|
|
1058
|
+
"end_time": end_time,
|
|
1059
|
+
"output_dir": output_dir,
|
|
1060
|
+
"use_fido": use_fido,
|
|
1061
|
+
"email": email,
|
|
1062
|
+
# Calibration options
|
|
1063
|
+
"skip_calibration": not self.calibrate_checkbox.isChecked(),
|
|
1064
|
+
"apply_psf": self.psf_checkbox.isChecked(),
|
|
1065
|
+
"apply_degradation": self.degradation_checkbox.isChecked(),
|
|
1066
|
+
"apply_exposure_norm": self.exposure_norm_checkbox.isChecked(),
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if instrument_index == 0: # AIA
|
|
1070
|
+
params.update(
|
|
1071
|
+
{
|
|
1072
|
+
"instrument": "AIA",
|
|
1073
|
+
"wavelength": self.wavelength_combo.currentText().split()[0],
|
|
1074
|
+
"cadence": self.cadence_combo.currentText(),
|
|
1075
|
+
}
|
|
1076
|
+
)
|
|
1077
|
+
elif instrument_index == 1: # HMI
|
|
1078
|
+
params.update(
|
|
1079
|
+
{
|
|
1080
|
+
"instrument": "HMI",
|
|
1081
|
+
"series": self.series_combo.currentText().split()[0],
|
|
1082
|
+
}
|
|
1083
|
+
)
|
|
1084
|
+
elif instrument_index == 2: # IRIS
|
|
1085
|
+
obs_type = "SJI" if "SJI" in self.obs_type_combo.currentText() else "raster"
|
|
1086
|
+
params.update(
|
|
1087
|
+
{
|
|
1088
|
+
"instrument": "IRIS",
|
|
1089
|
+
"obs_type": obs_type,
|
|
1090
|
+
"wavelength": self.iris_wavelength_combo.currentText().split()[0],
|
|
1091
|
+
}
|
|
1092
|
+
)
|
|
1093
|
+
elif instrument_index == 3: # SOHO
|
|
1094
|
+
soho_instrument = self.soho_instrument_combo.currentText()
|
|
1095
|
+
params.update({"instrument": "SOHO", "soho_instrument": soho_instrument})
|
|
1096
|
+
|
|
1097
|
+
if soho_instrument == "EIT":
|
|
1098
|
+
params["wavelength"] = self.eit_wavelength_combo.currentText().split()[0]
|
|
1099
|
+
elif soho_instrument == "LASCO":
|
|
1100
|
+
params["detector"] = self.lasco_detector_combo.currentText().split()[0]
|
|
1101
|
+
|
|
1102
|
+
elif instrument_index == 4: # GOES SUVI
|
|
1103
|
+
params.update(
|
|
1104
|
+
{
|
|
1105
|
+
"instrument": "GOES SUVI",
|
|
1106
|
+
"wavelength": self.suvi_wavelength_combo.currentText().split()[0],
|
|
1107
|
+
"level": "2" if "Level 2" in self.suvi_level_combo.currentText() else "1b",
|
|
1108
|
+
}
|
|
1109
|
+
)
|
|
1110
|
+
elif instrument_index == 5: # STEREO
|
|
1111
|
+
spacecraft = "A" if "STEREO-A" in self.stereo_sc_combo.currentText() else "B"
|
|
1112
|
+
stereo_inst_text = self.stereo_inst_combo.currentText()
|
|
1113
|
+
if "EUVI" in stereo_inst_text:
|
|
1114
|
+
stereo_inst = "EUVI"
|
|
1115
|
+
elif "COR1" in stereo_inst_text:
|
|
1116
|
+
stereo_inst = "COR1"
|
|
1117
|
+
else:
|
|
1118
|
+
stereo_inst = "COR2"
|
|
1119
|
+
|
|
1120
|
+
params.update(
|
|
1121
|
+
{
|
|
1122
|
+
"instrument": "STEREO",
|
|
1123
|
+
"spacecraft": spacecraft,
|
|
1124
|
+
"stereo_instrument": stereo_inst,
|
|
1125
|
+
}
|
|
1126
|
+
)
|
|
1127
|
+
if stereo_inst == "EUVI":
|
|
1128
|
+
params["wavelength"] = self.stereo_wavelength_combo.currentText().split()[0]
|
|
1129
|
+
|
|
1130
|
+
elif instrument_index == 6: # GONG
|
|
1131
|
+
params.update({"instrument": "GONG"})
|
|
1132
|
+
|
|
1133
|
+
return params
|
|
1134
|
+
|
|
1135
|
+
def start_download(self):
|
|
1136
|
+
"""Start the download process."""
|
|
1137
|
+
try:
|
|
1138
|
+
# Create output directory if it doesn't exist
|
|
1139
|
+
Path(self.output_dir.text()).mkdir(parents=True, exist_ok=True)
|
|
1140
|
+
|
|
1141
|
+
# Disable the download button and show progress
|
|
1142
|
+
self.download_button.setEnabled(False)
|
|
1143
|
+
self.progress_bar.setMaximum(0) # Indeterminate progress
|
|
1144
|
+
self.progress_bar.show()
|
|
1145
|
+
self.status_label.setText("Preparing download...")
|
|
1146
|
+
|
|
1147
|
+
# Create and start the download worker
|
|
1148
|
+
params = self.get_download_parameters()
|
|
1149
|
+
self.download_worker = DownloadWorker(params)
|
|
1150
|
+
self.download_worker.progress.connect(self.update_progress)
|
|
1151
|
+
self.download_worker.finished.connect(self.download_finished)
|
|
1152
|
+
self.download_worker.error.connect(self.download_error)
|
|
1153
|
+
self.download_worker.start()
|
|
1154
|
+
|
|
1155
|
+
except Exception as e:
|
|
1156
|
+
QMessageBox.critical(self, "Error", f"Failed to start download: {str(e)}")
|
|
1157
|
+
self.download_button.setEnabled(True)
|
|
1158
|
+
self.progress_bar.hide()
|
|
1159
|
+
|
|
1160
|
+
def update_progress(self, message):
|
|
1161
|
+
"""Update the progress display."""
|
|
1162
|
+
self.status_label.setText(message)
|
|
1163
|
+
|
|
1164
|
+
def download_finished(self, files):
|
|
1165
|
+
"""Handle download completion."""
|
|
1166
|
+
self.download_button.setEnabled(True)
|
|
1167
|
+
self.progress_bar.hide()
|
|
1168
|
+
|
|
1169
|
+
if files:
|
|
1170
|
+
message = f"Download complete! Downloaded {len(files)} files to {self.output_dir.text()}"
|
|
1171
|
+
self.status_label.setText(message)
|
|
1172
|
+
QMessageBox.information(self, "Success", message)
|
|
1173
|
+
else:
|
|
1174
|
+
QMessageBox.warning(self, "Warning", "No files were downloaded.")
|
|
1175
|
+
|
|
1176
|
+
def download_error(self, error_message):
|
|
1177
|
+
"""Handle download errors."""
|
|
1178
|
+
self.download_button.setEnabled(True)
|
|
1179
|
+
self.progress_bar.hide()
|
|
1180
|
+
self.status_label.setText(f"Error: {error_message}")
|
|
1181
|
+
QMessageBox.critical(self, "Error", f"Download failed: {error_message}")
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
def launch_gui(parent=None, initial_datetime=None) -> SolarDataViewerGUI:
|
|
1185
|
+
"""
|
|
1186
|
+
Launch the Solar Data Viewer GUI.
|
|
1187
|
+
|
|
1188
|
+
Args:
|
|
1189
|
+
parent: Optional parent widget for integration with other PyQt applications
|
|
1190
|
+
initial_datetime: Optional datetime to initialize the time selectors
|
|
1191
|
+
|
|
1192
|
+
Returns:
|
|
1193
|
+
SolarDataViewerGUI: The main window instance
|
|
1194
|
+
"""
|
|
1195
|
+
if not QApplication.instance():
|
|
1196
|
+
app = QApplication(sys.argv)
|
|
1197
|
+
else:
|
|
1198
|
+
app = QApplication.instance()
|
|
1199
|
+
|
|
1200
|
+
window = SolarDataViewerGUI(parent, initial_datetime=initial_datetime)
|
|
1201
|
+
window.show()
|
|
1202
|
+
|
|
1203
|
+
if parent is None:
|
|
1204
|
+
sys.exit(app.exec_())
|
|
1205
|
+
|
|
1206
|
+
return window
|
|
1207
|
+
|
|
1208
|
+
|
|
1209
|
+
if __name__ == "__main__":
|
|
1210
|
+
launch_gui()
|